snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
1
.coveralls.yml
Normal file
1
.coveralls.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
service_name: travis-pro
|
||||||
5
.github/CODEOWNERS
vendored
Normal file
5
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
* @nats-io/server
|
||||||
|
*.go @nats-io/server-dev
|
||||||
|
doc/ @nats-io/server-dev
|
||||||
|
test/ @nats-io/server-dev
|
||||||
|
locksordering.txt @nats-io/server-dev
|
||||||
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
# NATS.io
|
||||||
|
custom: "https://crowdfunding.linuxfoundation.org/initiatives/nats-io"
|
||||||
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discussion
|
||||||
|
url: https://github.com/nats-io/nats-server/discussions
|
||||||
|
about: Ideal for ideas, feedback, or longer form questions.
|
||||||
|
- name: Chat
|
||||||
|
url: https://slack.nats.io
|
||||||
|
about: Ideal for short, one-off questions, general conversation, and meeting other NATS users!
|
||||||
54
.github/ISSUE_TEMPLATE/defect.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/defect.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: Defect
|
||||||
|
description: Report a defect, such as a bug or regression.
|
||||||
|
labels:
|
||||||
|
- defect
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: observed
|
||||||
|
attributes:
|
||||||
|
label: Observed behavior
|
||||||
|
description: Describe the unexpected behavior or performance regression you are observing.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Describe the expected behavior or performance characteristics.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: versions
|
||||||
|
attributes:
|
||||||
|
label: Server and client version
|
||||||
|
description: |-
|
||||||
|
**One of the first things we are likely to ask you is if the defect reproduces on the latest version, so please check that first!**
|
||||||
|
For the server, use `nats-server --version`, check the startup log output, or the image tag pulled from Docker.
|
||||||
|
For the CLI client, use `nats --version`.
|
||||||
|
For language-specific clients, check the version downloaded by the language dependency manager.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Host environment
|
||||||
|
description: |-
|
||||||
|
Specify any relevant details about the host environment the server and/or client was running in,
|
||||||
|
such as operating system, CPU architecture, container runtime, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide as many concrete steps to reproduce the defect. Please also include stream info and consumer info if this issue is JetStream-related.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-ai-meat-proxy
|
||||||
|
attributes:
|
||||||
|
label: Submission acknowledgment
|
||||||
|
options:
|
||||||
|
- label: I am a human being writing in my own words and not an AI agent. I will not use an AI agent to communicate on my behalf in this issue, either directly or via copy-paste.
|
||||||
|
required: true
|
||||||
35
.github/ISSUE_TEMPLATE/proposal.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/proposal.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Proposal
|
||||||
|
description: Propose an enhancement or new feature.
|
||||||
|
labels:
|
||||||
|
- proposal
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: change
|
||||||
|
attributes:
|
||||||
|
label: Proposed change
|
||||||
|
description: This could be a behavior change, enhanced API, or a new feature.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: usecase
|
||||||
|
attributes:
|
||||||
|
label: Use case
|
||||||
|
description: What is the use case or general motivation for this proposal?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: contribute
|
||||||
|
attributes:
|
||||||
|
label: Contribution
|
||||||
|
description: |-
|
||||||
|
Are you intending or interested in contributing code for this proposal if accepted?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: no-ai-meat-proxy
|
||||||
|
attributes:
|
||||||
|
label: Submission acknowledgment
|
||||||
|
options:
|
||||||
|
- label: I am a human being writing in my own words and not an AI agent. I will not use an AI agent to communicate on my behalf in this issue, either directly or via copy-paste.
|
||||||
|
required: true
|
||||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!-- Please make sure to read @CONTRIBUTING.md, then delete this notice and replace it with your PR description. Commit sign-offs certify that the contribution is your original work and that you license the work to the project under the Apache-2.0 license. We cannot accept contributions without it. We accept AI-assisted contributions but AI agents should not open pull requests directly. -->
|
||||||
47
.github/actions/nightly-release/action.yaml
vendored
Normal file
47
.github/actions/nightly-release/action.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Nightly Docker Releaser
|
||||||
|
description: Builds nightly docker images
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
hub_username:
|
||||||
|
description: Docker hub username
|
||||||
|
required: true
|
||||||
|
|
||||||
|
hub_password:
|
||||||
|
description: Docker hub password
|
||||||
|
required: true
|
||||||
|
|
||||||
|
name:
|
||||||
|
description: The name of the build
|
||||||
|
default: nightly
|
||||||
|
required: false
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
shell: bash
|
||||||
|
run: docker login -u "${{ inputs.hub_username }}" -p "${{ inputs.hub_password }}"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Generate build metadata
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
echo "BUILD_DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
|
||||||
|
echo "GIT_COMMIT=$(cd ${{ inputs.workdir }}/nats-server && git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
echo "BUILD_NAME=$(echo ${{ inputs.name }} | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9.-]/-/g')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: "${{ github.workspace }}/src/github.com/nats-io"
|
||||||
|
file: "${{ github.workspace }}/src/github.com/nats-io/nats-server/docker/Dockerfile.nightly"
|
||||||
|
build-args: |
|
||||||
|
VERSION=nightly-${{ env.BUILD_DATE }}
|
||||||
|
GIT_COMMIT=${{ env.GIT_COMMIT }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
synadia/nats-server:${{ env.BUILD_NAME }}
|
||||||
|
synadia/nats-server:${{ env.BUILD_NAME }}-${{ env.BUILD_DATE }}
|
||||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
64
.github/workflows/claude.yml
vendored
Normal file
64
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Claude Code
|
||||||
|
|
||||||
|
# GITHUB_TOKEN needs contents:read and actions:read — required by
|
||||||
|
# claude-code-action for restoring trusted config files from the base branch.
|
||||||
|
# All other GitHub API access uses the App token.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude:
|
||||||
|
name: Claude Review
|
||||||
|
uses: synadia-io/ai-workflows/.github/workflows/claude.yml@v2
|
||||||
|
if: contains(
|
||||||
|
fromJson('["OWNER","MEMBER","COLLABORATOR"]'),
|
||||||
|
github.event.comment.author_association || github.event.pull_request.author_association
|
||||||
|
)
|
||||||
|
with:
|
||||||
|
gh_app_id: ${{ vars.CLAUDE_GH_APP_ID }}
|
||||||
|
checkout_mode: "base"
|
||||||
|
# review_focus is used only by the auto-review job (pull_request_target), not the
|
||||||
|
# interactive @claude job — so the check below only needs github.event.pull_request,
|
||||||
|
# which pull_request_target populates. On issue_comment events that field is absent,
|
||||||
|
# so this evaluates to startsWith(null, 'backports/') -> false; harmless, since the
|
||||||
|
# interactive job ignores review_focus. Can't be fixed in-expression: issue_comment
|
||||||
|
# payloads carry no head ref at all.
|
||||||
|
review_focus: |
|
||||||
|
${{ startsWith(github.event.pull_request.head.ref, 'backports/') && '
|
||||||
|
This is a BACKPORT / RELEASE pull request. Every cherry-picked commit
|
||||||
|
originates from a PR that was already independently reviewed and approved
|
||||||
|
on the source branch. Do NOT re-review the individual code changes and do
|
||||||
|
NOT comment on the implementation of cherry-picked hunks — assume they are
|
||||||
|
correct.
|
||||||
|
|
||||||
|
Instead, focus exclusively on:
|
||||||
|
- Cherry-pick completeness. Inspect the PRs/commits included in this
|
||||||
|
backport. For each source PR, check its milestone, labels, and target
|
||||||
|
branch to decide whether it belongs in this release line. Flag any PR
|
||||||
|
that targets this release but is MISSING here, and any included PR that
|
||||||
|
does not belong.
|
||||||
|
- Non-clean picks. Call out commits that appear to have been modified
|
||||||
|
during the cherry-pick (conflict resolution, adapted context) — those
|
||||||
|
are the only changes that warrant a closer look.
|
||||||
|
- Release overview. Produce a concise summary of what this release ships,
|
||||||
|
grouped by area (JetStream, Raft/NRG, leafnodes, security, etc.), so
|
||||||
|
reviewers can sanity-check the scope at a glance.
|
||||||
|
' || '
|
||||||
|
Additionally focus on:
|
||||||
|
- Performance implications (hot paths, allocations, lock contention)
|
||||||
|
- Concurrency safety (goroutine leaks, race conditions, deadlocks)
|
||||||
|
- Raft consensus and JetStream clustering correctness
|
||||||
|
- Security boundaries (authentication, authorization, TLS handling)
|
||||||
|
' }}
|
||||||
|
secrets:
|
||||||
|
claude_oauth_token: ${{ secrets.CLAUDE_OAUTH_TOKEN }}
|
||||||
|
gh_app_private_key: ${{ secrets.CLAUDE_GH_APP_PRIVATE_KEY }}
|
||||||
53
.github/workflows/cov.yaml
vendored
Normal file
53
.github/workflows/cov.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: NATS Server Code Coverage
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
schedule:
|
||||||
|
- cron: "40 4 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nightly_coverage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GOPATH: /home/runner/work/nats-server
|
||||||
|
GO111MODULE: "on"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
path: src/github.com/nats-io/nats-server
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
cache-dependency-path: src/github.com/nats-io/nats-server/go.sum
|
||||||
|
|
||||||
|
- name: Run code coverage
|
||||||
|
shell: bash --noprofile --norc -x -eo pipefail {0}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd src/github.com/nats-io/nats-server
|
||||||
|
./scripts/cov.sh upload
|
||||||
|
set +e
|
||||||
|
|
||||||
|
- name: Convert coverage.out to coverage.lcov
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit e4612787670fc5b5f49026b8c29c5569921de1db = tag v1.2.0
|
||||||
|
uses: jandelgado/gcov2lcov-action@e4612787670fc5b5f49026b8c29c5569921de1db
|
||||||
|
with:
|
||||||
|
infile: acc.out
|
||||||
|
working-directory: src/github.com/nats-io/nats-server
|
||||||
|
|
||||||
|
- name: Coveralls
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit 5cbfd81b66ca5d10c19b062c04de0199c215fb6e = tag v2.3.7
|
||||||
|
uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.github_token }}
|
||||||
|
file: src/github.com/nats-io/nats-server/coverage.lcov
|
||||||
33
.github/workflows/long-tests.yaml
vendored
Normal file
33
.github/workflows/long-tests.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: NATS Server Long Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Allow manual trigger (any branch)
|
||||||
|
workflow_dispatch:
|
||||||
|
# Run daily at 12:30 on default branch
|
||||||
|
schedule:
|
||||||
|
- cron: "30 12 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# At most one of these workflow per ref running
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
# New one cancels in-progress one
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
js-long:
|
||||||
|
name: Long JetStream tests
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -v -run='^TestLong.*' ./server -tags=include_js_long_tests -count=1 -vet=off -timeout=60m -shuffle on -p 1 -failfast
|
||||||
53
.github/workflows/mqtt-test.yaml
vendored
Normal file
53
.github/workflows/mqtt-test.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: MQTT External Tests
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
env:
|
||||||
|
GOPATH: /home/runner/work/nats-server
|
||||||
|
GO111MODULE: "on"
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_SMALL || 'ubuntu-latest' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
path: src/github.com/nats-io/nats-server
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
cache-dependency-path: src/github.com/nats-io/nats-server/go.sum
|
||||||
|
|
||||||
|
- name: Set up testing tools and environment
|
||||||
|
shell: bash --noprofile --norc -eo pipefail {0}
|
||||||
|
id: setup
|
||||||
|
run: |
|
||||||
|
wget https://github.com/hivemq/mqtt-cli/releases/download/v4.20.0/mqtt-cli-4.20.0.deb
|
||||||
|
sudo apt install ./mqtt-cli-4.20.0.deb
|
||||||
|
# Pinned to the mqtt-test commit that adds --pipeline (PR #5);
|
||||||
|
# bump to @v0.2.0 once it is tagged.
|
||||||
|
go install github.com/ConnectEverything/mqtt-test@6138352776422dfd16eeb3333d078358d8b85fea
|
||||||
|
|
||||||
|
- name: Run tests (3 times to detect flappers)
|
||||||
|
shell: bash --noprofile --norc -eo pipefail {0}
|
||||||
|
run: |
|
||||||
|
cd src/github.com/nats-io/nats-server
|
||||||
|
go test -v --count=3 --run='TestXMQTT' ./server
|
||||||
|
|
||||||
|
- name: Run tests with --race
|
||||||
|
shell: bash --noprofile --norc -eo pipefail {0}
|
||||||
|
run: |
|
||||||
|
cd src/github.com/nats-io/nats-server
|
||||||
|
go test -v --race --failfast --run='TestXMQTT' ./server
|
||||||
|
|
||||||
|
- name: Run benchmarks
|
||||||
|
shell: bash --noprofile --norc -eo pipefail {0}
|
||||||
|
run: |
|
||||||
|
cd src/github.com/nats-io/nats-server
|
||||||
|
go test --run='-' --count=3 --bench 'BenchmarkXMQTT' --benchtime=100x ./server
|
||||||
|
|
||||||
|
# TODO: compare benchmarks
|
||||||
51
.github/workflows/nightly.yaml
vendored
Normal file
51
.github/workflows/nightly.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Docker Nightly
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
description: "Override source branch (optional)"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
|
||||||
|
schedule:
|
||||||
|
- cron: "40 4 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_RELEASE || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout NATS
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
path: src/github.com/nats-io/nats-server
|
||||||
|
ref: ${{ inputs.target || 'main' }}
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Checkout NSC
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: nats-io/nsc
|
||||||
|
path: src/github.com/nats-io/nsc
|
||||||
|
fetch-depth: 1
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Checkout NATS CLI
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: nats-io/natscli
|
||||||
|
path: src/github.com/nats-io/natscli
|
||||||
|
fetch-depth: 1
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Build Docker nightly
|
||||||
|
uses: ./src/github.com/nats-io/nats-server/.github/actions/nightly-release
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.target || 'nightly' }}
|
||||||
|
hub_username: "${{ secrets.DOCKER_USERNAME }}"
|
||||||
|
hub_password: "${{ secrets.DOCKER_PASSWORD }}"
|
||||||
59
.github/workflows/pull-requests.yml
vendored
Normal file
59
.github/workflows/pull-requests.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: NATS Server PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
signoffs:
|
||||||
|
name: Sign-offs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout base repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch PR head
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git fetch --no-tags origin "$BASE_SHA" "pull/${PR_NUMBER}/head:pr-head"
|
||||||
|
|
||||||
|
- name: Check all PR commits are signed off
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
range="${BASE_SHA}..pr-head"
|
||||||
|
missing=0
|
||||||
|
missing_list=""
|
||||||
|
|
||||||
|
while read -r c; do
|
||||||
|
msg="$(git log -1 --pretty=%B "$c")"
|
||||||
|
if ! printf '%s\n' "$msg" | grep -q '^Signed-off-by:'; then
|
||||||
|
echo "::error ::Commit $c has not been signed off"
|
||||||
|
missing=1
|
||||||
|
missing_list+=$'\n- '"$c"
|
||||||
|
fi
|
||||||
|
done < <(git rev-list --no-merges "$range")
|
||||||
|
|
||||||
|
if [ "$missing" -ne 0 ]; then
|
||||||
|
body="$(printf 'Please review the `CONTRIBUTING.md` guide as some commits are missing `Signed-off-by:` in their commit messages.\n\nTo correct, please amend the following commits and force-push:\n\n%s' "$missing_list")"
|
||||||
|
gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
54
.github/workflows/release.yaml
vendored
Normal file
54
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: NATS Server Releases
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
name: GitHub Release
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_RELEASE || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "stable"
|
||||||
|
|
||||||
|
- name: Check version matches tag
|
||||||
|
env:
|
||||||
|
TRAVIS_TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
go test -race -v -run=TestVersionMatchesTag ./server -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -count=1 -vet=off
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit 6f9f17788090df1f26f669e9d70d6ae9567deba6 = tag v4.1.2
|
||||||
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
|
||||||
|
|
||||||
|
- name: Install syft
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit e22c389904149dbc22b58101806040fa8d37a610 = tag v0.24.0
|
||||||
|
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610
|
||||||
|
with:
|
||||||
|
syft-version: "v1.42.3"
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: "~> v2"
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
21
.github/workflows/stale-issues.yaml
vendored
Normal file
21
.github/workflows/stale-issues.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Stale Issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
stale-issue-label: stale
|
||||||
|
stale-pr-label: stale
|
||||||
|
days-before-stale: 56 # Mark stale after 8 weeks (56 days) of inactivity
|
||||||
|
days-before-close: -1 # Disable auto-closing
|
||||||
|
exempt-all-milestones: true # Any issue/PR within a milestone will be omitted
|
||||||
372
.github/workflows/tests.yaml
vendored
Normal file
372
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
name: NATS Server Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
RACE: ${{ (github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') && github.event_name != 'pull_request') && '-race' || '' }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit 82606bf257cbaff209d206a39f5134f0cfbfd2ee = tag v9.2.1
|
||||||
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee
|
||||||
|
with:
|
||||||
|
version: v2.13.0
|
||||||
|
skip-cache: true
|
||||||
|
skip-save-cache: true
|
||||||
|
args: --timeout=5m --config=.golangci.yml
|
||||||
|
|
||||||
|
build-latest:
|
||||||
|
name: Build (Latest Go)
|
||||||
|
needs: [lint]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Build NATS Server 64-bit
|
||||||
|
run: GOARCH=amd64 GOOS=linux go build
|
||||||
|
|
||||||
|
- name: Build NATS Server 32-bit
|
||||||
|
run: GOARCH=386 GOOS=linux go build
|
||||||
|
|
||||||
|
build-supported:
|
||||||
|
name: Build (Minimum Go)
|
||||||
|
needs: [lint]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
|
- name: Build NATS Server 64-bit
|
||||||
|
run: GOARCH=amd64 GOOS=linux go build
|
||||||
|
|
||||||
|
- name: Build NATS Server 32-bit
|
||||||
|
run: GOARCH=386 GOOS=linux go build
|
||||||
|
|
||||||
|
# Using GitHub-supplied workers for Windows for now.
|
||||||
|
# Note that the below testing steps depend on the Linux build
|
||||||
|
# only, as the Windows builds take a fair bit longer to set up.
|
||||||
|
build-windows:
|
||||||
|
name: Build (Minimum Go, ${{ matrix.os }})
|
||||||
|
needs: [lint]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-2022, windows-2025]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
|
- name: Build NATS Server
|
||||||
|
run: go build
|
||||||
|
|
||||||
|
store:
|
||||||
|
name: Test Stores
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh store_tests
|
||||||
|
|
||||||
|
js-no-cluster:
|
||||||
|
name: Test JetStream
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_tests
|
||||||
|
|
||||||
|
raft:
|
||||||
|
name: Test Raft
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh raft_tests
|
||||||
|
|
||||||
|
js-consumers:
|
||||||
|
name: Test JetStream Consumers
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_consumer_tests
|
||||||
|
|
||||||
|
js-cluster-1:
|
||||||
|
name: Test JetStream Cluster 1
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_1
|
||||||
|
|
||||||
|
js-cluster-2:
|
||||||
|
name: Test JetStream Cluster 2
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_2
|
||||||
|
|
||||||
|
js-cluster-3:
|
||||||
|
name: Test JetStream Cluster 3
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_3
|
||||||
|
|
||||||
|
js-cluster-4:
|
||||||
|
name: Test JetStream Cluster 4
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_4
|
||||||
|
|
||||||
|
js-supercluster:
|
||||||
|
name: Test JetStream Supercluster
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh js_super_cluster_tests
|
||||||
|
|
||||||
|
no-race-1:
|
||||||
|
name: Test No-Race 1
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh no_race_1_tests
|
||||||
|
|
||||||
|
no-race-2:
|
||||||
|
name: Test No-Race 2
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh no_race_2_tests
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
name: Test MQTT
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh mqtt_tests
|
||||||
|
|
||||||
|
msgtrace:
|
||||||
|
name: Test Message Tracing
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh msgtrace_tests
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
name: Test JWT
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh jwt_tests
|
||||||
|
|
||||||
|
server-pkg-non-js:
|
||||||
|
name: Test Remaining Server Tests
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh srv_pkg_non_js_tests
|
||||||
|
|
||||||
|
non-server-pkg:
|
||||||
|
name: Test Other Packages
|
||||||
|
needs: [build-latest, build-supported, lint]
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./scripts/runTestsOnTravis.sh non_srv_pkg_tests
|
||||||
33
.github/workflows/vuln.yaml
vendored
Normal file
33
.github/workflows/vuln.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Vulnerability Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
govulncheck:
|
||||||
|
runs-on: ${{ vars.GHA_WORKER_SMALL || 'ubuntu-latest' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Run govulncheck
|
||||||
|
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
|
||||||
|
# Commit b625fbe08f3bccbe446d94fbf87fcc875a4f50ee = tag v1.0.4
|
||||||
|
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee
|
||||||
|
with:
|
||||||
|
go-package: ./...
|
||||||
|
output-format: sarif
|
||||||
|
output-file: govulncheck.sarif
|
||||||
|
|
||||||
|
- name: Upload SARIF results
|
||||||
|
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
|
||||||
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
|
with:
|
||||||
|
sarif_file: govulncheck.sarif
|
||||||
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Configuration Files
|
||||||
|
*.conf
|
||||||
|
*.cfg
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.project
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Emacs
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
.\#*
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# bin
|
||||||
|
nats-server
|
||||||
|
gnatsd
|
||||||
|
check
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Cross compiled binaries
|
||||||
|
pkg
|
||||||
90
.golangci.yml
Normal file
90
.golangci.yml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
concurrency: 4
|
||||||
|
modules-download-mode: readonly
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: true
|
||||||
|
output:
|
||||||
|
formats:
|
||||||
|
text:
|
||||||
|
path: stdout
|
||||||
|
print-linter-name: true
|
||||||
|
print-issued-lines: true
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- forbidigo
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- pattern: ^fmt\.Print(f|ln)?$
|
||||||
|
govet:
|
||||||
|
settings:
|
||||||
|
printf:
|
||||||
|
funcs:
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -QF*
|
||||||
|
- -ST1003
|
||||||
|
- -ST1016
|
||||||
|
- -SA1019
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- forbidigo
|
||||||
|
path: main.go
|
||||||
|
- linters:
|
||||||
|
- forbidigo
|
||||||
|
source: "nats-server: v%s"
|
||||||
|
- linters:
|
||||||
|
- forbidigo
|
||||||
|
path: server/opts.go
|
||||||
|
- linters:
|
||||||
|
- forbidigo
|
||||||
|
path: _test.go
|
||||||
|
paths:
|
||||||
|
- .github
|
||||||
|
- doc
|
||||||
|
- docker
|
||||||
|
- logos
|
||||||
|
- scripts
|
||||||
|
- util
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- .github
|
||||||
|
- doc
|
||||||
|
- docker
|
||||||
|
- logos
|
||||||
|
- scripts
|
||||||
|
- util
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
118
.goreleaser.yml
Normal file
118
.goreleaser.yml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
project_name: nats-server
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: '{{ envOrDefault "GITHUB_REPOSITORY_OWNER" "nats-io" }}'
|
||||||
|
name: '{{ envOrDefault "GITHUB_REPOSITORY_NAME" "nats-server" }}'
|
||||||
|
name_template: "Release {{.Tag}}"
|
||||||
|
draft: true
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
disable: true
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- main: .
|
||||||
|
binary: nats-server
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -w -X 'github.com/nats-io/nats-server/v2/server.gitCommit={{.ShortCommit}}' -X 'github.com/nats-io/nats-server/v2/server.serverVersion={{.Tag}}'
|
||||||
|
env:
|
||||||
|
# This is the toolchain version we use for releases. To override, set the env var, e.g.:
|
||||||
|
# GORELEASER_TOOLCHAIN="go1.22.8" TARGET='linux_amd64' goreleaser build --snapshot --clean --single-target
|
||||||
|
- GOTOOLCHAIN={{ envOrDefault "GORELEASER_TOOLCHAIN" "go1.27.1" }}
|
||||||
|
- GO111MODULE=on
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- freebsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
- 386
|
||||||
|
- loong64
|
||||||
|
- mips64le
|
||||||
|
- s390x
|
||||||
|
- ppc64le
|
||||||
|
# RISC-V currently only supported on Linux
|
||||||
|
- riscv64
|
||||||
|
goarm:
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
|
ignore:
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: 386
|
||||||
|
- goos: darwin
|
||||||
|
goarch: riscv64
|
||||||
|
- goos: windows
|
||||||
|
goarch: riscv64
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: riscv64
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
|
nfpms:
|
||||||
|
- file_name_template: "{{.ProjectName}}-{{.Tag}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}"
|
||||||
|
homepage: https://nats.io
|
||||||
|
description: High-Performance server for NATS, the cloud native messaging system.
|
||||||
|
maintainer: Ivan Kozlovic <ivan@synadia.com>
|
||||||
|
license: Apache 2.0
|
||||||
|
vendor: Synadia Inc.
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
mtime: "{{ .CommitDate }}"
|
||||||
|
contents:
|
||||||
|
- src: /usr/bin/nats-server
|
||||||
|
dst: /usr/local/bin/nats-server
|
||||||
|
type: "symlink"
|
||||||
|
rpm:
|
||||||
|
buildhost: synadia.com
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- name_template: "{{.ProjectName}}-{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}"
|
||||||
|
id: targz-archives
|
||||||
|
wrap_in_directory: true
|
||||||
|
formats: ["tar.gz"]
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats: ["zip"]
|
||||||
|
builds_info:
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: 0755
|
||||||
|
files:
|
||||||
|
- src: README.md
|
||||||
|
info:
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: 0644
|
||||||
|
mtime: "{{ .CommitDate }}"
|
||||||
|
- src: LICENSE
|
||||||
|
info:
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: 0644
|
||||||
|
mtime: "{{ .CommitDate }}"
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "SHA256SUMS"
|
||||||
|
algorithm: sha256
|
||||||
|
|
||||||
|
sboms:
|
||||||
|
- artifacts: binary
|
||||||
|
documents:
|
||||||
|
[
|
||||||
|
"{{.ProjectName}}-{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}.sbom.spdx.json",
|
||||||
|
]
|
||||||
5
AMBASSADORS.md
Normal file
5
AMBASSADORS.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Ambassadors
|
||||||
|
|
||||||
|
The NATS ambassador program recognizes community members that go above and beyond in their contributions to the community and the ecosystem. Learn more [here](https://nats.io/community#nats-ambassador-program).
|
||||||
|
|
||||||
|
- [Maurice van Veen](https://nats.io/community#maurice-van-veen) <contact@mauricevanveen.com>
|
||||||
3
CODE-OF-CONDUCT.md
Normal file
3
CODE-OF-CONDUCT.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Community Code of Conduct
|
||||||
|
|
||||||
|
NATS follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||||
52
CONTRIBUTING.md
Normal file
52
CONTRIBUTING.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thanks for your interest in contributing! This document contains `nats-io/nats-server` specific contributing details. If you are a first-time contributor, please refer to the general [NATS Contributor Guide](https://nats.io/contributing/) to get a comprehensive overview of contributing to the NATS project.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
There are three general ways you can contribute to this repo:
|
||||||
|
|
||||||
|
- Proposing an enhancement or new feature
|
||||||
|
- Reporting a bug or regression
|
||||||
|
- Contributing changes to the source code
|
||||||
|
|
||||||
|
For the first two, refer to the [GitHub Issues](https://github.com/nats-io/nats-server/issues/new/choose) which guides you through the available options along with the needed information to collect.
|
||||||
|
|
||||||
|
## Contributing changes
|
||||||
|
|
||||||
|
_Prior to opening a pull request, it is recommended to open an issue first to ensure the maintainers can review intended changes. This saves time for both you and us. Exceptions to this rule include fixing non-functional source such as code comments, documentation or other supporting files._
|
||||||
|
|
||||||
|
Proposing source code changes is done through GitHub's standard pull request workflow.
|
||||||
|
|
||||||
|
If your branch is a work-in-progress then please start by creating your pull requests as draft, by clicking the down-arrow next to the `Create pull request` button and instead selecting `Create draft pull request`.
|
||||||
|
|
||||||
|
This will defer the automatic process of requesting a review from the NATS team and significantly reduces noise until you are ready. Once you are happy, you can click the `Ready for review` button.
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
A good pull request includes:
|
||||||
|
|
||||||
|
- A succinct yet descriptive title describing, in a few words, what is fixed/improved/optimised by this change.
|
||||||
|
- A high-level description of the changes, including an overview of _why_ the changes are relevant or what problem you are trying to solve. Include links to any issues that are related by adding comments like `Resolves #NNN` to your description. See [Linking a Pull Request to an Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) for more information.
|
||||||
|
- An up-to-date parent commit. Please make sure you are pulling in the latest `main` branch and rebasing your work on top of it, i.e. `git rebase main`.
|
||||||
|
- Unit tests where appropriate. Bug fixes will benefit from the addition of regression tests and performance improvements will benefit from the addition of new benchmarks where possible. New features will **NOT** be accepted without suitable test coverage!
|
||||||
|
- No more commits than necessary. Sometimes having multiple commits is useful for telling a story or isolating changes from one another, but please squash down any unnecessary commits that may just be for clean-up, comments or small changes.
|
||||||
|
- No additional external dependencies that aren't absolutely essential. Please do everything you can to avoid pulling in additional libraries/dependencies into `go.mod` as we will be very critical of these.
|
||||||
|
|
||||||
|
### AI Policy
|
||||||
|
|
||||||
|
We are happy to accept AI-assisted contributions, but we are not happy to talk to your AI agent during the process. When submitting an issue or a pull request, communicate with us in your own words and [do not meat-proxy](https://nomeatproxy.com). If we wanted to talk to an AI, we can do it ourselves! If you are submitting code, we expect you to understand how the code works and that it has been appropriately tested, even if an AI has written or reviewed it.
|
||||||
|
|
||||||
|
We also use AI review tools for pull requests, but it is always a human that makes the final decision on whether or not to accept it, so please engage faithfully during the code review process.
|
||||||
|
|
||||||
|
### Sign-off
|
||||||
|
|
||||||
|
In order to accept a contribution, you will first need to certify that the contribution is your original work and that you license the work to the project under the [Apache-2.0 license](https://github.com/nats-io/nats-server/blob/main/LICENSE).
|
||||||
|
|
||||||
|
This is done by using `Signed-off-by: Your Name <your.email@example.com>` statement which should appear in all of your commit messages. Please note that we can only accept sign-offs under a legal name. Nicknames and aliases are not permitted.
|
||||||
|
|
||||||
|
To perform a sign-off when committing with `git`, use `git commit -s` (or `--signoff`) to add the `Signed-off-by:` trailer to your commit message.
|
||||||
|
|
||||||
|
## Get help
|
||||||
|
|
||||||
|
If you have questions about the contribution process, please start a [GitHub discussion](https://github.com/nats-io/nats-server/discussions), join the [NATS Slack](https://slack.nats.io/), or send your question to the [NATS Google Group](https://groups.google.com/forum/#!forum/natsio).
|
||||||
18
DEPENDENCIES.md
Normal file
18
DEPENDENCIES.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# External Dependencies
|
||||||
|
|
||||||
|
This file lists the dependencies used in this repository.
|
||||||
|
|
||||||
|
| Dependency | License |
|
||||||
|
|-|-|
|
||||||
|
| Go | BSD 3-Clause "New" or "Revised" License |
|
||||||
|
| github.com/nats-io/nats-server/v2 | Apache License 2.0 |
|
||||||
|
| github.com/google/go-tpm | Apache License 2.0 |
|
||||||
|
| github.com/klauspost/compress | BSD 3-Clause "New" or "Revised" License |
|
||||||
|
| github.com/minio/highwayhash | Apache License 2.0 |
|
||||||
|
| github.com/nats-io/jwt/v2 | Apache License 2.0 |
|
||||||
|
| github.com/nats-io/nats.go | Apache License 2.0 |
|
||||||
|
| github.com/nats-io/nkeys | Apache License 2.0 |
|
||||||
|
| github.com/nats-io/nuid | Apache License 2.0 |
|
||||||
|
| golang.org/x/crypto | BSD 3-Clause "New" or "Revised" License |
|
||||||
|
| golang.org/x/sys | BSD 3-Clause "New" or "Revised" License |
|
||||||
|
| golang.org/x/time | BSD 3-Clause "New" or "Revised" License |
|
||||||
3
GOVERNANCE.md
Normal file
3
GOVERNANCE.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# NATS Server Governance
|
||||||
|
|
||||||
|
NATS Server is part of the NATS project and is subject to the [NATS Governance](https://github.com/nats-io/nats-general/blob/main/GOVERNANCE.md).
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
10
MAINTAINERS.md
Normal file
10
MAINTAINERS.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Maintainers
|
||||||
|
|
||||||
|
Maintainership is on a per project basis. Reference [NATS Governance Model](https://github.com/nats-io/nats-general/blob/main/GOVERNANCE.md).
|
||||||
|
|
||||||
|
### Maintainers
|
||||||
|
- Derek Collison <derek@nats.io> [@derekcollison](https://github.com/derekcollison)
|
||||||
|
- Ivan Kozlovic <ivan@nats.io> [@kozlovic](https://github.com/kozlovic)
|
||||||
|
- Waldemar Quevedo <wally@nats.io> [@wallyqs](https://github.com/wallyqs)
|
||||||
|
- Oleg Shaldybin <olegsh@google.com> [@olegshaldybin](https://github.com/olegshaldybin)
|
||||||
|
- R.I. Pienaar <rip@devco.net> [@ripienaar](https://github.com/ripienaar)
|
||||||
76
README.md
Normal file
76
README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="logos/nats-horizontal-color.png" width="300" alt="NATS Logo">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[NATS](https://nats.io) is a simple, secure and performant communications system for digital systems, services and devices. NATS is part of the Cloud Native Computing Foundation ([CNCF](https://cncf.io)). NATS has over [40 client language implementations](https://nats.io/download/), and its server can run on-premise, in the cloud, at the edge, and even on a Raspberry Pi. NATS can secure and simplify design and operation of modern distributed systems.
|
||||||
|
|
||||||
|
[![License][License-Image]][License-Url] [![Build][Build-Status-Image]][Build-Status-Url] [![Release][Release-Image]][Release-Url] [![Slack][Slack-Image]][Slack-Url] [![Coverage][Coverage-Image]][Coverage-Url] [![Docker Downloads][Docker-Image]][Docker-Url] [![GitHub Downloads][GitHub-Image]][Somsubhra-URL] [![CII Best Practices][CIIBestPractices-Image]][CIIBestPractices-Url] [![Artifact Hub][ArtifactHub-Image]][ArtifactHub-Url]
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Official Website](https://nats.io)
|
||||||
|
- [Official Documentation](https://docs.nats.io)
|
||||||
|
- [FAQ](https://docs.nats.io/reference/faq)
|
||||||
|
- Watch [a video overview](https://rethink.synadia.com/episodes/1/) of NATS.
|
||||||
|
- Watch [this video from SCALE 13x](https://www.youtube.com/watch?v=sm63oAVPqAM) to learn more about its origin story and design philosophy.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
- [Twitter](https://twitter.com/nats_io): Follow us on Twitter!
|
||||||
|
- [Google Groups](https://groups.google.com/forum/#!forum/natsio): Where you can ask questions
|
||||||
|
- [Slack](https://natsio.slack.com): Click [here](https://slack.nats.io) to join. You can ask questions to our maintainers and to the rich and active community.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you are interested in contributing to NATS, read about our...
|
||||||
|
|
||||||
|
- [Contributing guide](./CONTRIBUTING.md)
|
||||||
|
- [Report issues or propose Pull Requests](https://github.com/nats-io)
|
||||||
|
|
||||||
|
[License-Url]: https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
[License-Image]: https://img.shields.io/badge/License-Apache2-blue.svg
|
||||||
|
[Docker-Image]: https://img.shields.io/docker/pulls/_/nats.svg
|
||||||
|
[Docker-Url]: https://hub.docker.com/_/nats
|
||||||
|
[Slack-Image]: https://img.shields.io/badge/chat-on%20slack-green
|
||||||
|
[Slack-Url]: https://slack.nats.io
|
||||||
|
[Fossa-Url]: https://app.fossa.io/projects/git%2Bgithub.com%2Fnats-io%2Fnats-server?ref=badge_shield
|
||||||
|
[Fossa-Image]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnats-io%2Fnats-server.svg?type=shield
|
||||||
|
[Build-Status-Url]: https://github.com/nats-io/nats-server/actions/workflows/tests.yaml
|
||||||
|
[Build-Status-Image]: https://github.com/nats-io/nats-server/actions/workflows/tests.yaml/badge.svg?branch=main
|
||||||
|
[Release-Url]: https://github.com/nats-io/nats-server/releases/latest
|
||||||
|
[Release-Image]: https://img.shields.io/github/v/release/nats-io/nats-server
|
||||||
|
[Coverage-Url]: https://coveralls.io/r/nats-io/nats-server?branch=main
|
||||||
|
[Coverage-image]: https://coveralls.io/repos/github/nats-io/nats-server/badge.svg?branch=main
|
||||||
|
[ReportCard-Url]: https://goreportcard.com/report/nats-io/nats-server
|
||||||
|
[ReportCard-Image]: https://goreportcard.com/badge/github.com/nats-io/nats-server
|
||||||
|
[CIIBestPractices-Url]: https://bestpractices.coreinfrastructure.org/projects/1895
|
||||||
|
[CIIBestPractices-Image]: https://bestpractices.coreinfrastructure.org/projects/1895/badge
|
||||||
|
[ArtifactHub-Url]: https://artifacthub.io/packages/helm/nats/nats
|
||||||
|
[ArtifactHub-Image]: https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/nats
|
||||||
|
[GitHub-Release]: https://github.com/nats-io/nats-server/releases/
|
||||||
|
[GitHub-Image]: https://img.shields.io/github/downloads/nats-io/nats-server/total.svg?logo=github
|
||||||
|
[Somsubhra-url]: https://somsubhra.github.io/github-release-stats/?username=nats-io&repository=nats-server
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The NATS product roadmap can be found [here](https://nats.io/about/#roadmap).
|
||||||
|
|
||||||
|
## Adopters
|
||||||
|
|
||||||
|
Who uses NATS? See our [list of users](https://nats.io/#who-uses-nats) on [https://nats.io](https://nats.io).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Security Audit
|
||||||
|
|
||||||
|
A third party security audit was performed by Trail of Bits following engagement by the Open Source Technology Improvement Fund (OSTIF). You can see the [full report from April 2025 here](https://github.com/trailofbits/publications/blob/master/reviews/2025-04-ostif-nats-securityreview.pdf).
|
||||||
|
|
||||||
|
### Reporting Security Vulnerabilities
|
||||||
|
|
||||||
|
If you've found a vulnerability or a potential vulnerability in the NATS server, please let us know at
|
||||||
|
[nats-security](mailto:security@nats.io).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Unless otherwise noted, the NATS source files are distributed
|
||||||
|
under the Apache Version 2.0 license found in the LICENSE file.
|
||||||
9
RELEASES.md
Normal file
9
RELEASES.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Release Workflow
|
||||||
|
|
||||||
|
- As of NATS 2.11, the server follows a 6-month release cycle. 2.11 was released in March 2025.
|
||||||
|
- Version tagging follows semantic versioning with security fixes clearly marked.
|
||||||
|
- The `main` branch is for active development of features and bug fixes. Release branches are created for preparing patch releases. Releases are tagged with semantic versions
|
||||||
|
- The current and previous minor series are supported in terms of bug fixes and security patches.
|
||||||
|
- Binaries are released via [GitHub Releases](https://github.com/nats-io/nats-server/releases) and Docker images are available as an [official image](https://hub.docker.com/_/nats).
|
||||||
|
- Preview releases for the next minor version, e.g. 2.14.0, become available once feature development is complete.
|
||||||
|
- Tagged releases as well as nightly Docker images of `main` (for testing) are available in the [synadia/nats-server](https://hub.docker.com/r/synadia/nats-server/tags) Docker repository.
|
||||||
54
TODO.md
Normal file
54
TODO.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
# General
|
||||||
|
|
||||||
|
- [ ] Auth for queue groups?
|
||||||
|
- [ ] Blacklist or ERR escalation to close connection for auth/permissions
|
||||||
|
- [ ] Protocol updates, MAP, MPUB, etc
|
||||||
|
- [ ] Multiple listen endpoints
|
||||||
|
- [ ] Websocket / HTTP2 strategy
|
||||||
|
- [ ] T series reservations
|
||||||
|
- [ ] _SYS. server events?
|
||||||
|
- [ ] No downtime restart
|
||||||
|
- [ ] Signal based reload of configuration
|
||||||
|
- [ ] brew, apt-get, rpm, chocately (windows)
|
||||||
|
- [ ] IOVec pools and writev for high fanout?
|
||||||
|
- [ ] Modify cluster support for single message across routes between pub/sub and d-queue
|
||||||
|
- [ ] Memory limits/warnings?
|
||||||
|
- [ ] Limit number of subscriptions a client can have, total memory usage etc.
|
||||||
|
- [ ] Multi-tenant accounts with isolation of subject space
|
||||||
|
- [ ] Pedantic state
|
||||||
|
- [X] _SYS.> reserved for server events?
|
||||||
|
- [X] Listen configure key vs addr and port
|
||||||
|
- [X] Add ENV and variable support to dconf? ucl?
|
||||||
|
- [X] Buffer pools/sync pools?
|
||||||
|
- [X] Multiple Authorization / Access
|
||||||
|
- [X] Write dynamic socket buffer sizes
|
||||||
|
- [X] Read dynamic socket buffer sizes
|
||||||
|
- [X] Info updates contain other implicit route servers
|
||||||
|
- [X] Sublist better at high concurrency, cache uses writelock always currently
|
||||||
|
- [X] Switch to 1.4/1.5 and use maps vs hashmaps in sublist
|
||||||
|
- [X] NewSource on Rand to lower lock contention on QueueSubs, or redesign!
|
||||||
|
- [X] Default sort by cid on connz
|
||||||
|
- [X] Track last activity time per connection?
|
||||||
|
- [X] Add total connections to varz so we won't miss spikes, etc.
|
||||||
|
- [X] Add starttime and uptime to connz list.
|
||||||
|
- [X] Gossip Protocol for discovery for clustering
|
||||||
|
- [X] Add in HTTP requests to varz?
|
||||||
|
- [X] Add favico and help link for monitoring?
|
||||||
|
- [X] Better user/pass support using bcrypt etc.
|
||||||
|
- [X] SSL/TLS support
|
||||||
|
- [X] Add support for / to point to varz, connz, etc..
|
||||||
|
- [X] Support sort options for /connz via nats-top
|
||||||
|
- [X] Dropped message statistics (slow consumers)
|
||||||
|
- [X] Add current time to each monitoring endpoint
|
||||||
|
- [X] varz uptime do days and only integer secs
|
||||||
|
- [X] Place version in varz (same info sent to clients)
|
||||||
|
- [X] Place server ID/UUID in varz
|
||||||
|
- [X] nats-top equivalent, utils
|
||||||
|
- [X] Connz report routes (/routez)
|
||||||
|
- [X] Docker
|
||||||
|
- [X] Remove reliance on `ps`
|
||||||
|
- [X] Syslog support
|
||||||
|
- [X] Client support for language and version
|
||||||
|
- [X] Fix benchmarks on linux
|
||||||
|
- [X] Daemon mode? Won't fix
|
||||||
24
conf/fuzz.go
Normal file
24
conf/fuzz.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2020-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build gofuzz
|
||||||
|
|
||||||
|
package conf
|
||||||
|
|
||||||
|
func Fuzz(data []byte) int {
|
||||||
|
_, err := Parse(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
1319
conf/lex.go
Normal file
1319
conf/lex.go
Normal file
File diff suppressed because it is too large
Load Diff
1645
conf/lex_test.go
Normal file
1645
conf/lex_test.go
Normal file
File diff suppressed because it is too large
Load Diff
514
conf/parse.go
Normal file
514
conf/parse.go
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
// Copyright 2013-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package conf supports a configuration file format used by gnatsd. It is
|
||||||
|
// a flexible format that combines the best of traditional
|
||||||
|
// configuration formats and newer styles such as JSON and YAML.
|
||||||
|
package conf
|
||||||
|
|
||||||
|
// The format supported is less restrictive than today's formats.
|
||||||
|
// Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
|
||||||
|
// Also supports key value assignments using '=' or ':' or whiteSpace()
|
||||||
|
// e.g. foo = 2, foo : 2, foo 2
|
||||||
|
// maps can be assigned with no key separator as well
|
||||||
|
// semicolons as value terminators in key/value assignments are optional
|
||||||
|
//
|
||||||
|
// see parse_test.go for more examples.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _EMPTY_ = ""
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
mapping map[string]any
|
||||||
|
lx *lexer
|
||||||
|
|
||||||
|
// The current scoped context, can be array or map
|
||||||
|
ctx any
|
||||||
|
|
||||||
|
// stack of contexts, either map or array/slice stack
|
||||||
|
ctxs []any
|
||||||
|
|
||||||
|
// Keys stack
|
||||||
|
keys []string
|
||||||
|
|
||||||
|
// Keys stack as items
|
||||||
|
ikeys []item
|
||||||
|
|
||||||
|
// The config file path, empty by default.
|
||||||
|
fp string
|
||||||
|
|
||||||
|
// pedantic reports error when configuration is not correct.
|
||||||
|
pedantic bool
|
||||||
|
|
||||||
|
// Tracks environment variable references, to avoid cycles
|
||||||
|
envVarReferences map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse will return a map of keys to any, although concrete types
|
||||||
|
// underly them. The values supported are string, bool, int64, float64, DateTime.
|
||||||
|
// Arrays and nested Maps are also supported.
|
||||||
|
func Parse(data string) (map[string]any, error) {
|
||||||
|
p, err := parse(data, "", false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithChecks is equivalent to Parse but runs in pedantic mode.
|
||||||
|
func ParseWithChecks(data string) (map[string]any, error) {
|
||||||
|
p, err := parse(data, "", true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFile is a helper to open file, etc. and parse the contents.
|
||||||
|
func ParseFile(fp string) (map[string]any, error) {
|
||||||
|
data, err := os.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := parse(string(data), fp, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
|
||||||
|
func ParseFileWithChecks(fp string) (map[string]any, error) {
|
||||||
|
data, err := os.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := parse(string(data), fp, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// configDigest returns a digest for the parsed config.
|
||||||
|
func configDigest(m map[string]any) (string, error) {
|
||||||
|
digest := sha256.New()
|
||||||
|
e := json.NewEncoder(digest)
|
||||||
|
if err := e.Encode(m); err != nil {
|
||||||
|
return _EMPTY_, err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFileWithChecksDigest returns the processed config and a digest
|
||||||
|
// that represents the configuration.
|
||||||
|
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
|
||||||
|
m, err := ParseFileWithChecks(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, _EMPTY_, err
|
||||||
|
}
|
||||||
|
digest, err := configDigest(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, _EMPTY_, err
|
||||||
|
}
|
||||||
|
return m, digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
item item
|
||||||
|
value any
|
||||||
|
usedVariable bool
|
||||||
|
sourceFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) Value() any {
|
||||||
|
return t.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) Line() int {
|
||||||
|
return t.item.line
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) IsUsedVariable() bool {
|
||||||
|
return t.usedVariable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) SourceFile() string {
|
||||||
|
return t.sourceFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) Position() int {
|
||||||
|
return t.item.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParser(data, fp string, pedantic bool) *parser {
|
||||||
|
return &parser{
|
||||||
|
mapping: make(map[string]any),
|
||||||
|
lx: lex(data),
|
||||||
|
ctxs: make([]any, 0, 4),
|
||||||
|
keys: make([]string, 0, 4),
|
||||||
|
ikeys: make([]item, 0, 4),
|
||||||
|
fp: filepath.Dir(fp),
|
||||||
|
pedantic: pedantic,
|
||||||
|
envVarReferences: make(map[string]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(data, fp string, pedantic bool) (*parser, error) {
|
||||||
|
p := newParser(data, fp, pedantic)
|
||||||
|
if err := p.parse(fp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnv(data string, parent *parser) (*parser, error) {
|
||||||
|
p := newParser(data, "", false)
|
||||||
|
p.envVarReferences = parent.envVarReferences
|
||||||
|
if err := p.parse(""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parse(fp string) error {
|
||||||
|
p.pushContext(p.mapping)
|
||||||
|
|
||||||
|
var prevItem item
|
||||||
|
for {
|
||||||
|
it := p.next()
|
||||||
|
if it.typ == itemEOF {
|
||||||
|
// Here we allow the final character to be a bracket '}'
|
||||||
|
// in order to support JSON like configurations.
|
||||||
|
if prevItem.typ == itemKey && prevItem.val != mapEndString {
|
||||||
|
return fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
prevItem = it
|
||||||
|
if err := p.processItem(it, fp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) next() item {
|
||||||
|
return p.lx.nextItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) pushContext(ctx any) {
|
||||||
|
p.ctxs = append(p.ctxs, ctx)
|
||||||
|
p.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) popContext() any {
|
||||||
|
if len(p.ctxs) == 0 {
|
||||||
|
panic("BUG in parser, context stack empty")
|
||||||
|
}
|
||||||
|
li := len(p.ctxs) - 1
|
||||||
|
last := p.ctxs[li]
|
||||||
|
p.ctxs = p.ctxs[0:li]
|
||||||
|
p.ctx = p.ctxs[len(p.ctxs)-1]
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) pushKey(key string) {
|
||||||
|
p.keys = append(p.keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) popKey() string {
|
||||||
|
if len(p.keys) == 0 {
|
||||||
|
panic("BUG in parser, keys stack empty")
|
||||||
|
}
|
||||||
|
li := len(p.keys) - 1
|
||||||
|
last := p.keys[li]
|
||||||
|
p.keys = p.keys[0:li]
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) pushItemKey(key item) {
|
||||||
|
p.ikeys = append(p.ikeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) popItemKey() item {
|
||||||
|
if len(p.ikeys) == 0 {
|
||||||
|
panic("BUG in parser, item keys stack empty")
|
||||||
|
}
|
||||||
|
li := len(p.ikeys) - 1
|
||||||
|
last := p.ikeys[li]
|
||||||
|
p.ikeys = p.ikeys[0:li]
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) processItem(it item, fp string) error {
|
||||||
|
setValue := func(it item, v any) {
|
||||||
|
if p.pedantic {
|
||||||
|
p.setValue(&token{it, v, false, fp})
|
||||||
|
} else {
|
||||||
|
p.setValue(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch it.typ {
|
||||||
|
case itemError:
|
||||||
|
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
|
||||||
|
case itemKey:
|
||||||
|
// Keep track of the keys as items and strings,
|
||||||
|
// we do this in order to be able to still support
|
||||||
|
// includes without many breaking changes.
|
||||||
|
p.pushKey(it.val)
|
||||||
|
|
||||||
|
if p.pedantic {
|
||||||
|
p.pushItemKey(it)
|
||||||
|
}
|
||||||
|
case itemMapStart:
|
||||||
|
newCtx := make(map[string]any)
|
||||||
|
p.pushContext(newCtx)
|
||||||
|
case itemMapEnd:
|
||||||
|
setValue(it, p.popContext())
|
||||||
|
case itemString:
|
||||||
|
// FIXME(dlc) sanitize string?
|
||||||
|
setValue(it, it.val)
|
||||||
|
case itemInteger:
|
||||||
|
lastDigit := 0
|
||||||
|
for _, r := range it.val {
|
||||||
|
if !unicode.IsDigit(r) && r != '-' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastDigit++
|
||||||
|
}
|
||||||
|
numStr := it.val[:lastDigit]
|
||||||
|
num, err := strconv.ParseInt(numStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := err.(*strconv.NumError); ok &&
|
||||||
|
e.Err == strconv.ErrRange {
|
||||||
|
return fmt.Errorf("integer '%s' is out of the range", it.val)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("expected integer, but got '%s'", it.val)
|
||||||
|
}
|
||||||
|
// Process a suffix
|
||||||
|
suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
|
||||||
|
|
||||||
|
switch suffix {
|
||||||
|
case "":
|
||||||
|
setValue(it, num)
|
||||||
|
case "k":
|
||||||
|
setValue(it, num*1000)
|
||||||
|
case "kb", "ki", "kib":
|
||||||
|
setValue(it, num*1024)
|
||||||
|
case "m":
|
||||||
|
setValue(it, num*1000*1000)
|
||||||
|
case "mb", "mi", "mib":
|
||||||
|
setValue(it, num*1024*1024)
|
||||||
|
case "g":
|
||||||
|
setValue(it, num*1000*1000*1000)
|
||||||
|
case "gb", "gi", "gib":
|
||||||
|
setValue(it, num*1024*1024*1024)
|
||||||
|
case "t":
|
||||||
|
setValue(it, num*1000*1000*1000*1000)
|
||||||
|
case "tb", "ti", "tib":
|
||||||
|
setValue(it, num*1024*1024*1024*1024)
|
||||||
|
case "p":
|
||||||
|
setValue(it, num*1000*1000*1000*1000*1000)
|
||||||
|
case "pb", "pi", "pib":
|
||||||
|
setValue(it, num*1024*1024*1024*1024*1024)
|
||||||
|
case "e":
|
||||||
|
setValue(it, num*1000*1000*1000*1000*1000*1000)
|
||||||
|
case "eb", "ei", "eib":
|
||||||
|
setValue(it, num*1024*1024*1024*1024*1024*1024)
|
||||||
|
}
|
||||||
|
case itemFloat:
|
||||||
|
num, err := strconv.ParseFloat(it.val, 64)
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := err.(*strconv.NumError); ok &&
|
||||||
|
e.Err == strconv.ErrRange {
|
||||||
|
return fmt.Errorf("float '%s' is out of the range", it.val)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("expected float, but got '%s'", it.val)
|
||||||
|
}
|
||||||
|
setValue(it, num)
|
||||||
|
case itemBool:
|
||||||
|
switch strings.ToLower(it.val) {
|
||||||
|
case "true", "yes", "on":
|
||||||
|
setValue(it, true)
|
||||||
|
case "false", "no", "off":
|
||||||
|
setValue(it, false)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected boolean value, but got '%s'", it.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
case itemDatetime:
|
||||||
|
dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"expected Zulu formatted DateTime, but got '%s'", it.val)
|
||||||
|
}
|
||||||
|
setValue(it, dt)
|
||||||
|
case itemArrayStart:
|
||||||
|
var array = make([]any, 0)
|
||||||
|
p.pushContext(array)
|
||||||
|
case itemArrayEnd:
|
||||||
|
array := p.ctx
|
||||||
|
p.popContext()
|
||||||
|
setValue(it, array)
|
||||||
|
case itemVariable:
|
||||||
|
value, found, err := p.lookupVariable(it.val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
|
||||||
|
it.val, it.line, err)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("variable reference for '%s' on line %d can not be found",
|
||||||
|
it.val, it.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pedantic {
|
||||||
|
switch tk := value.(type) {
|
||||||
|
case *token:
|
||||||
|
// Mark the looked up variable as used, and make
|
||||||
|
// the variable reference become handled as a token.
|
||||||
|
tk.usedVariable = true
|
||||||
|
p.setValue(&token{it, tk.Value(), false, fp})
|
||||||
|
default:
|
||||||
|
// Special case to add position context to bcrypt references.
|
||||||
|
p.setValue(&token{it, value, false, fp})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.setValue(value)
|
||||||
|
}
|
||||||
|
case itemInclude:
|
||||||
|
var (
|
||||||
|
m map[string]any
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if p.pedantic {
|
||||||
|
m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
|
||||||
|
} else {
|
||||||
|
m, err = ParseFile(filepath.Join(p.fp, it.val))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing include file '%s', %v", it.val, err)
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
p.pushKey(k)
|
||||||
|
|
||||||
|
if p.pedantic {
|
||||||
|
switch tk := v.(type) {
|
||||||
|
case *token:
|
||||||
|
p.pushItemKey(tk.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.setValue(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to map an environment value into a temporary map to pass to secondary Parse call.
|
||||||
|
const pkey = "pk"
|
||||||
|
|
||||||
|
// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
|
||||||
|
const bcryptPrefix = "2a$"
|
||||||
|
|
||||||
|
// lookupVariable will lookup a variable reference. It will use block scoping on keys
|
||||||
|
// it has seen before, with the top level scoping being the environment variables. We
|
||||||
|
// ignore array contexts and only process the map contexts..
|
||||||
|
//
|
||||||
|
// Returns true for ok if it finds something, similar to map.
|
||||||
|
func (p *parser) lookupVariable(varReference string) (any, bool, error) {
|
||||||
|
// Do special check to see if it is a raw bcrypt string.
|
||||||
|
if strings.HasPrefix(varReference, bcryptPrefix) {
|
||||||
|
return "$" + varReference, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through contexts currently on the stack.
|
||||||
|
for i := len(p.ctxs) - 1; i >= 0; i-- {
|
||||||
|
ctx := p.ctxs[i]
|
||||||
|
// Process if it is a map context
|
||||||
|
if m, ok := ctx.(map[string]any); ok {
|
||||||
|
if v, ok := m[varReference]; ok {
|
||||||
|
return v, ok, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are here, we have exhausted our context maps and still not found anything.
|
||||||
|
// Detect reference cycles
|
||||||
|
if p.envVarReferences[varReference] {
|
||||||
|
return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
|
||||||
|
}
|
||||||
|
p.envVarReferences[varReference] = true
|
||||||
|
defer delete(p.envVarReferences, varReference)
|
||||||
|
|
||||||
|
// Parse from the environment
|
||||||
|
if vStr, ok := os.LookupEnv(varReference); ok {
|
||||||
|
// Everything we get here will be a string value, so we need to process as a parser would.
|
||||||
|
if subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
|
||||||
|
v, ok := subp.mapping[pkey]
|
||||||
|
return v, ok, nil
|
||||||
|
} else {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) setValue(val any) {
|
||||||
|
// Test to see if we are on an array or a map
|
||||||
|
|
||||||
|
// Array processing
|
||||||
|
if ctx, ok := p.ctx.([]any); ok {
|
||||||
|
p.ctx = append(ctx, val)
|
||||||
|
p.ctxs[len(p.ctxs)-1] = p.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map processing
|
||||||
|
if ctx, ok := p.ctx.(map[string]any); ok {
|
||||||
|
key := p.popKey()
|
||||||
|
|
||||||
|
if p.pedantic {
|
||||||
|
// Change the position to the beginning of the key
|
||||||
|
// since more useful when reporting errors.
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *token:
|
||||||
|
it := p.popItemKey()
|
||||||
|
v.item.pos = it.pos
|
||||||
|
v.item.line = it.line
|
||||||
|
ctx[key] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// FIXME(dlc), make sure to error if redefining same key?
|
||||||
|
ctx[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1034
conf/parse_test.go
Normal file
1034
conf/parse_test.go
Normal file
File diff suppressed because it is too large
Load Diff
3
doc/README.md
Normal file
3
doc/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
The NATS ADR documents have moved to their [own repository](https://github.com/nats-io/nats-architecture-and-design/)
|
||||||
49
docker/Dockerfile.nightly
Normal file
49
docker/Dockerfile.nightly
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION="nightly"
|
||||||
|
ARG GIT_COMMIT
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
ENV GOOS=$TARGETOS \
|
||||||
|
GOARCH=$TARGETARCH \
|
||||||
|
GO111MODULE=on \
|
||||||
|
CGO_ENABLED=0
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
RUN mkdir -p /src/out/$GOOS/$GOARCH
|
||||||
|
|
||||||
|
COPY ./nats-server/ /src/nats-server/
|
||||||
|
COPY ./nsc/ /src/nsc/
|
||||||
|
COPY ./natscli/ /src/natscli/
|
||||||
|
|
||||||
|
RUN cd /src/nats-server && go build -trimpath \
|
||||||
|
-ldflags "-w -X server.serverVersion=${VERSION},server.gitCommit=${GIT_COMMIT}" \
|
||||||
|
-o /src/out/$TARGETOS/$TARGETARCH/nats-server .
|
||||||
|
|
||||||
|
RUN cd /src/natscli && go build -trimpath \
|
||||||
|
-ldflags "-w -X main.version=${VERSION}" \
|
||||||
|
-o /src/out/$TARGETOS/$TARGETARCH/nats ./nats
|
||||||
|
|
||||||
|
RUN cd /src/nsc && go build -trimpath \
|
||||||
|
-o /src/out/$TARGETOS/$TARGETARCH/nsc .
|
||||||
|
|
||||||
|
FROM --platform=$TARGETPLATFORM alpine:latest
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nats-server /bin/nats-server
|
||||||
|
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nats /bin/nats
|
||||||
|
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nsc /bin/nsc
|
||||||
|
|
||||||
|
EXPOSE 4222 8222 6222 5222
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/nats-server"]
|
||||||
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module github.com/nats-io/nats-server/v2
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.8
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op
|
||||||
|
github.com/google/go-tpm v0.9.8
|
||||||
|
github.com/klauspost/compress v1.20.0
|
||||||
|
github.com/minio/highwayhash v1.0.4
|
||||||
|
github.com/nats-io/jwt/v2 v2.8.2
|
||||||
|
github.com/nats-io/nats.go v1.51.0
|
||||||
|
github.com/nats-io/nkeys v0.4.16
|
||||||
|
github.com/nats-io/nuid v1.0.1
|
||||||
|
golang.org/x/crypto v0.57.0
|
||||||
|
golang.org/x/sys v0.48.0
|
||||||
|
golang.org/x/time v0.16.0
|
||||||
|
)
|
||||||
23
go.sum
Normal file
23
go.sum
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||||
|
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
|
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||||
|
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
|
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||||
|
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
|
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||||
|
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||||
|
github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI=
|
||||||
|
github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
||||||
|
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||||
|
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||||
|
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||||
27
internal/antithesis/noop.go
Normal file
27
internal/antithesis/noop.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright 2022-2024 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This file is used iff the `enable_antithesis_sdk` build tag is not present
|
||||||
|
//go:build !enable_antithesis_sdk
|
||||||
|
|
||||||
|
package antithesis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssertUnreachable this implementation is a NOOP
|
||||||
|
func AssertUnreachable(_ testing.TB, _ string, _ map[string]any) {}
|
||||||
|
|
||||||
|
// Assert this implementation is a NOOP
|
||||||
|
func Assert(_ testing.TB, _ bool, _ string, _ map[string]any) {}
|
||||||
111
internal/antithesis/test_assert.go
Normal file
111
internal/antithesis/test_assert.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2022-2024 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This file is used iff the `enable_antithesis_sdk` build tag is present
|
||||||
|
//go:build enable_antithesis_sdk
|
||||||
|
|
||||||
|
package antithesis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/antithesishq/antithesis-sdk-go/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file provides assertions utility functions suitable for use in tests.
|
||||||
|
// It is a thin wrapper around the Antithesis SDK.
|
||||||
|
//
|
||||||
|
// Notice that unlike other assertions libraries, a violation does not halt execution or fail the test.
|
||||||
|
// The effects of violating an assertion, are:
|
||||||
|
// 1. Print the violation message, prefixed by the test name in which it happened
|
||||||
|
// 2. Print the stack for the calling goroutine so each failed test has a clear trace of the failure
|
||||||
|
// 3. Invoke the underlying Antithesis assertion
|
||||||
|
//
|
||||||
|
// N.B. Enabling this module outside for tests running outside of Antithesis will enable 1 and 2 above, but not 3.
|
||||||
|
// therefore it can be useful to output additional test failure details when running tests locally or in CI.
|
||||||
|
|
||||||
|
// AssertUnreachable is used to flag code branches that should not get invoked ever.
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// pubAck, err := js.Publish(...)
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// antithesis.AssertUnreachable(t, "Publish failed", map[string]any{"error": err.Error()})
|
||||||
|
// t.Fatalf("Publish failed with error: %s", err)
|
||||||
|
// }
|
||||||
|
func AssertUnreachable(t testing.TB, message string, details map[string]any) {
|
||||||
|
// Always print a message
|
||||||
|
fmt.Printf("{*} [%s] Assert Unreachable violation: %s\n", t.Name(), message)
|
||||||
|
if details != nil && len(details) > 0 {
|
||||||
|
fmt.Printf("{*} Details:\n")
|
||||||
|
jsonDetails, err := json.MarshalIndent(details, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(jsonDetails))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always print the stack trace
|
||||||
|
fmt.Printf("{*} Stack trace:\n")
|
||||||
|
debug.PrintStack()
|
||||||
|
|
||||||
|
// N.B. as of today, message (as-is) is the unique identifier of the event
|
||||||
|
// Therefore this will de-duplicate the same assertion failing in 2 different tests
|
||||||
|
// But not the same assertion failing at 2 different lines of the same test
|
||||||
|
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
|
||||||
|
|
||||||
|
// Fire assertion violation event (if Antithesis is enabled)
|
||||||
|
assert.Unreachable(messageWithTestName, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert is used to check that some given condition is always true,
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// antithesis.Assert(t, sequence > lastSequence, "Non-monotonic stream sequence number", map[string]any{
|
||||||
|
// "stream": streamName,
|
||||||
|
// "connection_id": nc.Id(),
|
||||||
|
// "sequence": sequence,
|
||||||
|
// "lastSequence": lastSequence,
|
||||||
|
// })
|
||||||
|
func Assert(t testing.TB, condition bool, message string, details map[string]any) {
|
||||||
|
// Condition is true, nothing to do
|
||||||
|
if condition {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always print a message
|
||||||
|
fmt.Printf("{*} [%s] Assert violation: %s\n", t.Name(), message)
|
||||||
|
if details != nil && len(details) > 0 {
|
||||||
|
fmt.Printf("{*} Details:\n")
|
||||||
|
jsonDetails, err := json.MarshalIndent(details, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(jsonDetails))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always print the stack trace
|
||||||
|
fmt.Printf("{*} Stack trace:\n")
|
||||||
|
debug.PrintStack()
|
||||||
|
|
||||||
|
// N.B. as of today, message (as-is) is the unique identifier of the event
|
||||||
|
// Therefore this will de-duplicate the same assertion failing in 2 different tests
|
||||||
|
// But not the same assertion failing at 2 different lines of the same test
|
||||||
|
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
|
||||||
|
|
||||||
|
// Fire assertion violation event (if Antithesis is enabled)
|
||||||
|
assert.AlwaysOrUnreachable(false, messageWithTestName, details)
|
||||||
|
}
|
||||||
305
internal/ldap/dn.go
Normal file
305
internal/ldap/dn.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
|
||||||
|
// Portions copyright (c) 2015-2016 go-ldap Authors
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
enchex "encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var attributeTypeNames = map[string]string{
|
||||||
|
"2.5.4.3": "CN",
|
||||||
|
"2.5.4.5": "SERIALNUMBER",
|
||||||
|
"2.5.4.6": "C",
|
||||||
|
"2.5.4.7": "L",
|
||||||
|
"2.5.4.8": "ST",
|
||||||
|
"2.5.4.9": "STREET",
|
||||||
|
"2.5.4.10": "O",
|
||||||
|
"2.5.4.11": "OU",
|
||||||
|
"2.5.4.17": "POSTALCODE",
|
||||||
|
// FIXME: Add others.
|
||||||
|
"0.9.2342.19200300.100.1.25": "DC",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
|
||||||
|
type AttributeTypeAndValue struct {
|
||||||
|
// Type is the attribute type
|
||||||
|
Type string
|
||||||
|
// Value is the attribute value
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
|
||||||
|
type RelativeDN struct {
|
||||||
|
Attributes []*AttributeTypeAndValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
|
||||||
|
type DN struct {
|
||||||
|
RDNs []*RelativeDN
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromCertSubject takes a pkix.Name from a cert and returns a DN
|
||||||
|
// that uses the same set. Does not support multi value RDNs.
|
||||||
|
func FromCertSubject(subject pkix.Name) (*DN, error) {
|
||||||
|
dn := &DN{
|
||||||
|
RDNs: make([]*RelativeDN, 0),
|
||||||
|
}
|
||||||
|
for i := len(subject.Names) - 1; i >= 0; i-- {
|
||||||
|
name := subject.Names[i]
|
||||||
|
oidString := name.Type.String()
|
||||||
|
typeName, ok := attributeTypeNames[oidString]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type name: %+v", name)
|
||||||
|
}
|
||||||
|
v, ok := name.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type value: %+v", v)
|
||||||
|
}
|
||||||
|
rdn := &RelativeDN{
|
||||||
|
Attributes: []*AttributeTypeAndValue{
|
||||||
|
{
|
||||||
|
Type: typeName,
|
||||||
|
Value: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dn.RDNs = append(dn.RDNs, rdn)
|
||||||
|
}
|
||||||
|
return dn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromRawCertSubject takes a raw subject from a certificate
|
||||||
|
// and uses asn1.Unmarshal to get the individual RDNs in the
|
||||||
|
// original order, including multi-value RDNs.
|
||||||
|
func FromRawCertSubject(rawSubject []byte) (*DN, error) {
|
||||||
|
dn := &DN{
|
||||||
|
RDNs: make([]*RelativeDN, 0),
|
||||||
|
}
|
||||||
|
var rdns pkix.RDNSequence
|
||||||
|
_, err := asn1.Unmarshal(rawSubject, &rdns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(rdns) - 1; i >= 0; i-- {
|
||||||
|
rdn := rdns[i]
|
||||||
|
if len(rdn) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &RelativeDN{}
|
||||||
|
attrs := make([]*AttributeTypeAndValue, 0)
|
||||||
|
for j := len(rdn) - 1; j >= 0; j-- {
|
||||||
|
atv := rdn[j]
|
||||||
|
|
||||||
|
typeName := ""
|
||||||
|
name := atv.Type.String()
|
||||||
|
typeName, ok := attributeTypeNames[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type name: %+v", name)
|
||||||
|
}
|
||||||
|
value, ok := atv.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type value: %+v", atv.Value)
|
||||||
|
}
|
||||||
|
attr := &AttributeTypeAndValue{
|
||||||
|
Type: typeName,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
attrs = append(attrs, attr)
|
||||||
|
}
|
||||||
|
r.Attributes = attrs
|
||||||
|
dn.RDNs = append(dn.RDNs, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDN returns a distinguishedName or an error.
|
||||||
|
// The function respects https://tools.ietf.org/html/rfc4514
|
||||||
|
func ParseDN(str string) (*DN, error) {
|
||||||
|
dn := new(DN)
|
||||||
|
dn.RDNs = make([]*RelativeDN, 0)
|
||||||
|
rdn := new(RelativeDN)
|
||||||
|
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
attribute := new(AttributeTypeAndValue)
|
||||||
|
escaping := false
|
||||||
|
|
||||||
|
unescapedTrailingSpaces := 0
|
||||||
|
stringFromBuffer := func() string {
|
||||||
|
s := buffer.String()
|
||||||
|
s = s[0 : len(s)-unescapedTrailingSpaces]
|
||||||
|
buffer.Reset()
|
||||||
|
unescapedTrailingSpaces = 0
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(str); i++ {
|
||||||
|
char := str[i]
|
||||||
|
switch {
|
||||||
|
case escaping:
|
||||||
|
unescapedTrailingSpaces = 0
|
||||||
|
escaping = false
|
||||||
|
switch char {
|
||||||
|
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
|
||||||
|
buffer.WriteByte(char)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Not a special character, assume hex encoded octet
|
||||||
|
if len(str) == i+1 {
|
||||||
|
return nil, errors.New("got corrupted escaped character")
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := []byte{0}
|
||||||
|
n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode escaped character: %s", err)
|
||||||
|
} else if n != 1 {
|
||||||
|
return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
|
||||||
|
}
|
||||||
|
buffer.WriteByte(dst[0])
|
||||||
|
i++
|
||||||
|
case char == '\\':
|
||||||
|
unescapedTrailingSpaces = 0
|
||||||
|
escaping = true
|
||||||
|
case char == '=':
|
||||||
|
attribute.Type = stringFromBuffer()
|
||||||
|
// Special case: If the first character in the value is # the following data
|
||||||
|
// is BER encoded. Throw an error since not supported right now.
|
||||||
|
if len(str) > i+1 && str[i+1] == '#' {
|
||||||
|
return nil, errors.New("unsupported BER encoding")
|
||||||
|
}
|
||||||
|
case char == ',' || char == '+':
|
||||||
|
// We're done with this RDN or value, push it
|
||||||
|
if len(attribute.Type) == 0 {
|
||||||
|
return nil, errors.New("incomplete type, value pair")
|
||||||
|
}
|
||||||
|
attribute.Value = stringFromBuffer()
|
||||||
|
rdn.Attributes = append(rdn.Attributes, attribute)
|
||||||
|
attribute = new(AttributeTypeAndValue)
|
||||||
|
if char == ',' {
|
||||||
|
dn.RDNs = append(dn.RDNs, rdn)
|
||||||
|
rdn = new(RelativeDN)
|
||||||
|
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
|
||||||
|
}
|
||||||
|
case char == ' ' && buffer.Len() == 0:
|
||||||
|
// ignore unescaped leading spaces
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
if char == ' ' {
|
||||||
|
// Track unescaped spaces in case they are trailing and we need to remove them
|
||||||
|
unescapedTrailingSpaces++
|
||||||
|
} else {
|
||||||
|
// Reset if we see a non-space char
|
||||||
|
unescapedTrailingSpaces = 0
|
||||||
|
}
|
||||||
|
buffer.WriteByte(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
if len(attribute.Type) == 0 {
|
||||||
|
return nil, errors.New("DN ended with incomplete type, value pair")
|
||||||
|
}
|
||||||
|
attribute.Value = stringFromBuffer()
|
||||||
|
rdn.Attributes = append(rdn.Attributes, attribute)
|
||||||
|
dn.RDNs = append(dn.RDNs, rdn)
|
||||||
|
}
|
||||||
|
return dn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
|
||||||
|
// Returns true if they have the same number of relative distinguished names
|
||||||
|
// and corresponding relative distinguished names (by position) are the same.
|
||||||
|
func (d *DN) Equal(other *DN) bool {
|
||||||
|
if len(d.RDNs) != len(other.RDNs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range d.RDNs {
|
||||||
|
if !d.RDNs[i].Equal(other.RDNs[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDNsMatch returns true if the individual RDNs of the DNs
|
||||||
|
// are the same regardless of ordering.
|
||||||
|
func (d *DN) RDNsMatch(other *DN) bool {
|
||||||
|
if len(d.RDNs) != len(other.RDNs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
matched := make([]bool, len(other.RDNs))
|
||||||
|
for _, irdn := range d.RDNs {
|
||||||
|
found := false
|
||||||
|
for j, ordn := range other.RDNs {
|
||||||
|
if !matched[j] && irdn.Equal(ordn) {
|
||||||
|
matched[j] = true
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
|
||||||
|
// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
|
||||||
|
// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
|
||||||
|
// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
|
||||||
|
func (d *DN) AncestorOf(other *DN) bool {
|
||||||
|
if len(d.RDNs) >= len(other.RDNs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
|
||||||
|
otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
|
||||||
|
for i := range d.RDNs {
|
||||||
|
if !d.RDNs[i].Equal(otherRDNs[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
|
||||||
|
// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
|
||||||
|
// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
|
||||||
|
// The order of attributes is not significant.
|
||||||
|
// Case of attribute types is not significant.
|
||||||
|
func (r *RelativeDN) Equal(other *RelativeDN) bool {
|
||||||
|
if len(r.Attributes) != len(other.Attributes) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
|
||||||
|
for _, attr := range attrs {
|
||||||
|
found := false
|
||||||
|
for _, myattr := range r.Attributes {
|
||||||
|
if myattr.Equal(attr) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
|
||||||
|
// Case of the attribute type is not significant
|
||||||
|
func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
|
||||||
|
return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
|
||||||
|
}
|
||||||
225
internal/ldap/dn_test.go
Normal file
225
internal/ldap/dn_test.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
|
||||||
|
// Portions copyright (c) 2015-2016 go-ldap Authors
|
||||||
|
// Static-Check Fixes Copyright 2024 The NATS Authors
|
||||||
|
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSuccessfulDNParsing(t *testing.T) {
|
||||||
|
testcases := map[string]DN{
|
||||||
|
"": {[]*RelativeDN{}},
|
||||||
|
"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"dc", "dummy"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"dc", "com"}}}}},
|
||||||
|
"UID=jsmith,DC=example,DC=net": {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{"UID", "jsmith"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"DC", "example"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||||
|
"OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{
|
||||||
|
{"OU", "Sales"},
|
||||||
|
{"CN", "J. Smith"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"DC", "example"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||||
|
//
|
||||||
|
// "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{
|
||||||
|
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
|
||||||
|
// "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{
|
||||||
|
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}},
|
||||||
|
// {[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||||
|
"CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
|
||||||
|
" CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
|
||||||
|
` A = 1 , B = 2 `: {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{"A", "1"}}},
|
||||||
|
{[]*AttributeTypeAndValue{{"B", "2"}}}}},
|
||||||
|
` A = 1 + B = 2 `: {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{
|
||||||
|
{"A", "1"},
|
||||||
|
{"B", "2"}}}}},
|
||||||
|
` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{{" A ", " 1 "}}},
|
||||||
|
{[]*AttributeTypeAndValue{{" B ", " 2 "}}}}},
|
||||||
|
` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
|
||||||
|
{[]*AttributeTypeAndValue{
|
||||||
|
{" A ", " 1 "},
|
||||||
|
{" B ", " 2 "}}}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for test, answer := range testcases {
|
||||||
|
dn, err := ParseDN(test)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(dn, &answer) {
|
||||||
|
t.Errorf("Parsed DN %s is not equal to the expected structure", test)
|
||||||
|
t.Logf("Expected:")
|
||||||
|
for _, rdn := range answer.RDNs {
|
||||||
|
for _, attribs := range rdn.Attributes {
|
||||||
|
t.Logf("#%v\n", attribs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("Actual:")
|
||||||
|
for _, rdn := range dn.RDNs {
|
||||||
|
for _, attribs := range rdn.Attributes {
|
||||||
|
t.Logf("#%v\n", attribs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorDNParsing(t *testing.T) {
|
||||||
|
testcases := map[string]string{
|
||||||
|
"*": "DN ended with incomplete type, value pair",
|
||||||
|
"cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
|
||||||
|
"cn=Jim\\0": "got corrupted escaped character",
|
||||||
|
"DC=example,=net": "DN ended with incomplete type, value pair",
|
||||||
|
// "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string",
|
||||||
|
"test,DC=example,DC=com": "incomplete type, value pair",
|
||||||
|
"=test,DC=example,DC=com": "incomplete type, value pair",
|
||||||
|
}
|
||||||
|
|
||||||
|
for test, answer := range testcases {
|
||||||
|
_, err := ParseDN(test)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected %s to fail parsing but succeeded\n", test)
|
||||||
|
} else if err.Error() != answer {
|
||||||
|
t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNEqual(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
Equal bool
|
||||||
|
}{
|
||||||
|
// Exact match
|
||||||
|
{"", "", true},
|
||||||
|
{"o=A", "o=A", true},
|
||||||
|
{"o=A", "o=B", false},
|
||||||
|
|
||||||
|
{"o=A,o=B", "o=A,o=B", true},
|
||||||
|
{"o=A,o=B", "o=A,o=C", false},
|
||||||
|
|
||||||
|
{"o=A+o=B", "o=A+o=B", true},
|
||||||
|
{"o=A+o=B", "o=A+o=C", false},
|
||||||
|
|
||||||
|
// Case mismatch in type is ignored
|
||||||
|
{"o=A", "O=A", true},
|
||||||
|
{"o=A,o=B", "o=A,O=B", true},
|
||||||
|
{"o=A+o=B", "o=A+O=B", true},
|
||||||
|
|
||||||
|
// Case mismatch in value is significant
|
||||||
|
{"o=a", "O=A", false},
|
||||||
|
{"o=a,o=B", "o=A,O=B", false},
|
||||||
|
{"o=a+o=B", "o=A+O=B", false},
|
||||||
|
|
||||||
|
// Multi-valued RDN order mismatch is ignored
|
||||||
|
{"o=A+o=B", "O=B+o=A", true},
|
||||||
|
// Number of RDN attributes is significant
|
||||||
|
{"o=A+o=B", "O=B+o=A+O=B", false},
|
||||||
|
|
||||||
|
// Missing values are significant
|
||||||
|
{"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter
|
||||||
|
{"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter
|
||||||
|
|
||||||
|
// Whitespace tests
|
||||||
|
// Matching
|
||||||
|
{
|
||||||
|
"cn=John Doe, ou=People, dc=sun.com",
|
||||||
|
"cn=John Doe, ou=People, dc=sun.com",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// Difference in leading/trailing chars is ignored
|
||||||
|
{
|
||||||
|
"cn=John Doe, ou=People, dc=sun.com",
|
||||||
|
"cn=John Doe,ou=People,dc=sun.com",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// Difference in values is significant
|
||||||
|
{
|
||||||
|
"cn=John Doe, ou=People, dc=sun.com",
|
||||||
|
"cn=John Doe, ou=People, dc=sun.com",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testcases {
|
||||||
|
a, err := ParseDN(tc.A)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%d: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := ParseDN(tc.B)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%d: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
|
||||||
|
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
|
||||||
|
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNAncestor(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
Ancestor bool
|
||||||
|
}{
|
||||||
|
// Exact match returns false
|
||||||
|
{"", "", false},
|
||||||
|
{"o=A", "o=A", false},
|
||||||
|
{"o=A,o=B", "o=A,o=B", false},
|
||||||
|
{"o=A+o=B", "o=A+o=B", false},
|
||||||
|
|
||||||
|
// Mismatch
|
||||||
|
{"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false},
|
||||||
|
|
||||||
|
// Descendant
|
||||||
|
{"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testcases {
|
||||||
|
a, err := ParseDN(tc.A)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%d: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := ParseDN(tc.B)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%d: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual {
|
||||||
|
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNReverseMatchDuplicates(t *testing.T) {
|
||||||
|
// A has two CN=Alice RDNs and one O=NATS.
|
||||||
|
// B has one CN=Alice, one O=NATS, and one OU=Users.
|
||||||
|
// They have the same length (3).
|
||||||
|
dnA, _ := ParseDN("CN=Alice,CN=Alice,O=NATS")
|
||||||
|
dnB, _ := ParseDN("CN=Alice,O=NATS,OU=Users")
|
||||||
|
if dnA.RDNsMatch(dnB) {
|
||||||
|
t.Fatal("Expected RDNsMatch to return false for different RDN sets, but it returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
276
internal/ocsp/ocsp.go
Normal file
276
internal/ocsp/ocsp.go
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
// Copyright 2019-2024 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultResponseTTL = 4 * time.Second
|
||||||
|
defaultAddress = "127.0.0.1:8888"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOCSPResponderCustomAddress(t *testing.T, issuerCertPEM, issuerKeyPEM string, addr string) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, addr, defaultResponseTTL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPResponder(t *testing.T, issuerCertPEM, issuerKeyPEM string) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPResponderDesignatedCustomAddress(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, addr string) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
return NewOCSPResponderBase(t, issuerCertPEM, respCertPEM, respKeyPEM, true, addr, defaultResponseTTL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPResponderPreferringHTTPMethod(t *testing.T, issuerCertPEM, issuerKeyPEM, method string) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPResponderCustomTimeout(t *testing.T, issuerCertPEM, issuerKeyPEM string, responseTTL time.Duration) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, responseTTL, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPResponderBase(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, embed bool, addr string, responseTTL time.Duration, method string) *http.Server {
|
||||||
|
t.Helper()
|
||||||
|
var mu sync.Mutex
|
||||||
|
status := make(map[string]int)
|
||||||
|
|
||||||
|
issuerCert := parseCertPEM(t, issuerCertPEM)
|
||||||
|
respCert := parseCertPEM(t, respCertPEM)
|
||||||
|
respKey := parseKeyPEM(t, respKeyPEM)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
// The "/statuses/" endpoint is for directly setting a key-value pair in
|
||||||
|
// the CA's status database.
|
||||||
|
mux.HandleFunc("/statuses/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
key := r.URL.Path[len("/statuses/"):]
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
mu.Lock()
|
||||||
|
n, ok := status[key]
|
||||||
|
if !ok {
|
||||||
|
n = ocsp.Unknown
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
fmt.Fprintf(rw, "%s %d", key, n)
|
||||||
|
case "POST":
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := strconv.Atoi(string(data))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
status[key] = n
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
fmt.Fprintf(rw, "%s %d", key, n)
|
||||||
|
default:
|
||||||
|
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// The "/" endpoint is for normal OCSP requests. This actually parses an
|
||||||
|
// OCSP status request and signs a response with a CA. Lightly based off:
|
||||||
|
// https://www.ietf.org/rfc/rfc2560.txt
|
||||||
|
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var reqData []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
if method != "" && r.Method != method {
|
||||||
|
http.Error(rw, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqData, err = base64.StdEncoding.DecodeString(r.URL.Path[1:])
|
||||||
|
case r.Method == "POST":
|
||||||
|
if method != "" && r.Method != method {
|
||||||
|
http.Error(rw, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqData, err = io.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
default:
|
||||||
|
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ocspReq, err := ocsp.ParseRequest(reqData)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
n, ok := status[ocspReq.SerialNumber.String()]
|
||||||
|
if !ok {
|
||||||
|
n = ocsp.Unknown
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
tmpl := ocsp.Response{
|
||||||
|
Status: n,
|
||||||
|
SerialNumber: ocspReq.SerialNumber,
|
||||||
|
ThisUpdate: time.Now(),
|
||||||
|
}
|
||||||
|
if responseTTL != 0 {
|
||||||
|
tmpl.NextUpdate = tmpl.ThisUpdate.Add(responseTTL)
|
||||||
|
}
|
||||||
|
if embed {
|
||||||
|
tmpl.Certificate = respCert
|
||||||
|
}
|
||||||
|
respData, err := ocsp.CreateResponse(issuerCert, respCert, tmpl, respKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||||
|
rw.Header().Set("Content-Length", fmt.Sprint(len(respData)))
|
||||||
|
|
||||||
|
fmt.Fprint(rw, string(respData))
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: time.Second * 5,
|
||||||
|
}
|
||||||
|
go srv.ListenAndServe()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCertPEM(t *testing.T, certPEM string) *x509.Certificate {
|
||||||
|
t.Helper()
|
||||||
|
block := parsePEM(t, certPEM)
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse cert '%s': %s", certPEM, err)
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeyPEM(t *testing.T, keyPEM string) crypto.Signer {
|
||||||
|
t.Helper()
|
||||||
|
block := parsePEM(t, keyPEM)
|
||||||
|
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse ikey %s: %s", keyPEM, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyc := key.(crypto.Signer)
|
||||||
|
return keyc
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePEM(t *testing.T, pemPath string) *pem.Block {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(pemPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(data)
|
||||||
|
if block == nil {
|
||||||
|
t.Fatalf("failed to decode PEM %s", pemPath)
|
||||||
|
}
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
|
||||||
|
if len(s.VerifiedChains) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing TLS verified chains")
|
||||||
|
}
|
||||||
|
chain := s.VerifiedChains[0]
|
||||||
|
|
||||||
|
if got, want := len(chain), 2; got < want {
|
||||||
|
return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
|
||||||
|
}
|
||||||
|
leaf, issuer := chain[0], chain[1]
|
||||||
|
|
||||||
|
resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
|
||||||
|
}
|
||||||
|
if err := resp.CheckSignatureFrom(issuer); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cert := parseCertPEM(t, certPEM)
|
||||||
|
|
||||||
|
hc := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := hc.Post(
|
||||||
|
fmt.Sprintf("%s/statuses/%s", ocspURL, cert.SerialNumber),
|
||||||
|
"",
|
||||||
|
strings.NewReader(fmt.Sprint(status)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read OCSP HTTP response body: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := resp.Status, "200 OK"; got != want {
|
||||||
|
t.Error(strings.TrimSpace(string(data)))
|
||||||
|
t.Fatalf("unexpected OCSP HTTP set status, got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
127
internal/testhelper/logging.go
Normal file
127
internal/testhelper/logging.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2019-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
// These routines need to be accessible in both the server and test
|
||||||
|
// directories, and tests importing a package don't get exported symbols from
|
||||||
|
// _test.go files in the imported package, so we put them here where they can
|
||||||
|
// be used freely.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DummyLogger struct {
|
||||||
|
sync.Mutex
|
||||||
|
Msg string
|
||||||
|
AllMsgs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DummyLogger) CheckContent(t *testing.T, expectedStr string) {
|
||||||
|
t.Helper()
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
if l.Msg != expectedStr {
|
||||||
|
t.Fatalf("Expected log to be: %v, got %v", expectedStr, l.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DummyLogger) aggregate() {
|
||||||
|
if l.AllMsgs != nil {
|
||||||
|
l.AllMsgs = append(l.AllMsgs, l.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DummyLogger) Noticef(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
func (l *DummyLogger) Errorf(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
func (l *DummyLogger) Warnf(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
func (l *DummyLogger) Fatalf(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
func (l *DummyLogger) Debugf(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
func (l *DummyLogger) Tracef(format string, v ...any) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.Msg = fmt.Sprintf(format, v...)
|
||||||
|
l.aggregate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDummyLogger creates a dummy logger and allows to ask for logs to be
|
||||||
|
// retained instead of just keeping the most recent. Use retain to provide an
|
||||||
|
// initial size estimate on messages (not to provide a max capacity).
|
||||||
|
func NewDummyLogger(retain uint) *DummyLogger {
|
||||||
|
l := &DummyLogger{}
|
||||||
|
if retain > 0 {
|
||||||
|
l.AllMsgs = make([]string, 0, retain)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DummyLogger) Drain() {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
if l.AllMsgs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.AllMsgs = make([]string, 0, len(l.AllMsgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DummyLogger) CheckForProhibited(t *testing.T, reason, needle string) {
|
||||||
|
t.Helper()
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
|
||||||
|
if l.AllMsgs == nil {
|
||||||
|
t.Fatal("DummyLogger.CheckForProhibited called without AllMsgs being collected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect _all_ matches, rather than have to re-test repeatedly.
|
||||||
|
// This will particularly help with less deterministic tests with multiple matches.
|
||||||
|
shouldFail := false
|
||||||
|
for i := range l.AllMsgs {
|
||||||
|
if strings.Contains(l.AllMsgs[i], needle) {
|
||||||
|
t.Errorf("log contains %s: %v", reason, l.AllMsgs[i])
|
||||||
|
shouldFail = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldFail {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
67
locksordering.txt
Normal file
67
locksordering.txt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
Here is the list of some established lock ordering.
|
||||||
|
|
||||||
|
In this list, A -> B means that you can have A.Lock() then B.Lock(), not the opposite.
|
||||||
|
|
||||||
|
jetStream -> jsAccount -> Server -> client -> Account
|
||||||
|
|
||||||
|
jetStream -> jsAccount -> stream -> consumer
|
||||||
|
|
||||||
|
The "prMu" lock protects the peer reconcile signaling state on jetStream, so
|
||||||
|
signaling is cheap and never contends on the JS lock. It must not be held while
|
||||||
|
acquiring the JetStream lock.
|
||||||
|
|
||||||
|
jetStream -> prMu
|
||||||
|
|
||||||
|
A lock to protect jetstream account's usage has been introduced: jsAccount.usageMu.
|
||||||
|
This lock is independent and can be invoked under any other lock: jsAccount -> jsa.usageMu, stream -> jsa.usageMu, etc...
|
||||||
|
|
||||||
|
A lock to protect the account's leafnodes list was also introduced to
|
||||||
|
allow that lock to be held and the acquire a client lock which is not
|
||||||
|
possible with the normal account lock.
|
||||||
|
|
||||||
|
accountLeafList -> client
|
||||||
|
|
||||||
|
AccountResolver interface has various implementations, but assume: AccountResolver -> Server
|
||||||
|
|
||||||
|
A reloadMu lock was added to prevent newly connecting clients racing with the configuration reload.
|
||||||
|
This must be taken out as soon as a reload is about to happen before any other locks:
|
||||||
|
|
||||||
|
reloadMu -> Server
|
||||||
|
reloadMu -> optsMu
|
||||||
|
|
||||||
|
The "jscmMu" lock in the Account is used to serialise calls to checkJetStreamMigrate and
|
||||||
|
clearObserverState so that they cannot interleave which would leave Raft nodes in
|
||||||
|
inconsistent observer states.
|
||||||
|
|
||||||
|
jscmMu -> Account -> jsAccount
|
||||||
|
jscmMu -> stream.clsMu
|
||||||
|
jscmMu -> RaftNode
|
||||||
|
|
||||||
|
The "clsMu" lock protects the consumer list on a stream, used for signalling consumer activity.
|
||||||
|
|
||||||
|
stream -> clsMu
|
||||||
|
|
||||||
|
The "clMu" and "ddMu" locks protect clustered and dedupe state respectively.
|
||||||
|
The stream lock (`mset.mu`) is optional, but if holding "clMu" or "ddMu",
|
||||||
|
locking the stream lock afterward would violate locking order.
|
||||||
|
|
||||||
|
stream -> clMu
|
||||||
|
stream -> clMu -> ddMu
|
||||||
|
|
||||||
|
The "mset.batches.mu" lock protects the batching state without needing to hold the stream lock.
|
||||||
|
If "clMu" is used to commit a batch, it should only be acquired while already holding the batch lock.
|
||||||
|
|
||||||
|
stream -> mset.batches.mu -> clMu
|
||||||
|
|
||||||
|
The "isolateMu" lock isolates direct get and msg get reads from writes, so those reads do not
|
||||||
|
observe partially applied rollups or atomic batches. Other read paths (consumer delivery,
|
||||||
|
mirrors/sources, info requests, etc.) are sequentially consistent and may observe a prefix of
|
||||||
|
an inflight batch. It must be acquired before the stream lock, never while holding it.
|
||||||
|
|
||||||
|
isolateMu -> stream
|
||||||
|
|
||||||
|
The "eventIdsMu" lock protects the server's event ID generator. It may be acquired
|
||||||
|
while holding the Server lock, but the Server lock must not be acquired while
|
||||||
|
holding eventIdsMu.
|
||||||
|
|
||||||
|
Server -> eventIdsMu
|
||||||
405
logger/log.go
Normal file
405
logger/log.go
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package logger provides logging facilities for the NATS server
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default file permissions for log files.
|
||||||
|
const defaultLogPerms = os.FileMode(0640)
|
||||||
|
|
||||||
|
// Logger is the server logger
|
||||||
|
type Logger struct {
|
||||||
|
sync.Mutex
|
||||||
|
logger *log.Logger
|
||||||
|
debug bool
|
||||||
|
trace bool
|
||||||
|
infoLabel string
|
||||||
|
warnLabel string
|
||||||
|
errorLabel string
|
||||||
|
fatalLabel string
|
||||||
|
debugLabel string
|
||||||
|
traceLabel string
|
||||||
|
fl *fileLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogOption interface {
|
||||||
|
isLoggerOption()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogUTC controls whether timestamps in the log output should be UTC or local time.
|
||||||
|
type LogUTC bool
|
||||||
|
|
||||||
|
func (l LogUTC) isLoggerOption() {}
|
||||||
|
|
||||||
|
func logFlags(time bool, opts ...LogOption) int {
|
||||||
|
flags := 0
|
||||||
|
if time {
|
||||||
|
flags = log.LstdFlags | log.Lmicroseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch v := opt.(type) {
|
||||||
|
case LogUTC:
|
||||||
|
if time && bool(v) {
|
||||||
|
flags |= log.LUTC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStdLogger creates a logger with output directed to Stderr
|
||||||
|
func NewStdLogger(time, debug, trace, colors, pid bool, opts ...LogOption) *Logger {
|
||||||
|
flags := logFlags(time, opts...)
|
||||||
|
|
||||||
|
pre := ""
|
||||||
|
if pid {
|
||||||
|
pre = pidPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &Logger{
|
||||||
|
logger: log.New(os.Stderr, pre, flags),
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
if colors {
|
||||||
|
setColoredLabelFormats(l)
|
||||||
|
} else {
|
||||||
|
setPlainLabelFormats(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileLogger creates a logger with output directed to a file
|
||||||
|
func NewFileLogger(filename string, time, debug, trace, pid bool, opts ...LogOption) *Logger {
|
||||||
|
flags := logFlags(time, opts...)
|
||||||
|
|
||||||
|
pre := ""
|
||||||
|
if pid {
|
||||||
|
pre = pidPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
fl, err := newFileLogger(filename, pre, time)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error opening file: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &Logger{
|
||||||
|
logger: log.New(fl, pre, flags),
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
fl: fl,
|
||||||
|
}
|
||||||
|
fl.Lock()
|
||||||
|
fl.l = l
|
||||||
|
fl.Unlock()
|
||||||
|
|
||||||
|
setPlainLabelFormats(l)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
type writerAndCloser interface {
|
||||||
|
Write(b []byte) (int, error)
|
||||||
|
Close() error
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileLogger struct {
|
||||||
|
out int64
|
||||||
|
canRotate int32
|
||||||
|
sync.Mutex
|
||||||
|
l *Logger
|
||||||
|
f writerAndCloser
|
||||||
|
limit int64
|
||||||
|
olimit int64
|
||||||
|
pid string
|
||||||
|
time bool
|
||||||
|
closed bool
|
||||||
|
maxNumFiles int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
|
||||||
|
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||||
|
f, err := os.OpenFile(filename, fileflags, defaultLogPerms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fl := &fileLogger{
|
||||||
|
canRotate: 0,
|
||||||
|
f: f,
|
||||||
|
out: stats.Size(),
|
||||||
|
pid: pidPrefix,
|
||||||
|
time: time,
|
||||||
|
}
|
||||||
|
return fl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) setLimit(limit int64) {
|
||||||
|
l.Lock()
|
||||||
|
l.olimit, l.limit = limit, limit
|
||||||
|
atomic.StoreInt32(&l.canRotate, 1)
|
||||||
|
rotateNow := l.out > l.limit
|
||||||
|
l.Unlock()
|
||||||
|
if rotateNow {
|
||||||
|
l.l.Noticef("Rotating logfile...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) setMaxNumFiles(max int) {
|
||||||
|
l.Lock()
|
||||||
|
l.maxNumFiles = max
|
||||||
|
l.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) logDirect(label, format string, v ...any) int {
|
||||||
|
var entrya = [256]byte{}
|
||||||
|
var entry = entrya[:0]
|
||||||
|
if l.pid != "" {
|
||||||
|
entry = append(entry, l.pid...)
|
||||||
|
}
|
||||||
|
if l.time {
|
||||||
|
now := time.Now()
|
||||||
|
year, month, day := now.Date()
|
||||||
|
hour, min, sec := now.Clock()
|
||||||
|
microsec := now.Nanosecond() / 1000
|
||||||
|
entry = append(entry, fmt.Sprintf("%04d/%02d/%02d %02d:%02d:%02d.%06d ",
|
||||||
|
year, month, day, hour, min, sec, microsec)...)
|
||||||
|
}
|
||||||
|
entry = append(entry, label...)
|
||||||
|
entry = append(entry, fmt.Sprintf(format, v...)...)
|
||||||
|
entry = append(entry, '\r', '\n')
|
||||||
|
l.f.Write(entry)
|
||||||
|
return len(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) logPurge(fname string) {
|
||||||
|
var backups []string
|
||||||
|
lDir := filepath.Dir(fname)
|
||||||
|
lBase := filepath.Base(fname)
|
||||||
|
entries, err := os.ReadDir(lDir)
|
||||||
|
if err != nil {
|
||||||
|
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, ".")); found {
|
||||||
|
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
|
||||||
|
if err == nil {
|
||||||
|
backups = append(backups, entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currBackups := len(backups)
|
||||||
|
maxBackups := l.maxNumFiles - 1
|
||||||
|
if currBackups > maxBackups {
|
||||||
|
// backups sorted oldest to latest based on timestamped lexical filename (ReadDir)
|
||||||
|
for i := 0; i < currBackups-maxBackups; i++ {
|
||||||
|
if err := os.Remove(filepath.Join(lDir, string(os.PathSeparator), backups[i])); err != nil {
|
||||||
|
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
|
||||||
|
// Bail fast, we'll try again next rotation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.logDirect(l.l.infoLabel, "Purged log file %q", backups[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) Write(b []byte) (int, error) {
|
||||||
|
if atomic.LoadInt32(&l.canRotate) == 0 {
|
||||||
|
n, err := l.f.Write(b)
|
||||||
|
if err == nil {
|
||||||
|
atomic.AddInt64(&l.out, int64(n))
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
l.Lock()
|
||||||
|
n, err := l.f.Write(b)
|
||||||
|
if err == nil {
|
||||||
|
l.out += int64(n)
|
||||||
|
if l.out > l.limit {
|
||||||
|
if err := l.f.Close(); err != nil {
|
||||||
|
l.limit *= 2
|
||||||
|
l.logDirect(l.l.errorLabel, "Unable to close logfile for rotation (%v), will attempt next rotation at size %v", err, l.limit)
|
||||||
|
l.Unlock()
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
fname := l.f.Name()
|
||||||
|
now := time.Now()
|
||||||
|
bak := fmt.Sprintf("%s.%04d.%02d.%02d.%02d.%02d.%02d.%09d", fname,
|
||||||
|
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(),
|
||||||
|
now.Second(), now.Nanosecond())
|
||||||
|
os.Rename(fname, bak)
|
||||||
|
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||||
|
f, err := os.OpenFile(fname, fileflags, defaultLogPerms)
|
||||||
|
if err != nil {
|
||||||
|
l.Unlock()
|
||||||
|
panic(fmt.Sprintf("Unable to re-open the logfile %q after rotation: %v", fname, err))
|
||||||
|
}
|
||||||
|
l.f = f
|
||||||
|
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
|
||||||
|
l.out = int64(n)
|
||||||
|
l.limit = l.olimit
|
||||||
|
if l.maxNumFiles > 0 {
|
||||||
|
l.logPurge(fname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogger) close() error {
|
||||||
|
l.Lock()
|
||||||
|
if l.closed {
|
||||||
|
l.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l.closed = true
|
||||||
|
l.Unlock()
|
||||||
|
return l.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSizeLimit sets the size of a logfile after which a backup
|
||||||
|
// is created with the file name + "year.month.day.hour.min.sec.nanosec"
|
||||||
|
// and the current log is truncated.
|
||||||
|
func (l *Logger) SetSizeLimit(limit int64) error {
|
||||||
|
l.Lock()
|
||||||
|
if l.fl == nil {
|
||||||
|
l.Unlock()
|
||||||
|
return fmt.Errorf("can set log size limit only for file logger")
|
||||||
|
}
|
||||||
|
fl := l.fl
|
||||||
|
l.Unlock()
|
||||||
|
fl.setLimit(limit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxNumFiles sets the number of archived log files that will be retained
|
||||||
|
func (l *Logger) SetMaxNumFiles(max int) error {
|
||||||
|
l.Lock()
|
||||||
|
if l.fl == nil {
|
||||||
|
l.Unlock()
|
||||||
|
return fmt.Errorf("can set log max number of files only for file logger")
|
||||||
|
}
|
||||||
|
fl := l.fl
|
||||||
|
l.Unlock()
|
||||||
|
fl.setMaxNumFiles(max)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestLogger creates a logger with output directed to Stderr with a prefix.
|
||||||
|
// Useful for tracing in tests when multiple servers are in the same pid
|
||||||
|
func NewTestLogger(prefix string, time bool) *Logger {
|
||||||
|
flags := 0
|
||||||
|
if time {
|
||||||
|
flags = log.LstdFlags | log.Lmicroseconds
|
||||||
|
}
|
||||||
|
l := &Logger{
|
||||||
|
logger: log.New(os.Stderr, prefix, flags),
|
||||||
|
debug: true,
|
||||||
|
trace: true,
|
||||||
|
}
|
||||||
|
setColoredLabelFormats(l)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the io.Closer interface to clean up
|
||||||
|
// resources in the server's logger implementation.
|
||||||
|
// Caller must ensure threadsafety.
|
||||||
|
func (l *Logger) Close() error {
|
||||||
|
if l.fl != nil {
|
||||||
|
return l.fl.close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the pid prefix string
|
||||||
|
func pidPrefix() string {
|
||||||
|
return fmt.Sprintf("[%d] ", os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPlainLabelFormats(l *Logger) {
|
||||||
|
l.infoLabel = "[INF] "
|
||||||
|
l.debugLabel = "[DBG] "
|
||||||
|
l.warnLabel = "[WRN] "
|
||||||
|
l.errorLabel = "[ERR] "
|
||||||
|
l.fatalLabel = "[FTL] "
|
||||||
|
l.traceLabel = "[TRC] "
|
||||||
|
}
|
||||||
|
|
||||||
|
func setColoredLabelFormats(l *Logger) {
|
||||||
|
colorFormat := "[\x1b[%sm%s\x1b[0m] "
|
||||||
|
l.infoLabel = fmt.Sprintf(colorFormat, "32", "INF")
|
||||||
|
l.debugLabel = fmt.Sprintf(colorFormat, "36", "DBG")
|
||||||
|
l.warnLabel = fmt.Sprintf(colorFormat, "0;93", "WRN")
|
||||||
|
l.errorLabel = fmt.Sprintf(colorFormat, "31", "ERR")
|
||||||
|
l.fatalLabel = fmt.Sprintf(colorFormat, "31", "FTL")
|
||||||
|
l.traceLabel = fmt.Sprintf(colorFormat, "33", "TRC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noticef logs a notice statement
|
||||||
|
func (l *Logger) Noticef(format string, v ...any) {
|
||||||
|
l.logger.Printf(l.infoLabel+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a notice statement
|
||||||
|
func (l *Logger) Warnf(format string, v ...any) {
|
||||||
|
l.logger.Printf(l.warnLabel+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs an error statement
|
||||||
|
func (l *Logger) Errorf(format string, v ...any) {
|
||||||
|
l.logger.Printf(l.errorLabel+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a fatal error
|
||||||
|
func (l *Logger) Fatalf(format string, v ...any) {
|
||||||
|
l.logger.Fatalf(l.fatalLabel+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a debug statement
|
||||||
|
func (l *Logger) Debugf(format string, v ...any) {
|
||||||
|
if l.debug {
|
||||||
|
l.logger.Printf(l.debugLabel+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracef logs a trace statement
|
||||||
|
func (l *Logger) Tracef(format string, v ...any) {
|
||||||
|
if l.trace {
|
||||||
|
l.logger.Printf(l.traceLabel+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
327
logger/log_test.go
Normal file
327
logger/log_test.go
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
// Copyright 2012-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStdLogger(t *testing.T) {
|
||||||
|
logger := NewStdLogger(false, false, false, false, false)
|
||||||
|
|
||||||
|
flags := logger.logger.Flags()
|
||||||
|
if flags != 0 {
|
||||||
|
t.Fatalf("Expected %v, received %v\n", 0, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerWithDebugTraceAndTime(t *testing.T) {
|
||||||
|
logger := NewStdLogger(true, true, true, false, false)
|
||||||
|
|
||||||
|
flags := logger.logger.Flags()
|
||||||
|
if flags != log.LstdFlags|log.Lmicroseconds {
|
||||||
|
t.Fatalf("Expected %d, received %d\n", log.LstdFlags, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerNotice(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, false, false, false, false)
|
||||||
|
logger.Noticef("foo")
|
||||||
|
}, "[INF] foo\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerNoticeWithColor(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, false, false, true, false)
|
||||||
|
logger.Noticef("foo")
|
||||||
|
}, "[\x1b[32mINF\x1b[0m] foo\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerDebug(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, true, false, false, false)
|
||||||
|
logger.Debugf("foo %s", "bar")
|
||||||
|
}, "[DBG] foo bar\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerDebugWithOutDebug(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, false, false, false, false)
|
||||||
|
logger.Debugf("foo")
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerTrace(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, false, true, false, false)
|
||||||
|
logger.Tracef("foo")
|
||||||
|
}, "[TRC] foo\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLoggerTraceWithOutDebug(t *testing.T) {
|
||||||
|
expectOutput(t, func() {
|
||||||
|
logger := NewStdLogger(false, false, false, false, false)
|
||||||
|
logger.Tracef("foo")
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileLogger(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := createFileAtDir(t, tmpDir, "nats-server:log_")
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
logger := NewFileLogger(file.Name(), false, false, false, false)
|
||||||
|
defer logger.Close()
|
||||||
|
logger.Noticef("foo")
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not read logfile: %v", err)
|
||||||
|
}
|
||||||
|
if len(buf) <= 0 {
|
||||||
|
t.Fatal("Expected a non-zero length logfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(buf) != "[INF] foo\n" {
|
||||||
|
t.Fatalf("Expected '%s', received '%s'\n", "[INFO] foo", string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
file = createFileAtDir(t, tmpDir, "nats-server:log_")
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
logger = NewFileLogger(file.Name(), true, false, true, true)
|
||||||
|
defer logger.Close()
|
||||||
|
logger.Errorf("foo")
|
||||||
|
|
||||||
|
buf, err = os.ReadFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not read logfile: %v", err)
|
||||||
|
}
|
||||||
|
if len(buf) <= 0 {
|
||||||
|
t.Fatal("Expected a non-zero length logfile")
|
||||||
|
}
|
||||||
|
str := string(buf)
|
||||||
|
errMsg := fmt.Sprintf("Expected '%s', received '%s'\n", "[pid] <date> [ERR] foo", str)
|
||||||
|
pidEnd := strings.Index(str, " ")
|
||||||
|
infoStart := strings.LastIndex(str, "[ERR]")
|
||||||
|
if pidEnd == -1 || infoStart == -1 {
|
||||||
|
t.Fatalf("%v", errMsg)
|
||||||
|
}
|
||||||
|
pid := str[0:pidEnd]
|
||||||
|
if pid[0] != '[' || pid[len(pid)-1] != ']' {
|
||||||
|
t.Fatalf("%v", errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
date := str[pidEnd:infoStart]
|
||||||
|
dateRegExp := "[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}"
|
||||||
|
reg, err := regexp.Compile(dateRegExp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Compile date regexp error: %v", err)
|
||||||
|
}
|
||||||
|
if matched := reg.Match([]byte(date)); !matched {
|
||||||
|
t.Fatalf("Date string '%s' does not match '%s'", date, dateRegExp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(str, "[ERR] foo\n") {
|
||||||
|
t.Fatalf("%v", errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileLoggerSizeLimit(t *testing.T) {
|
||||||
|
// Create std logger
|
||||||
|
logger := NewStdLogger(true, false, false, false, true)
|
||||||
|
if err := logger.SetSizeLimit(1000); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "only for file logger") {
|
||||||
|
t.Fatalf("Expected error about being able to use only for file logger, got %v", err)
|
||||||
|
}
|
||||||
|
logger.Close()
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
file := createFileAtDir(t, tmpDir, "log_")
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||||
|
defer logger.Close()
|
||||||
|
logger.SetSizeLimit(1000)
|
||||||
|
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
logger.Noticef("This is a line in the log file")
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := os.ReadDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading logs dir: %v", err)
|
||||||
|
}
|
||||||
|
if len(files) == 1 {
|
||||||
|
t.Fatalf("Expected file to have been rotated")
|
||||||
|
}
|
||||||
|
lastBackup := files[len(files)-1]
|
||||||
|
if err := logger.Close(); err != nil {
|
||||||
|
t.Fatalf("Error closing log: %v", err)
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error loading latest log: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(content, []byte("Rotated log")) ||
|
||||||
|
!bytes.Contains(content, []byte(lastBackup.Name())) {
|
||||||
|
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir = t.TempDir()
|
||||||
|
|
||||||
|
// Recreate logger and don't set a limit
|
||||||
|
file = createFileAtDir(t, tmpDir, "log_")
|
||||||
|
file.Close()
|
||||||
|
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||||
|
defer logger.Close()
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
logger.Noticef("This is line %d in the log file", i+1)
|
||||||
|
}
|
||||||
|
files, err = os.ReadDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading logs dir: %v", err)
|
||||||
|
}
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Fatalf("Expected file to not be rotated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now set a limit that is below current size
|
||||||
|
logger.SetSizeLimit(1000)
|
||||||
|
// Should have triggered rotation
|
||||||
|
files, err = os.ReadDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading logs dir: %v", err)
|
||||||
|
}
|
||||||
|
if len(files) <= 1 {
|
||||||
|
t.Fatalf("Expected file to have been rotated")
|
||||||
|
}
|
||||||
|
if err := logger.Close(); err != nil {
|
||||||
|
t.Fatalf("Error closing log: %v", err)
|
||||||
|
}
|
||||||
|
lastBackup = files[len(files)-1]
|
||||||
|
content, err = os.ReadFile(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error loading latest log: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(content, []byte("Rotated log")) ||
|
||||||
|
!bytes.Contains(content, []byte(lastBackup.Name())) {
|
||||||
|
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||||
|
defer logger.Close()
|
||||||
|
logger.SetSizeLimit(1000)
|
||||||
|
|
||||||
|
// Check error on rotate.
|
||||||
|
logger.Lock()
|
||||||
|
logger.fl.Lock()
|
||||||
|
failClose := &fileLogFailClose{logger.fl.f, true}
|
||||||
|
logger.fl.f = failClose
|
||||||
|
logger.fl.Unlock()
|
||||||
|
logger.Unlock()
|
||||||
|
// Write a big line that will force rotation.
|
||||||
|
// Since we fail to close the log file, we should have bumped the limit to 2000
|
||||||
|
logger.Noticef("This is a big line: %v", make([]byte, 1000))
|
||||||
|
|
||||||
|
// Remove the failure
|
||||||
|
failClose.fail = false
|
||||||
|
// Write a big line that makes rotation happen
|
||||||
|
logger.Noticef("This is a big line: %v", make([]byte, 2000))
|
||||||
|
// Close
|
||||||
|
logger.Close()
|
||||||
|
|
||||||
|
files, err = os.ReadDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading logs dir: %v", err)
|
||||||
|
}
|
||||||
|
lastBackup = files[len(files)-1]
|
||||||
|
content, err = os.ReadFile(filepath.Join(tmpDir, lastBackup.Name()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading backup file: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(content, []byte("on purpose")) || !bytes.Contains(content, []byte("size 2000")) {
|
||||||
|
t.Fatalf("Expected error that file could not rotated and max size bumped to 2000, got %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileLogFailClose struct {
|
||||||
|
writerAndCloser
|
||||||
|
fail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLogFailClose) Close() error {
|
||||||
|
if l.fail {
|
||||||
|
return fmt.Errorf("on purpose")
|
||||||
|
}
|
||||||
|
return l.writerAndCloser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectOutput(t *testing.T, f func(), expected string) {
|
||||||
|
old := os.Stderr // keep backup of the real stderr
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
|
f()
|
||||||
|
|
||||||
|
outC := make(chan string)
|
||||||
|
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||||
|
go func() {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
outC <- buf.String()
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Stderr.Close()
|
||||||
|
os.Stderr = old // restoring the real stdout
|
||||||
|
out := <-outC
|
||||||
|
if out != expected {
|
||||||
|
t.Fatalf("Expected '%s', received '%s'\n", expected, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFileAtDir(t *testing.T, dir, prefix string) *os.File {
|
||||||
|
t.Helper()
|
||||||
|
f, err := os.CreateTemp(dir, prefix)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
132
logger/syslog.go
Normal file
132
logger/syslog.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/syslog"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SysLogger provides a system logger facility
|
||||||
|
type SysLogger struct {
|
||||||
|
writer *syslog.Writer
|
||||||
|
debug bool
|
||||||
|
trace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSyslogName sets the name to use for the syslog.
|
||||||
|
// Currently used only on Windows.
|
||||||
|
func SetSyslogName(name string) {}
|
||||||
|
|
||||||
|
// GetSysLoggerTag generates the tag name for use in syslog statements. If
|
||||||
|
// the executable is linked, the name of the link will be used as the tag,
|
||||||
|
// otherwise, the name of the executable is used. "nats-server" is the default
|
||||||
|
// for the NATS server.
|
||||||
|
func GetSysLoggerTag() string {
|
||||||
|
procName := os.Args[0]
|
||||||
|
if strings.ContainsRune(procName, os.PathSeparator) {
|
||||||
|
parts := strings.FieldsFunc(procName, func(c rune) bool {
|
||||||
|
return c == os.PathSeparator
|
||||||
|
})
|
||||||
|
procName = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
return procName
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSysLogger creates a new system logger
|
||||||
|
func NewSysLogger(debug, trace bool) *SysLogger {
|
||||||
|
w, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_NOTICE, GetSysLoggerTag())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error connecting to syslog: %q", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SysLogger{
|
||||||
|
writer: w,
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRemoteSysLogger creates a new remote system logger
|
||||||
|
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
|
||||||
|
network, addr := getNetworkAndAddr(fqn)
|
||||||
|
w, err := syslog.Dial(network, addr, syslog.LOG_DEBUG, GetSysLoggerTag())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error connecting to syslog: %q", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SysLogger{
|
||||||
|
writer: w,
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNetworkAndAddr(fqn string) (network, addr string) {
|
||||||
|
u, err := url.Parse(fqn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
network = u.Scheme
|
||||||
|
if network == "udp" || network == "tcp" {
|
||||||
|
addr = u.Host
|
||||||
|
} else if network == "unix" {
|
||||||
|
addr = u.Path
|
||||||
|
} else {
|
||||||
|
log.Fatalf("error invalid network type: %q", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noticef logs a notice statement
|
||||||
|
func (l *SysLogger) Noticef(format string, v ...any) {
|
||||||
|
l.writer.Notice(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a warning statement
|
||||||
|
func (l *SysLogger) Warnf(format string, v ...any) {
|
||||||
|
l.writer.Warning(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a fatal error
|
||||||
|
func (l *SysLogger) Fatalf(format string, v ...any) {
|
||||||
|
l.writer.Crit(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs an error statement
|
||||||
|
func (l *SysLogger) Errorf(format string, v ...any) {
|
||||||
|
l.writer.Err(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a debug statement
|
||||||
|
func (l *SysLogger) Debugf(format string, v ...any) {
|
||||||
|
if l.debug {
|
||||||
|
l.writer.Debug(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracef logs a trace statement
|
||||||
|
func (l *SysLogger) Tracef(format string, v ...any) {
|
||||||
|
if l.trace {
|
||||||
|
l.writer.Notice(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
237
logger/syslog_test.go
Normal file
237
logger/syslog_test.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serverFQN string
|
||||||
|
|
||||||
|
func TestSysLogger(t *testing.T) {
|
||||||
|
logger := NewSysLogger(false, false)
|
||||||
|
|
||||||
|
if logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||||
|
logger := NewSysLogger(true, true)
|
||||||
|
|
||||||
|
if !logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTag(t *testing.T, exePath, expected string) {
|
||||||
|
os.Args[0] = exePath
|
||||||
|
if result := GetSysLoggerTag(); result != expected {
|
||||||
|
t.Fatalf("Expected %s, received %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreArg(orig string) {
|
||||||
|
os.Args[0] = orig
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSysLoggerTagGen(t *testing.T) {
|
||||||
|
origArg := os.Args[0]
|
||||||
|
defer restoreArg(origArg)
|
||||||
|
|
||||||
|
testTag(t, "nats-server", "nats-server")
|
||||||
|
testTag(t, filepath.Join(".", "nats-server"), "nats-server")
|
||||||
|
testTag(t, filepath.Join("home", "bin", "nats-server"), "nats-server")
|
||||||
|
testTag(t, filepath.Join("..", "..", "nats-server"), "nats-server")
|
||||||
|
testTag(t, "nats-server.service1", "nats-server.service1")
|
||||||
|
testTag(t, "nats-server_service1", "nats-server_service1")
|
||||||
|
testTag(t, "nats-server-service1", "nats-server-service1")
|
||||||
|
testTag(t, "nats-server service1", "nats-server service1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSysLoggerTag(t *testing.T) {
|
||||||
|
origArg := os.Args[0]
|
||||||
|
defer restoreArg(origArg)
|
||||||
|
|
||||||
|
os.Args[0] = "ServerLoggerTag"
|
||||||
|
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||||
|
logger.Noticef("foo")
|
||||||
|
|
||||||
|
line := <-done
|
||||||
|
data := strings.Split(line, "[")
|
||||||
|
if len(data) != 2 {
|
||||||
|
t.Fatalf("Unexpected syslog line %s\n", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(data[0], os.Args[0]) {
|
||||||
|
t.Fatalf("Expected '%s', received '%s'\n", os.Args[0], data[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLogger(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||||
|
|
||||||
|
if !logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||||
|
}
|
||||||
|
logger.Noticef("foo")
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLoggerNotice(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||||
|
|
||||||
|
logger.Noticef("foo %s", "bar")
|
||||||
|
expectSyslogOutput(t, <-done, "foo bar\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLoggerDebug(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||||
|
|
||||||
|
logger.Debugf("foo %s", "qux")
|
||||||
|
expectSyslogOutput(t, <-done, "foo qux\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLoggerDebugDisabled(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, false, false)
|
||||||
|
|
||||||
|
logger.Debugf("foo %s", "qux")
|
||||||
|
rcvd := <-done
|
||||||
|
if rcvd != "" {
|
||||||
|
t.Fatalf("Unexpected syslog response %s\n", rcvd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLoggerTrace(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||||
|
|
||||||
|
logger.Tracef("foo %s", "qux")
|
||||||
|
expectSyslogOutput(t, <-done, "foo qux\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteSysLoggerTraceDisabled(t *testing.T) {
|
||||||
|
done := make(chan string)
|
||||||
|
startServer(done)
|
||||||
|
logger := NewRemoteSysLogger(serverFQN, true, false)
|
||||||
|
|
||||||
|
logger.Tracef("foo %s", "qux")
|
||||||
|
rcvd := <-done
|
||||||
|
if rcvd != "" {
|
||||||
|
t.Fatalf("Unexpected syslog response %s\n", rcvd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNetworkAndAddrUDP(t *testing.T) {
|
||||||
|
n, a := getNetworkAndAddr("udp://foo.com:1000")
|
||||||
|
|
||||||
|
if n != "udp" {
|
||||||
|
t.Fatalf("Unexpected network %s\n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != "foo.com:1000" {
|
||||||
|
t.Fatalf("Unexpected addr %s\n", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNetworkAndAddrTCP(t *testing.T) {
|
||||||
|
n, a := getNetworkAndAddr("tcp://foo.com:1000")
|
||||||
|
|
||||||
|
if n != "tcp" {
|
||||||
|
t.Fatalf("Unexpected network %s\n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != "foo.com:1000" {
|
||||||
|
t.Fatalf("Unexpected addr %s\n", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNetworkAndAddrUnix(t *testing.T) {
|
||||||
|
n, a := getNetworkAndAddr("unix:///foo.sock")
|
||||||
|
|
||||||
|
if n != "unix" {
|
||||||
|
t.Fatalf("Unexpected network %s\n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != "/foo.sock" {
|
||||||
|
t.Fatalf("Unexpected addr %s\n", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func expectSyslogOutput(t *testing.T, line string, expected string) {
|
||||||
|
data := strings.Split(line, "]: ")
|
||||||
|
if len(data) != 2 {
|
||||||
|
t.Fatalf("Unexpected syslog line %s\n", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[1] != expected {
|
||||||
|
t.Fatalf("Expected '%s', received '%s'\n", expected, data[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSyslog(c net.PacketConn, done chan<- string) {
|
||||||
|
var buf [4096]byte
|
||||||
|
var rcvd string
|
||||||
|
for {
|
||||||
|
n, _, err := c.ReadFrom(buf[:])
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rcvd += string(buf[:n])
|
||||||
|
}
|
||||||
|
done <- rcvd
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(done chan<- string) {
|
||||||
|
c, e := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if e != nil {
|
||||||
|
log.Fatalf("net.ListenPacket failed udp :0 %v", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverFQN = fmt.Sprintf("udp://%s", c.LocalAddr().String())
|
||||||
|
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||||
|
go runSyslog(c, done)
|
||||||
|
}
|
||||||
112
logger/syslog_windows.go
Normal file
112
logger/syslog_windows.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package logger logs to the windows event log
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var natsEventSource = "NATS-Server"
|
||||||
|
|
||||||
|
// SetSyslogName sets the name to use for the system log event source
|
||||||
|
func SetSyslogName(name string) {
|
||||||
|
natsEventSource = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysLogger logs to the windows event logger
|
||||||
|
type SysLogger struct {
|
||||||
|
writer *eventlog.Log
|
||||||
|
debug bool
|
||||||
|
trace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSysLogger creates a log using the windows event logger
|
||||||
|
func NewSysLogger(debug, trace bool) *SysLogger {
|
||||||
|
if err := eventlog.InstallAsEventCreate(natsEventSource, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "registry key already exists") {
|
||||||
|
panic(fmt.Sprintf("could not access event log: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := eventlog.Open(natsEventSource)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not open event log: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SysLogger{
|
||||||
|
writer: w,
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRemoteSysLogger creates a remote event logger
|
||||||
|
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
|
||||||
|
w, err := eventlog.OpenRemote(fqn, natsEventSource)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not open event log: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SysLogger{
|
||||||
|
writer: w,
|
||||||
|
debug: debug,
|
||||||
|
trace: trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMsg(tag, format string, v ...any) string {
|
||||||
|
orig := fmt.Sprintf(format, v...)
|
||||||
|
return fmt.Sprintf("pid[%d][%s]: %s", os.Getpid(), tag, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noticef logs a notice statement
|
||||||
|
func (l *SysLogger) Noticef(format string, v ...any) {
|
||||||
|
l.writer.Info(1, formatMsg("NOTICE", format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a warning statement
|
||||||
|
func (l *SysLogger) Warnf(format string, v ...any) {
|
||||||
|
l.writer.Info(1, formatMsg("WARN", format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a fatal error
|
||||||
|
func (l *SysLogger) Fatalf(format string, v ...any) {
|
||||||
|
msg := formatMsg("FATAL", format, v...)
|
||||||
|
l.writer.Error(5, msg)
|
||||||
|
panic(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs an error statement
|
||||||
|
func (l *SysLogger) Errorf(format string, v ...any) {
|
||||||
|
l.writer.Error(2, formatMsg("ERROR", format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a debug statement
|
||||||
|
func (l *SysLogger) Debugf(format string, v ...any) {
|
||||||
|
if l.debug {
|
||||||
|
l.writer.Info(3, formatMsg("DEBUG", format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracef logs a trace statement
|
||||||
|
func (l *SysLogger) Tracef(format string, v ...any) {
|
||||||
|
if l.trace {
|
||||||
|
l.writer.Info(4, formatMsg("TRACE", format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
140
logger/syslog_windows_test.go
Normal file
140
logger/syslog_windows_test.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Skips testing if we do not have privledges to run this test.
|
||||||
|
// This lets us skip the tests for general (non admin/system) users.
|
||||||
|
func checkPrivledges(t *testing.T) {
|
||||||
|
src := "NATS-eventlog-testsource"
|
||||||
|
defer eventlog.Remove(src)
|
||||||
|
if err := eventlog.InstallAsEventCreate(src, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Access is denied") {
|
||||||
|
// Skip this test because elevated privileges are required.
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
// let the tests report other types of errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastLogEntryContains reads the last entry (/c:1 /rd:true) written
|
||||||
|
// to the event log by the NATS-Server source, returning true if the
|
||||||
|
// passed text was found, false otherwise.
|
||||||
|
func lastLogEntryContains(t *testing.T, text string) bool {
|
||||||
|
var output []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cmd := exec.Command("wevtutil.exe", "qe", "Application", "/q:*[System[Provider[@Name='NATS-Server']]]",
|
||||||
|
"/rd:true", "/c:1")
|
||||||
|
if output, err = cmd.Output(); err != nil {
|
||||||
|
t.Fatalf("Unable to execute command: %v", err)
|
||||||
|
}
|
||||||
|
return strings.Contains(string(output), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSysLogger tests event logging on windows
|
||||||
|
func TestSysLogger(t *testing.T) {
|
||||||
|
checkPrivledges(t)
|
||||||
|
logger := NewSysLogger(false, false)
|
||||||
|
if logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||||
|
}
|
||||||
|
logger.Noticef("%s", "Noticef")
|
||||||
|
if !lastLogEntryContains(t, "[NOTICE]: Noticef") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Errorf("%s", "Errorf")
|
||||||
|
if !lastLogEntryContains(t, "[ERROR]: Errorf") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Tracef("%s", "Tracef")
|
||||||
|
if lastLogEntryContains(t, "Tracef") {
|
||||||
|
t.Fatalf("should not contain log entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("%s", "Debugf")
|
||||||
|
if lastLogEntryContains(t, "Debugf") {
|
||||||
|
t.Fatalf("should not contain log entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSysLoggerWithDebugAndTrace tests event logging
|
||||||
|
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||||
|
checkPrivledges(t)
|
||||||
|
logger := NewSysLogger(true, true)
|
||||||
|
if !logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Tracef("%s", "Tracef")
|
||||||
|
if !lastLogEntryContains(t, "[TRACE]: Tracef") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("%s", "Debugf")
|
||||||
|
if !lastLogEntryContains(t, "[DEBUG]: Debugf") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRemoteSysLoggerWithDebugAndTrace tests remote event logging
|
||||||
|
func TestRemoteSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||||
|
checkPrivledges(t)
|
||||||
|
logger := NewRemoteSysLogger("", true, true)
|
||||||
|
if !logger.debug {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logger.trace {
|
||||||
|
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||||
|
}
|
||||||
|
logger.Tracef("NATS %s", "[TRACE]: Remote Noticef")
|
||||||
|
if !lastLogEntryContains(t, "Remote Noticef") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSysLoggerFatalf(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if !lastLogEntryContains(t, "[FATAL]: Fatalf") {
|
||||||
|
t.Fatalf("missing log entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
checkPrivledges(t)
|
||||||
|
logger := NewSysLogger(true, true)
|
||||||
|
logger.Fatalf("%s", "Fatalf")
|
||||||
|
t.Fatalf("did not panic when expected to")
|
||||||
|
}
|
||||||
BIN
logos/nats-horizontal-color.png
Normal file
BIN
logos/nats-horizontal-color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
logos/nats-server.png
Normal file
BIN
logos/nats-server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
135
main.go
Normal file
135
main.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright 2012-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run server/errors_gen.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats-server/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usageStr = `
|
||||||
|
Usage: nats-server [options]
|
||||||
|
|
||||||
|
Server Options:
|
||||||
|
-a, --addr, --net <host> Bind to host address (default: 0.0.0.0)
|
||||||
|
-p, --port <port> Use port for clients (default: 4222)
|
||||||
|
-n, --name
|
||||||
|
--server_name <server_name> Server name (default: auto)
|
||||||
|
-P, --pid <file> File to store PID
|
||||||
|
-m, --http_port <port> Use port for http monitoring
|
||||||
|
-ms,--https_port <port> Use port for https monitoring
|
||||||
|
-c, --config <file> Configuration file
|
||||||
|
-t Test configuration and exit
|
||||||
|
-sl,--signal <signal>[=<pid>] Send signal to nats-server process (ldm, stop, quit, term, reopen, reload)
|
||||||
|
<pid> can be either a PID (e.g. 1) or the path to a PID file (e.g. /var/run/nats-server.pid)
|
||||||
|
--client_advertise <string> Client URL to advertise to other servers
|
||||||
|
--ports_file_dir <dir> Creates a ports file in the specified directory (<executable_name>_<pid>.ports).
|
||||||
|
|
||||||
|
Logging Options:
|
||||||
|
-l, --log <file> File to redirect log output
|
||||||
|
-T, --logtime Timestamp log entries (default: true)
|
||||||
|
--logtime_utc Timestamps in UTC instead of local timezone (default: false)
|
||||||
|
-s, --syslog Log to syslog or windows event log
|
||||||
|
-r, --remote_syslog <addr> Syslog server addr (udp://localhost:514)
|
||||||
|
-D, --debug Enable debugging output
|
||||||
|
-V, --trace Trace the raw protocol
|
||||||
|
-VV Verbose trace (traces system account as well)
|
||||||
|
-DV Debug and trace
|
||||||
|
-DVV Debug and verbose trace (traces system account as well)
|
||||||
|
--log_size_limit <limit> Logfile size limit (default: auto)
|
||||||
|
--max_traced_msg_len <len> Maximum printable length for traced messages (default: unlimited)
|
||||||
|
|
||||||
|
JetStream Options:
|
||||||
|
-js, --jetstream Enable JetStream functionality
|
||||||
|
-sd, --store_dir <dir> Set the storage directory
|
||||||
|
|
||||||
|
Authorization Options:
|
||||||
|
--user <user> User required for connections
|
||||||
|
--pass <password> Password required for connections
|
||||||
|
--auth <token> Authorization token required for connections
|
||||||
|
|
||||||
|
TLS Options:
|
||||||
|
--tls Enable TLS, do not verify clients (default: false)
|
||||||
|
--tlscert <file> Server certificate file
|
||||||
|
--tlskey <file> Private key for server certificate
|
||||||
|
--tlsverify Enable TLS, verify client certificates
|
||||||
|
--tlscacert <file> Client certificate CA for verification
|
||||||
|
|
||||||
|
Cluster Options:
|
||||||
|
--routes <rurl-1, rurl-2> Routes to solicit and connect
|
||||||
|
--cluster <cluster-url> Cluster URL for solicited routes
|
||||||
|
--cluster_name <string> Cluster Name, if not set one will be dynamically generated
|
||||||
|
--no_advertise <bool> Do not advertise known cluster information to clients
|
||||||
|
--cluster_advertise <string> Cluster URL to advertise to other servers
|
||||||
|
--connect_retries <number> For implicit routes, number of connect retries
|
||||||
|
--cluster_listen <url> Cluster url from which members can solicit routes
|
||||||
|
|
||||||
|
Profiling Options:
|
||||||
|
--profile <port> Profiling HTTP port
|
||||||
|
|
||||||
|
Common Options:
|
||||||
|
-h, --help Show this message
|
||||||
|
-v, --version Show version
|
||||||
|
--help_tls TLS help
|
||||||
|
`
|
||||||
|
|
||||||
|
// usage will print out the flag options for the server.
|
||||||
|
func usage() {
|
||||||
|
fmt.Printf("%s\n", usageStr)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
exe := "nats-server"
|
||||||
|
|
||||||
|
// Create a FlagSet and sets the usage
|
||||||
|
fs := flag.NewFlagSet(exe, flag.ExitOnError)
|
||||||
|
fs.Usage = usage
|
||||||
|
|
||||||
|
// Configure the options from the flags/config file
|
||||||
|
opts, err := server.ConfigureOptions(fs, os.Args[1:],
|
||||||
|
server.PrintServerAndExit,
|
||||||
|
fs.Usage,
|
||||||
|
server.PrintTLSHelpAndDie)
|
||||||
|
if err != nil {
|
||||||
|
server.PrintAndDie(fmt.Sprintf("%s: %s", exe, err))
|
||||||
|
} else if opts.CheckConfig {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: configuration file %s is valid (%s)\n", exe, opts.ConfigFile, opts.ConfigDigest())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redact secret arguments before expvar reads them.
|
||||||
|
server.RedactArgs(os.Args)
|
||||||
|
|
||||||
|
// Create the server with appropriate options.
|
||||||
|
s, err := server.NewServer(opts)
|
||||||
|
if err != nil {
|
||||||
|
server.PrintAndDie(fmt.Sprintf("%s: %s", exe, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the logger based on the flags.
|
||||||
|
s.ConfigureLogger()
|
||||||
|
|
||||||
|
// Start things up. Block here until done.
|
||||||
|
if err := server.Run(s); err != nil {
|
||||||
|
server.PrintAndDie(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WaitForShutdown()
|
||||||
|
}
|
||||||
62
scripts/cov.sh
Executable file
62
scripts/cov.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run from directory above via ./scripts/cov.sh
|
||||||
|
|
||||||
|
check_file () {
|
||||||
|
# If the content of the file is simply "mode: atomic", then it means that the
|
||||||
|
# code coverage did not complete due to a panic in one of the tests.
|
||||||
|
if [[ $(cat ./cov/$2) == "mode: atomic" ]]; then
|
||||||
|
echo "#############################################"
|
||||||
|
echo "## Code coverage for $1 package failed ##"
|
||||||
|
echo "#############################################"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Do not globally set the -e flag because we don't a flapper to prevent the push to coverall.
|
||||||
|
|
||||||
|
export GO111MODULE="on"
|
||||||
|
|
||||||
|
go install github.com/wadey/gocovmerge@latest
|
||||||
|
# Fail fast by checking if we can run gocovmerge
|
||||||
|
gocovmerge
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
echo "Unable to run gocovmerge"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf ./cov
|
||||||
|
mkdir cov
|
||||||
|
#
|
||||||
|
# Since it is difficult to get a full run without a flapper, do not use `-failfast`.
|
||||||
|
# It is better to have one flapper or two and still get the report than have
|
||||||
|
# to re-run the whole code coverage. One or two failed tests should not affect
|
||||||
|
# so much the code coverage.
|
||||||
|
#
|
||||||
|
# However, we need to take into account that if there is a panic in one test, all
|
||||||
|
# other tests in that package will not run, which then would cause the code coverage
|
||||||
|
# to drastically be lowered. In that case, we don't want the code coverage to be
|
||||||
|
# uploaded.
|
||||||
|
#
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/conf.out ./conf -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "conf" "conf.out"
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/internal.out ./internal/ldap -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "internal" "internal.out"
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/log.out ./logger -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "logger" "log.out"
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/server_avl.out ./server/avl -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "server_avl" "server_avl.out"
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/server.out ./server -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "server" "server.out"
|
||||||
|
go test -v -covermode=atomic -coverprofile=./cov/test.out -coverpkg=./server ./test -timeout=1h -tags=skip_no_race_tests
|
||||||
|
check_file "test" "test.out"
|
||||||
|
|
||||||
|
# At this point, if that fails, we want the caller to know about the failure.
|
||||||
|
set -e
|
||||||
|
gocovmerge ./cov/*.out > acc.out
|
||||||
|
rm -rf ./cov
|
||||||
|
|
||||||
|
# If no argument passed, launch a browser to see the results.
|
||||||
|
if [[ $1 == "" ]]; then
|
||||||
|
go tool cover -html=acc.out
|
||||||
|
fi
|
||||||
|
set +e
|
||||||
138
scripts/runTestsOnTravis.sh
Executable file
138
scripts/runTestsOnTravis.sh
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
if [ "$1" = "compile" ]; then
|
||||||
|
# First check that NATS builds.
|
||||||
|
go build -v;
|
||||||
|
|
||||||
|
# Now run the linters.
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.4;
|
||||||
|
golangci-lint run;
|
||||||
|
if [ "$TRAVIS_TAG" != "" ]; then
|
||||||
|
go test -v -run=TestVersionMatchesTag ./server -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -count=1 -vet=off
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [ "$1" = "build_only" ]; then
|
||||||
|
go build -v;
|
||||||
|
|
||||||
|
elif [ "$1" = "no_race_1_tests" ]; then
|
||||||
|
|
||||||
|
# Run tests without the `-race` flag. By convention, those tests start
|
||||||
|
# with `TestNoRace`. This will include all test files that have the
|
||||||
|
# "&& !skip_no_race_1_tests" in the go build directive.
|
||||||
|
|
||||||
|
go test -v -p=1 -run=TestNoRace ./... -tags=skip_no_race_2_tests -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "no_race_2_tests" ]; then
|
||||||
|
|
||||||
|
# Run tests without the `-race` flag. By convention, those tests start
|
||||||
|
# with `TestNoRace`. This will include all test files that have the
|
||||||
|
# "&& !skip_no_race_2_tests" in the go build directive.
|
||||||
|
|
||||||
|
go test -v -p=1 -run=TestNoRace ./... -tags=skip_no_race_1_tests -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "store_tests" ]; then
|
||||||
|
|
||||||
|
# Run store tests. By convention, all file store tests start with `TestFileStore`,
|
||||||
|
# and memory store tests start with `TestMemStore`.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=Test\(Store\|FileStore\|MemStore\) ./server -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_tests" ]; then
|
||||||
|
|
||||||
|
# Run JetStream non-clustered tests. By convention, all JS tests start
|
||||||
|
# with `TestJetStream`. We exclude the clustered, super-clustered and
|
||||||
|
# consumer tests by using the appropriate tags.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStream ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_3,skip_js_cluster_tests_4,skip_js_super_cluster_tests,skip_js_consumer_tests -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_consumer_tests" ]; then
|
||||||
|
|
||||||
|
# Run JetStream consumer tests. All tests start with TestJetStreamConsumer prefix.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamConsumer ./server -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "raft_tests" ]; then
|
||||||
|
|
||||||
|
# Run the RAFT tests. All tests start with TestNRG prefix.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 ./server/... -run="^TestNRG" -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_cluster_tests_1" ]; then
|
||||||
|
|
||||||
|
# Run JetStream clustered tests. By convention, all JS cluster tests
|
||||||
|
# start with `TestJetStreamCluster`. Will run the first batch of tests,
|
||||||
|
# excluding others with use of proper tags.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests_2,skip_js_cluster_tests_3,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_cluster_tests_2" ]; then
|
||||||
|
|
||||||
|
# Run JetStream clustered tests. By convention, all JS cluster tests
|
||||||
|
# start with `TestJetStreamCluster`. Will run the second batch of tests,
|
||||||
|
# excluding others with use of proper tags.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_3,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_cluster_tests_3" ]; then
|
||||||
|
|
||||||
|
# Run JetStream clustered tests. By convention, all JS cluster tests
|
||||||
|
# start with `TestJetStreamCluster`. Will run the third batch of tests,
|
||||||
|
# excluding others with use of proper tags.
|
||||||
|
#
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_cluster_tests_4" ]; then
|
||||||
|
|
||||||
|
# Run JetStream clustered tests. By convention, all JS cluster tests
|
||||||
|
# start with `TestJetStreamCluster`. Will run the third batch of tests,
|
||||||
|
# excluding others with use of proper tags.
|
||||||
|
#
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_3 -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "js_super_cluster_tests" ]; then
|
||||||
|
|
||||||
|
# Run JetStream super clustered tests. By convention, all JS super cluster
|
||||||
|
# tests start with `TestJetStreamSuperCluster`.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestJetStreamSuperCluster ./server -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "mqtt_tests" ]; then
|
||||||
|
|
||||||
|
# Run MQTT tests. By convention, all MQTT tests start with `TestMQTT`.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestMQTT ./server -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "msgtrace_tests" ]; then
|
||||||
|
|
||||||
|
# Run Message Tracing tests. By convention, all message tracing tests start with `TestMsgTrace`.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 -run=TestMsgTrace ./server -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "srv_pkg_non_js_tests" ]; then
|
||||||
|
|
||||||
|
# Run all non JetStream tests in the server package. We exclude the
|
||||||
|
# store tests by using the `skip_store_tests` build tag, the JS tests
|
||||||
|
# by using `skip_js_tests`, MQTT tests by using `skip_mqtt_tests` and
|
||||||
|
# message tracing tests by using `skip_msgtrace_tests`.
|
||||||
|
# Ignore JWT and NRG tests here as they have their own matrix run.
|
||||||
|
|
||||||
|
# Also including the ldflag with the version since this includes the `TestVersionMatchesTag`.
|
||||||
|
go test $RACE -v -p=1 ./server/... -run="^Test(N[^R]|NR[^G]|J[^W]|JW[^T]|[^JN])" -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -tags=skip_store_tests,skip_js_tests,skip_mqtt_tests,skip_msgtrace_tests,skip_no_race_tests -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "jwt_tests" ]; then
|
||||||
|
|
||||||
|
# Run the JWT tests. All tests start with TestJWT.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 ./server/... -run="^TestJWT" -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
elif [ "$1" = "non_srv_pkg_tests" ]; then
|
||||||
|
|
||||||
|
# Run all tests of all non server package.
|
||||||
|
|
||||||
|
go test $RACE -v -p=1 $(go list ./... | grep -v "/server") -count=1 -vet=off -timeout=30m -failfast
|
||||||
|
|
||||||
|
fi
|
||||||
39
scripts/updateCopyrights.sh
Executable file
39
scripts/updateCopyrights.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ensure script is run at the root of a git repository
|
||||||
|
git rev-parse --is-inside-work-tree &>/dev/null || { echo "Not inside a git repository"; exit 1; }
|
||||||
|
|
||||||
|
# Find all .go files tracked by git
|
||||||
|
git ls-files "*.go" | while read -r file; do
|
||||||
|
# Skip files that don't have a copyright belonging to "The NATS Authors"
|
||||||
|
current_copyright=$(grep -oE "^// Copyright [0-9]{4}(-[0-9]{4})? The NATS Authors" "$file" || echo "")
|
||||||
|
[[ -z "$current_copyright" ]] && continue
|
||||||
|
|
||||||
|
# Get the last commit year for the file, ignore commit messages containing the word "copyright"
|
||||||
|
last_year=$(git log --follow --format="%ad" --date=format:%Y --grep="(C|c)opyright" --invert-grep -n 1 -- "$file")
|
||||||
|
existing_years=$(echo "$current_copyright" | grep -oE "[0-9]{4}(-[0-9]{4})?")
|
||||||
|
|
||||||
|
# Determine the new copyright range
|
||||||
|
if [[ "$existing_years" =~ ^([0-9]{4})-([0-9]{4})$ ]]; then
|
||||||
|
first_year=${BASH_REMATCH[1]}
|
||||||
|
new_copyright="// Copyright $first_year-$last_year The NATS Authors"
|
||||||
|
elif [[ "$existing_years" =~ ^([0-9]{4})$ ]]; then
|
||||||
|
first_year=${BASH_REMATCH[1]}
|
||||||
|
if [[ "$first_year" == "$last_year" ]]; then
|
||||||
|
new_copyright="// Copyright $first_year The NATS Authors"
|
||||||
|
else
|
||||||
|
new_copyright="// Copyright $first_year-$last_year The NATS Authors"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
continue # If the format is somehow incorrect, skip the file
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update the first line
|
||||||
|
if sed --version &>/dev/null; then
|
||||||
|
# Linux sed
|
||||||
|
sed -i "1s|^// Copyright.*|$new_copyright|" "$file"
|
||||||
|
else
|
||||||
|
# BSD/macOS sed, needs -i ''
|
||||||
|
sed -i '' "1s|^// Copyright.*|$new_copyright|" "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
595
server/README-MQTT.md
Normal file
595
server/README-MQTT.md
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
**MQTT Implementation Overview**
|
||||||
|
|
||||||
|
Revision 1.1
|
||||||
|
|
||||||
|
Authors: Ivan Kozlovic, Lev Brouk
|
||||||
|
|
||||||
|
NATS Server currently supports most of MQTT 3.1.1. This document describes how
|
||||||
|
it is implemented.
|
||||||
|
|
||||||
|
It is strongly recommended to review the [MQTT v3.1.1
|
||||||
|
specifications](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
|
||||||
|
and get a detailed understanding before proceeding with this document.
|
||||||
|
|
||||||
|
# Contents
|
||||||
|
|
||||||
|
1. [Concepts](#1-concepts)
|
||||||
|
- [Server, client](#server-client)
|
||||||
|
- [Connection, client ID, session](#connection-client-id-session)
|
||||||
|
- [Packets, messages, and subscriptions](#packets-messages-and-subscriptions)
|
||||||
|
- [Quality of Service (QoS), publish identifier (PI)](#quality-of-service-qos-publish-identifier-pi)
|
||||||
|
- [Retained message](#retained-message)
|
||||||
|
- [Will message](#will-message)
|
||||||
|
2. [Use of JetStream](#2-use-of-jetstream)
|
||||||
|
- [JetStream API](#jetstream-api)
|
||||||
|
- [Streams](#streams)
|
||||||
|
- [Consumers and Internal NATS Subscriptions](#consumers-and-internal-nats-subscriptions)
|
||||||
|
3. [Lifecycles](#3-lifecycles)
|
||||||
|
- [Connection, Session](#connection-session)
|
||||||
|
- [Subscription](#subscription)
|
||||||
|
- [Message](#message)
|
||||||
|
- [Retained messages](#retained-messages)
|
||||||
|
4. [Implementation Notes](#4-implementation-notes)
|
||||||
|
- [Hooking into NATS I/O](#hooking-into-nats-io)
|
||||||
|
- [Session Management](#session-management)
|
||||||
|
- [Processing QoS acks: PUBACK, PUBREC, PUBCOMP](#processing-qos-acks-puback-pubrec-pubcomp)
|
||||||
|
- [Subject Wildcards](#subject-wildcards)
|
||||||
|
5. [Known issues](#5-known-issues)
|
||||||
|
|
||||||
|
# 1. Concepts
|
||||||
|
|
||||||
|
## Server, client
|
||||||
|
|
||||||
|
In the MQTT specification there are concepts of **Client** and **Server**, used
|
||||||
|
somewhat interchangeably with those of **Sender** and **Receiver**. A **Server**
|
||||||
|
acts as a **Receiver** when it gets `PUBLISH` messages from a **Sender**
|
||||||
|
**Client**, and acts as a **Sender** when it delivers them to subscribed
|
||||||
|
**Clients**.
|
||||||
|
|
||||||
|
In the NATS server implementation there are also concepts (types) `server` and
|
||||||
|
`client`. `client` is an internal representation of a (connected) client and
|
||||||
|
runs its own read and write loops. Both of these have an `mqtt` field that if
|
||||||
|
set makes them behave as MQTT-compliant.
|
||||||
|
|
||||||
|
The code and comments may sometimes be confusing as they refer to `server` and
|
||||||
|
`client` sometimes ambiguously between MQTT and NATS.
|
||||||
|
|
||||||
|
## Connection, client ID, session
|
||||||
|
|
||||||
|
When an MQTT client connects to a server, it must send a `CONNECT` packet to
|
||||||
|
create an **MQTT Connection**. The packet must include a **Client Identifier**.
|
||||||
|
The server will then create or load a previously saved **Session** for the (hash
|
||||||
|
of) the client ID.
|
||||||
|
|
||||||
|
## Packets, messages, and subscriptions
|
||||||
|
|
||||||
|
The low level unit of transmission in MQTT is a **Packet**. Examples of packets
|
||||||
|
are: `CONNECT`, `SUBSCRIBE`, `SUBACK`, `PUBLISH`, `PUBCOMP`, etc.
|
||||||
|
|
||||||
|
An **MQTT Message** starts with a `PUBLISH` packet that a client sends to the
|
||||||
|
server. It is then matched against the current **MQTT Subscriptions** and is
|
||||||
|
delivered to them as appropriate. During the message delivery the server acts as
|
||||||
|
an MQTT client, and the receiver acts as an MQTT server.
|
||||||
|
|
||||||
|
Internally we use **NATS Messages** and **NATS Subscriptions** to facilitate
|
||||||
|
message delivery. This may be somewhat confusing as the code refers to `msg` and
|
||||||
|
`sub`. What may be even more confusing is that some MQTT packets (specifically,
|
||||||
|
`PUBREL`) are represented as NATS messages, and that the original MQTT packet
|
||||||
|
"metadata" may be encoded as NATS message headers.
|
||||||
|
|
||||||
|
## Quality of Service (QoS), publish identifier (PI)
|
||||||
|
|
||||||
|
MQTT specifies 3 levels of quality of service (**QoS**):
|
||||||
|
|
||||||
|
- `0` for at most once. A single delivery attempt.
|
||||||
|
- `1` for at least once. Will try to redeliver until acknowledged by the
|
||||||
|
receiver.
|
||||||
|
- `2` for exactly once. See the [SPEC REF] for the acknowledgement flow.
|
||||||
|
|
||||||
|
QoS 1 and 2 messages need to be identified with publish identifiers (**PI**s). A
|
||||||
|
PI is a 16-bit integer that must uniquely identify a message for the duration of
|
||||||
|
the required exchange of acknowledgment packets.
|
||||||
|
|
||||||
|
Note that the QoS applies separately to the transmission of a message from a
|
||||||
|
sender client to the server, and from the server to the receiving client. There
|
||||||
|
is no protocol-level acknowledgements between the receiver and the original
|
||||||
|
sender. The sender passes the ownership of messages to the server, and the
|
||||||
|
server then delivers them at maximum possible QoS to the receivers
|
||||||
|
(subscribers). The PIs for in-flight outgoing messages are issued and stored per
|
||||||
|
session.
|
||||||
|
|
||||||
|
## Retained message
|
||||||
|
|
||||||
|
A **Retained Message** is not part of any MQTT session and is not removed when the
|
||||||
|
session that produced it goes away. Instead, the server needs to persist a
|
||||||
|
_single_ retained message per topic. When a subscription is started, the server
|
||||||
|
needs to send the “matching” retained messages, that is, messages that would
|
||||||
|
have been delivered to the new subscription should that subscription had been
|
||||||
|
running prior to the publication of this message.
|
||||||
|
|
||||||
|
Retained messages are removed when the server receives a retained message with
|
||||||
|
an empty body. Still, this retained message that serves as a “delete” of a
|
||||||
|
retained message will be processed as a normal published message.
|
||||||
|
|
||||||
|
Retained messages can have QoS.
|
||||||
|
|
||||||
|
## Will message
|
||||||
|
|
||||||
|
The `CONNECT` packet can contain information about a **Will Message** that needs to
|
||||||
|
be sent to any client subscribing on the Will topic/subject in the event that
|
||||||
|
the client is disconnected implicitly, that is, not as a result as the client
|
||||||
|
sending the `DISCONNECT` packet.
|
||||||
|
|
||||||
|
Will messages can have the retain flag and QoS.
|
||||||
|
|
||||||
|
# 2. Use of JetStream
|
||||||
|
|
||||||
|
The MQTT implementation relies heavily on JetStream. We use it to:
|
||||||
|
|
||||||
|
- Persist (and restore) the [Session](#connection-client-id-session) state.
|
||||||
|
- Store and retrieve [Retained messages](#retained-message).
|
||||||
|
- Persist incoming [QoS 1 and
|
||||||
|
2](#quality-of-service-qos-publish-identifier-pi) messages, and
|
||||||
|
re-deliver if needed.
|
||||||
|
- Store and de-duplicate incoming [QoS
|
||||||
|
2](#quality-of-service-qos-publish-identifier-pi) messages.
|
||||||
|
- Persist and re-deliver outgoing [QoS
|
||||||
|
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL` packets.
|
||||||
|
|
||||||
|
Here is the overview of how we set up and use JetStream **streams**,
|
||||||
|
**consumers**, and **internal NATS subscriptions**.
|
||||||
|
|
||||||
|
## JetStream API
|
||||||
|
|
||||||
|
All interactions with JetStream are performed via `mqttJSA` that sends NATS
|
||||||
|
requests to JetStream. Most are processed synchronously and await a response,
|
||||||
|
some (e.g. `jsa.sendAck()`) are sent asynchronously. JetStream API is usually
|
||||||
|
referred to as `jsa` in the code. No special locking is required to use `jsa`,
|
||||||
|
however the asynchronous use of JetStream may create race conditions with
|
||||||
|
delivery callbacks.
|
||||||
|
|
||||||
|
## Streams
|
||||||
|
|
||||||
|
We create the following streams unless they already exist. Failing to ensure the
|
||||||
|
streams would prevent the client from connecting.
|
||||||
|
|
||||||
|
Each stream is created with a replica value that is determined by the size of
|
||||||
|
the cluster but limited to 3. It can also be overwritten by the stream_replicas
|
||||||
|
option in the MQTT configuration block.
|
||||||
|
|
||||||
|
The streams are created the first time an Account Session Manager is initialized
|
||||||
|
and are used by all sessions in it. Note that to avoid race conditions, some
|
||||||
|
subscriptions are created first. The streams are never deleted. See
|
||||||
|
`mqttCreateAccountSessionManager()` for details.
|
||||||
|
|
||||||
|
1. `$MQTT_sess` stores persisted **Session** records. It filters on
|
||||||
|
`"$MQTT.sess.>` subject and has a “limits” policy with `MaxMsgsPer` setting
|
||||||
|
of 1.
|
||||||
|
2. `$MQTT_msgs` is used for **QoS 1 and 2 message delivery**.
|
||||||
|
It filters on `$MQTT.msgs.>` subject and has an “interest” policy.
|
||||||
|
3. `$MQTT_rmsgs` stores **Retained Messages**. They are all
|
||||||
|
stored (and filtered) on a single subject `$MQTT.rmsg`. This stream has a
|
||||||
|
limits policy.
|
||||||
|
4. `$MQTT_qos2in` stores and deduplicates **Incoming QoS 2 Messages**. It
|
||||||
|
filters on `$MQTT.qos2.in.>` and has a "limits" policy with `MaxMsgsPer` of
|
||||||
|
1.
|
||||||
|
5. `$MQTT_out` stores **Outgoing QoS 2** `PUBREL` packets. It filters on
|
||||||
|
`$MQTT.out.>` and has a "interest" retention policy.
|
||||||
|
|
||||||
|
## Consumers and Internal NATS Subscriptions
|
||||||
|
|
||||||
|
### Account Scope
|
||||||
|
|
||||||
|
- A durable consumer for [Retained Messages](#retained-message) -
|
||||||
|
`$MQTT_rmsgs_<server name hash>`
|
||||||
|
- A subscription to handle all [jsa](#jetstream-api) replies for the account.
|
||||||
|
- A subscription to replies to "session persist" requests, so that we can detect
|
||||||
|
the use of a session with the same client ID anywhere in the cluster.
|
||||||
|
- 2 subscriptions to support [retained messages](#retained-message):
|
||||||
|
`$MQTT.sub.<nuid>` for the messages themselves, and one to receive replies to
|
||||||
|
"delete retained message" JS API (on the JS reply subject var).
|
||||||
|
|
||||||
|
### Session Scope
|
||||||
|
|
||||||
|
When a new QoS 2 MQTT subscription is detected in a session, we ensure that
|
||||||
|
there is a durable consumer for [QoS
|
||||||
|
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL`s out for delivery -
|
||||||
|
`$MQTT_PUBREL_<session id hash>`
|
||||||
|
|
||||||
|
### Subscription Scope
|
||||||
|
|
||||||
|
For all MQTT subscriptions, regardless of their QoS, we create internal NATS subscriptions to
|
||||||
|
|
||||||
|
- `subject` (directly encoded from `topic`). This subscription is used to
|
||||||
|
deliver QoS 0 messages, and messages originating from NATS.
|
||||||
|
- if needed, `subject fwc` complements `subject` for topics like `topic.#` to
|
||||||
|
include `topic` itself, see [top-level wildcards](#subject-wildcards)
|
||||||
|
|
||||||
|
For QoS 1 or 2 MQTT subscriptions we ensure:
|
||||||
|
|
||||||
|
- A durable consumer for messages out for delivery - `<session ID hash>_<nuid>`
|
||||||
|
- An internal subscription to `$MQTT.sub.<nuid>` to deliver the messages to the
|
||||||
|
receiving client.
|
||||||
|
|
||||||
|
### (Old) Notes
|
||||||
|
|
||||||
|
As indicated before, for a QoS1 or QoS2 subscription, the server will create a
|
||||||
|
JetStream consumer with the appropriate subject filter. If the subscription
|
||||||
|
already existed, then only the NATS subscription is created for the JetStream
|
||||||
|
consumer’s delivery subject.
|
||||||
|
|
||||||
|
Note that JS consumers can be created with an “Replicas” override, which from
|
||||||
|
recent discussion is problematic with “Interest” policy streams, which
|
||||||
|
“$MQTT_msgs” is.
|
||||||
|
|
||||||
|
We do handle situations where a subscription on the same subject filter is sent
|
||||||
|
with a different QoS as per MQTT specifications. If the existing was on QoS 1 or
|
||||||
|
2, and the “new” is for QoS 0, then we delete the existing JS consumer.
|
||||||
|
|
||||||
|
Subscriptions that are QoS 0 have a NATS subscription with the callback function
|
||||||
|
being `mqttDeliverMsgCbQos0()`; while QoS 1 and 2 have a NATS subscription with
|
||||||
|
callback `mqttDeliverMsgCbQos12()`. Both those functions have comments that
|
||||||
|
describe the reason for their existence and what they are doing. For instance
|
||||||
|
the `mqttDeliverMsgCbQos0()` callback will reject any producing client that is
|
||||||
|
of type JETSTREAM, so that it handles only non JetStream (QoS 1 and 2) messages.
|
||||||
|
|
||||||
|
Both these functions end-up calling mqttDeliver() which will first enqueue the
|
||||||
|
possible retained messages buffer before delivering any new message. The message
|
||||||
|
itself being delivered is serialized in MQTT format and enqueued to the client’s
|
||||||
|
outbound buffer and call to addToPCD is made so that it is flushed out of the
|
||||||
|
readloop.
|
||||||
|
|
||||||
|
# 3. Lifecycles
|
||||||
|
|
||||||
|
## Connection, Session
|
||||||
|
|
||||||
|
An MQTT connection is created when a listening MQTT server receives a `CONNECT`
|
||||||
|
packet. See `mqttProcessConnect()`. A connection is associated with a session.
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Ensure that we have an `AccountSessionManager` so we can have an
|
||||||
|
`mqttSession`. Lazily initialize JetStream streams, and internal consumers
|
||||||
|
and subscriptions. See `getOrCreateMQTTAccountSessionManager()`.
|
||||||
|
2. Find and disconnect any previous session/client for the same ID. See
|
||||||
|
`mqttProcessConnect()`.
|
||||||
|
3. Ensure we have an `mqttSession` - create a new or load a previously persisted
|
||||||
|
one. If the clean flag is set in `CONNECT`, clean the session. see
|
||||||
|
`mqttSession.clear()`
|
||||||
|
4. Initialize session's subscriptions, if any.
|
||||||
|
5. Always send back a `CONNACK` packet. If there were errors in previous steps,
|
||||||
|
include the error.
|
||||||
|
|
||||||
|
An MQTT connection can be closed for a number of reasons, including receiving a
|
||||||
|
`DISCONNECT` from the client, explicit internal errors processing MQTT packets,
|
||||||
|
or the server receiving another `CONNECT` packet with the same client ID. See
|
||||||
|
`mqttHandleClosedClient()` and `mqttHandleWill()`. Steps:
|
||||||
|
|
||||||
|
1. Send out the Will Message if applicable (if not caused by a `DISCONNECT` packet)
|
||||||
|
2. Delete the JetStream consumers for to QoS 1 and 2 packet delivery through
|
||||||
|
JS API calls (if "clean" session flag is set)
|
||||||
|
3. Delete the session record from the “$MQTT_sess” stream, based on recorded
|
||||||
|
stream sequence. (if "clean" session flag is set)
|
||||||
|
4. Close the client connection.
|
||||||
|
|
||||||
|
On an explicit disconnect, that is, the client sends the DISCONNECT packet, the
|
||||||
|
server will NOT send the Will, as per specifications.
|
||||||
|
|
||||||
|
For sessions that had the “clean” flag, the JS consumers corresponding to QoS 1
|
||||||
|
subscriptions are deleted through JS API calls, the session record is then
|
||||||
|
deleted (based on recorded stream sequence) from the “$MQTT_sess” stream.
|
||||||
|
|
||||||
|
Finally, the client connection is closed
|
||||||
|
|
||||||
|
Sessions are persisted on disconnect, and on subscriptions changes.
|
||||||
|
|
||||||
|
## Subscription
|
||||||
|
|
||||||
|
Receiving an MQTT `SUBSCRIBE` packet creates new subscriptions, or updates
|
||||||
|
existing subscriptions in a session. Each `SUBSCRIBE` packet may contain several
|
||||||
|
specific subscriptions (`topic` + QoS in each). We always respond with a
|
||||||
|
`SUBACK`, which may indicate which subscriptions errored out.
|
||||||
|
|
||||||
|
For each subscription in the packet, we:
|
||||||
|
|
||||||
|
1. Ignore it if `topic` starts with `$MQTT.sub.`.
|
||||||
|
2. Set up QoS 0 message delivery - an internal NATS subscription on `topic`.
|
||||||
|
3. Replay any retained messages for `topic`, once as QoS 0.
|
||||||
|
4. If we already have a subscription on `topic`, update its QoS
|
||||||
|
5. If this is a QoS 2 subscription in the session, ensure we have the [PUBREL
|
||||||
|
consumer](#session-scope) for the session.
|
||||||
|
6. If this is a QoS 1 or 2 subscription, ensure we have the [Message
|
||||||
|
consumer](#subscription-scope) for this subscription (or delete one if it
|
||||||
|
exists and this is now a QoS 0 sub).
|
||||||
|
7. Add an extra subscription for the [top-level wildcard](#subject-wildcards) case.
|
||||||
|
8. Update the session, persist it if changed.
|
||||||
|
|
||||||
|
When a session is restored (no clean flag), we go through the same steps to
|
||||||
|
re-subscribe to its stored subscription, except step #8 which would have been
|
||||||
|
redundant.
|
||||||
|
|
||||||
|
When we get an `UNSUBSCRIBE` packet, it can contain multiple subscriptions to
|
||||||
|
unsubscribe. The parsing will generate a slice of mqttFilter objects that
|
||||||
|
contain the “filter” (the topic with possibly wildcard of the subscription) and
|
||||||
|
the QoS value. The server goes through the list and deletes the JS consumer (if
|
||||||
|
QoS 1 or 2) and unsubscribes the NATS subscription for the delivery subject (if
|
||||||
|
it was a QoS 1 or 2) or on the actual topic/subject. In case of the “#”
|
||||||
|
wildcard, the server will handle the “level up” subscriptions that NATS had to
|
||||||
|
create.
|
||||||
|
|
||||||
|
Again, we update the session and persist it as needed in the `$MQTT_sess`
|
||||||
|
stream.
|
||||||
|
|
||||||
|
## Message
|
||||||
|
|
||||||
|
1. Detect an incoming PUBLISH packet, parse and check the message QoS. Fill out
|
||||||
|
the session's `mqttPublish` struct that contains information about the
|
||||||
|
published message. (see `mqttParse()`, `mqttParsePub()`)
|
||||||
|
2. Process the message according to its QoS (see `mqttProcessPub()`)
|
||||||
|
|
||||||
|
- QoS 0:
|
||||||
|
- Initiate message delivery
|
||||||
|
- QoS 1:
|
||||||
|
- Initiate message delivery
|
||||||
|
- Send back a `PUBACK`
|
||||||
|
- QoS 2:
|
||||||
|
- Store the message in `$MQTT_qos2in` stream, using a PI-specific subject.
|
||||||
|
Since `MaxMsgsPer` is set to 1, we will ignore duplicates on the PI.
|
||||||
|
- Send back a `PUBREC`
|
||||||
|
- "Wait" for a `PUBREL`, then initiate message delivery
|
||||||
|
- Remove the previously stored QoS2 message
|
||||||
|
- Send back a `PUBCOMP`
|
||||||
|
|
||||||
|
3. Initiate message delivery (see `mqttInitiateMsgDelivery()`)
|
||||||
|
|
||||||
|
- Convert the MQTT `topic` into a NATS `subject` using
|
||||||
|
`mqttTopicToNATSPubSubject()` function. If there is a known subject
|
||||||
|
mapping, then we select the new subject using `selectMappedSubject()`
|
||||||
|
function and then convert back this subject into an MQTT topic using
|
||||||
|
`natsSubjectToMQTTTopic()` function.
|
||||||
|
- Re-serialize the `PUBLISH` packet received as a NATS message. Use NATS
|
||||||
|
headers for the metadata, and the deliverable MQTT `PUBLISH` packet as the
|
||||||
|
contents.
|
||||||
|
- Publish the messages as `subject` (and `subject fwc` if applicable, see
|
||||||
|
[subject wildcards](#subject-wildcards)). Use the "standard" NATS
|
||||||
|
`c.processInboundClientMsg()` to do that. `processInboundClientMsg()` will
|
||||||
|
distribute the message to any NATS subscriptions (including routes,
|
||||||
|
gateways, leafnodes) and the relevant MQTT subscriptions.
|
||||||
|
- Check for retained messages, process as needed. See
|
||||||
|
`c.processInboundClientMsg()` calling `c.mqttHandlePubRetain()` For MQTT
|
||||||
|
clients.
|
||||||
|
- If the message QoS is 1 or 2, store it in `$MQTT_msgs` stream as
|
||||||
|
`$MQTT.msgs.<subject>` for "at least once" delivery with retries.
|
||||||
|
|
||||||
|
4. Let NATS and JetStream deliver to the internal subscriptions, and to the
|
||||||
|
receiving clients. See `mqttDeliverMsgCb...()`
|
||||||
|
|
||||||
|
- The NATS message posted to `subject` (and `subject fwc`) will be delivered
|
||||||
|
to each relevant internal subscription by calling `mqttDeliverMsgCbQoS0()`.
|
||||||
|
The function has access to both the publishing and the receiving clients.
|
||||||
|
|
||||||
|
- Ignore all irrelevant invocations. Specifically, do nothing if the
|
||||||
|
message needs to be delivered with a higher QoS - that will be handled by
|
||||||
|
the other, `...QoS12` callback. Note that if the original message was
|
||||||
|
publuished with a QoS 1 or 2, but the subscription has its maximum QoS
|
||||||
|
set to 0, the message will be delivered by this callback.
|
||||||
|
- Ignore "reserved" subscriptions, as per MQTT spec.
|
||||||
|
- Decode delivery `topic` from the NATS `subject`.
|
||||||
|
- Write (enqueue) outgoing `PUBLISH` packet.
|
||||||
|
- **DONE for QoS 0**
|
||||||
|
|
||||||
|
- The NATS message posted to JetStream as `$MQTT.msgs.subject` will be
|
||||||
|
consumed by subscription-specific consumers. Note that MQTT subscriptions
|
||||||
|
with max QoS 0 do not have JetStream consumers. They are handled by the
|
||||||
|
QoS0 callback.
|
||||||
|
|
||||||
|
The consumers will deliver it to the `$MQTT.sub.<nuid>`
|
||||||
|
subject for their respective NATS subscriptions by calling
|
||||||
|
`mqttDeliverMsgCbQoS12()`. This callback too has access to both the
|
||||||
|
publishing and the receiving clients.
|
||||||
|
|
||||||
|
- Ignore "reserved" subscriptions, as per MQTT spec.
|
||||||
|
- See if this is a re-delivery from JetStream by checking `sess.cpending`
|
||||||
|
for the JS reply subject. If so, use the existing PI and treat this as a
|
||||||
|
duplicate redelivery.
|
||||||
|
- Otherwise, assign the message a new PI (see `trackPublish()` and
|
||||||
|
`bumpPI()`) and store it in `sess.cpending` and `sess.pendingPublish`,
|
||||||
|
along with the JS reply subject that can be used to remove this pending
|
||||||
|
message from the consumer once it's delivered to the receipient.
|
||||||
|
- Decode delivery `topic` from the NATS `subject`.
|
||||||
|
- Write (enqueue) outgoing `PUBLISH` packet.
|
||||||
|
|
||||||
|
5. QoS 1: "Wait" for a `PUBACK`. See `mqttProcessPubAck()`.
|
||||||
|
|
||||||
|
- When received, remove the PI from the tracking maps, send an ACK to
|
||||||
|
consumer to remove the message.
|
||||||
|
- **DONE for QoS 1**
|
||||||
|
|
||||||
|
6. QoS 2: "Wait" for a `PUBREC`. When received, we need to do all the same
|
||||||
|
things as in the QoS 1 `PUBACK` case, but we need to send out a `PUBREL`, and
|
||||||
|
continue using the same PI until the delivery flow is complete and we get
|
||||||
|
back a `PUBCOMP`. For that, we add the PI to `sess.pendingPubRel`, and to
|
||||||
|
`sess.cpending` with the PubRel consumer durable name.
|
||||||
|
|
||||||
|
We also compose and store a headers-only NATS message signifying a `PUBREL`
|
||||||
|
out for delivery, and store it in the `$MQTT_qos2out` stream, as
|
||||||
|
`$MQTT.qos2.out.<session-id>`.
|
||||||
|
|
||||||
|
7. QoS 2: Deliver `PUBREL`. The PubRel session-specific consumer will publish to
|
||||||
|
internal subscription on `$MQTT.qos2.delivery`, calling
|
||||||
|
`mqttDeliverPubRelCb()`. We store the ACK reply subject in `cpending` to
|
||||||
|
remove the JS message on `PUBCOMP`, compose and send out a `PUBREL` packet.
|
||||||
|
|
||||||
|
8. QoS 2: "Wait" for a `PUBCOMP`. See `mqttProcessPubComp()`.
|
||||||
|
- When received, remove the PI from the tracking maps, send an ACK to
|
||||||
|
consumer to remove the `PUBREL` message.
|
||||||
|
- **DONE for QoS 2**
|
||||||
|
|
||||||
|
## Retained messages
|
||||||
|
|
||||||
|
When we process an inbound `PUBLISH` and submit it to
|
||||||
|
`processInboundClientMsg()` function, for MQTT clients it will invoke
|
||||||
|
`mqttHandlePubRetain()` which checks if the published message is “retained” or
|
||||||
|
not.
|
||||||
|
|
||||||
|
If it is, then we construct a record representing the retained message and store
|
||||||
|
it in the `$MQTT_rmsg` stream, under the single `$MQTT.rmsg` subject. The stored
|
||||||
|
record (in JSON) contains information about the subject, topic, MQTT flags, user
|
||||||
|
that produced this message and the message content itself. It is stored and the
|
||||||
|
stream sequence is remembered in the memory structure that contains retained
|
||||||
|
messages.
|
||||||
|
|
||||||
|
Note that when creating an account session manager, the retained messages stream
|
||||||
|
is read from scratch to load all the messages through the use of a JS consumer.
|
||||||
|
The associated subscription will process the recovered retained messages or any
|
||||||
|
new that comes from the network.
|
||||||
|
|
||||||
|
A retained message is added to a map and a subscription is created and inserted
|
||||||
|
into a sublist that will be used to perform a ReverseMatch() when a subscription
|
||||||
|
is started and we want to find all retained messages that the subscription would
|
||||||
|
have received if it had been running prior to the message being published.
|
||||||
|
|
||||||
|
If a retained message on topic “foo” already exists, then the server has to
|
||||||
|
delete the old message at the stream sequence we saved when storing it.
|
||||||
|
|
||||||
|
This could have been done with having retained messages stored under
|
||||||
|
`$MQTT.rmsg.<subject>` as opposed to all under a single subject, and make use of
|
||||||
|
the `MaxMsgsPer` field set to 1. The `MaxMsgsPer` option was introduced well into
|
||||||
|
the availability of MQTT and changes to the sessions was made in [PR
|
||||||
|
#2501](https://github.com/nats-io/nats-server/pull/2501), with a conversion of
|
||||||
|
existing streams such as `$MQTT*sess*<sess ID>` into a single stream with unique
|
||||||
|
subjects, but the changes were not made to the retained messages stream.
|
||||||
|
|
||||||
|
There are also subscriptions for the handling of retained messages which are
|
||||||
|
messages that are asked by the publisher to be retained by the MQTT server to be
|
||||||
|
delivered to matching subscriptions when they start. There is a single message
|
||||||
|
per topic. Retained messages are deleted when the user sends a retained message
|
||||||
|
(there is a flag in the PUBLISH protocol) on a given topic with an empty body.
|
||||||
|
The difficulty with retained messages is to handle them in a cluster since all
|
||||||
|
servers need to be aware of their presence so that they can deliver them to
|
||||||
|
subscriptions that those servers may become the leader for.
|
||||||
|
|
||||||
|
- `$MQTT_rmsgs` which has a “limits” policy and holds retained messages, all
|
||||||
|
under `$MQTT.rmsg` single subject. Not sure why I did not use MaxMsgsPer for
|
||||||
|
this stream and not filter `$MQTT.rmsg.>`.
|
||||||
|
|
||||||
|
The first step when processing a new subscription is to gather the retained
|
||||||
|
messages that would be a match for this subscription. To do so, the server will
|
||||||
|
serialize into a buffer all messages for the account session manager’s sublist’s
|
||||||
|
ReverseMatch result. We use the returned subscriptions’ subject to find from a
|
||||||
|
map appropriate retained message (see `serializeRetainedMsgsForSub()` for
|
||||||
|
details).
|
||||||
|
|
||||||
|
# 4. Implementation Notes
|
||||||
|
|
||||||
|
## Hooking into NATS I/O
|
||||||
|
|
||||||
|
### Starting the accept loop
|
||||||
|
|
||||||
|
The MQTT accept loop is started when the server detects that an MQTT port has
|
||||||
|
been defined in the configuration file. It works similarly to all other accept
|
||||||
|
loops. Note that for MQTT over websocket, the websocket port has to be defined
|
||||||
|
and MQTT clients will connect to that port instead of the MQTT port and need to
|
||||||
|
provide `/mqtt` as part of the URL to redirect the creation of the client to an
|
||||||
|
MQTT client (with websocket support) instead of a regular NATS with websocket.
|
||||||
|
See the branching done in `startWebsocketServer()`. See `startMQTT()`.
|
||||||
|
|
||||||
|
### Starting the read/write loops
|
||||||
|
|
||||||
|
When a TCP connection is accepted, the internal go routine will invoke
|
||||||
|
`createMQTTClient()`. This function will set a `c.mqtt` object that will make it
|
||||||
|
become an MQTT client (through the `isMqtt()` helper function). The `readLoop()`
|
||||||
|
and `writeLoop()` are started similarly to other clients. However, the read loop
|
||||||
|
will branch out to `mqttParse()` instead when detecting that this is an MQTT
|
||||||
|
client.
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
### Account Session Manager
|
||||||
|
|
||||||
|
`mqttAccountSessionManager` is an object that holds the state of all sessions in
|
||||||
|
an account. It also manages the lifecycle of JetStream streams and internal
|
||||||
|
subscriptions for processing JS API replies, session updates, etc. See
|
||||||
|
`mqttCreateAccountSessionManager()`. It is lazily initialized upon the first
|
||||||
|
MQTT `CONNECT` packet received. Account session manager is referred to as `asm`
|
||||||
|
in the code.
|
||||||
|
|
||||||
|
Note that creating the account session manager (and attempting to create the
|
||||||
|
streams) is done only once per account on a given server, since once created the
|
||||||
|
account session manager for a given account would be found in the sessions map
|
||||||
|
of the mqttSessionManager object.
|
||||||
|
|
||||||
|
### Find and disconnect previous session/client
|
||||||
|
|
||||||
|
Once all that is done, we now go to the creation of the session object itself.
|
||||||
|
For that, we first need to make sure that it does not already exist, meaning
|
||||||
|
that it is registered on the server - or anywhere in the cluster. Note that MQTT
|
||||||
|
dictates that if a session with the same ID connects, the OLD session needs to
|
||||||
|
be closed, not the new one being created. NATS Server complies with this
|
||||||
|
requirement.
|
||||||
|
|
||||||
|
Once a session is detected to already exists, the old one (as described above)
|
||||||
|
is closed and the new one accepted, however, the session ID is maintained in a
|
||||||
|
flappers map so that we detect situations where sessions with the same ID are
|
||||||
|
started multiple times causing the previous one to be closed. When that
|
||||||
|
detection occurs, the newly created session is put in “jail” for a second to
|
||||||
|
avoid a very rapid succession of connect/disconnect. This has already been seen
|
||||||
|
by users since there was some issue there where we would schedule the connection
|
||||||
|
closed instead of waiting in place which was causing a panic.
|
||||||
|
|
||||||
|
We also protect from multiple clients on a given server trying to connect with
|
||||||
|
the same ID at the “same time” while the processing of a CONNECT of a session is
|
||||||
|
not yet finished. This is done with the use of a sessLocked map, keyed by the
|
||||||
|
session ID.
|
||||||
|
|
||||||
|
### Create or restore the session
|
||||||
|
|
||||||
|
If everything is good up to that point, the server will either create or restore
|
||||||
|
a session from the stream. This is done in the `createOrRestoreSession()`
|
||||||
|
function. The client/session ID is hashed and added to the session’s stream
|
||||||
|
subject along with the JS domain to prevent clients connecting from different
|
||||||
|
domains to “pollute” the session stream of a given domain.
|
||||||
|
|
||||||
|
Since each session constitutes a subject and the stream has a maximum of 1
|
||||||
|
message per subject, we attempt to load the last message on the formed subject.
|
||||||
|
If we don’t find it, then the session object is created “empty”, while if we
|
||||||
|
find a record, we create the session object based on the record persisted on the
|
||||||
|
stream.
|
||||||
|
|
||||||
|
If the session was restored from the JS stream, we keep track of the stream
|
||||||
|
sequence where the record was located. When we save the session (even if it
|
||||||
|
already exists) we will use this sequence number to set the
|
||||||
|
`JSExpectedLastSubjSeq` header so that we handle possibly different servers in a
|
||||||
|
(super)cluster to detect the race of clients trying to use the same session ID,
|
||||||
|
since only one of the write should succeed. On success, the session’s new
|
||||||
|
sequence is remembered by the server that did the write.
|
||||||
|
|
||||||
|
When created or restored, the CONNACK can now be sent back to the client, and if
|
||||||
|
there were any recovered subscriptions, they are now processed.
|
||||||
|
|
||||||
|
## Processing QoS acks: PUBACK, PUBREC, PUBCOMP
|
||||||
|
|
||||||
|
When the server delivers a message with QoS 1 or 2 (also a `PUBREL` for QoS 2) to a subscribed client, the client will send back an acknowledgement. See `mqttProcessPubAck()`, `mqttProcessPubRec()`, and `mqttProcessPubComp()`
|
||||||
|
|
||||||
|
While the specific logic for each packet differs, these handlers all update the
|
||||||
|
session's PI mappings (`cpending`, `pendingPublish`, `pendingPubRel`), and if
|
||||||
|
needed send an ACK to JetStream to remove the message from its consumer and stop
|
||||||
|
the re-delivery attempts.
|
||||||
|
|
||||||
|
## Subject Wildcards
|
||||||
|
|
||||||
|
Note that MQTT subscriptions have wildcards too, the `“+”` wildcard is equivalent
|
||||||
|
to NATS’s `“*”` wildcard, however, MQTT’s wildcard `“#”` is similar to `“>”`, except
|
||||||
|
that it also includes the level above. That is, a subscription on `“foo/#”` would
|
||||||
|
receive messages on `“foo/bar/baz”`, but also on `“foo”`.
|
||||||
|
|
||||||
|
So, for MQTT subscriptions enging with a `'#'` we are forced to create 2
|
||||||
|
internal NATS subscriptions, one on `“foo”` and one on `“foo.>”`.
|
||||||
|
|
||||||
|
# 5. Known issues
|
||||||
|
- "active" redelivery for QoS from JetStream (compliant, just a note)
|
||||||
|
- JetStream QoS redelivery happens out of (original) order
|
||||||
|
- finish delivery of in-flight messages after UNSUB
|
||||||
|
- finish delivery of in-flight messages after a reconnect
|
||||||
|
- consider replacing `$MQTT_msgs` with `$MQTT_out`.
|
||||||
|
- consider using unique `$MQTT.rmsg.>` and `MaxMsgsPer` for retained messages.
|
||||||
|
- add a cli command to list/clean old sessions
|
||||||
17
server/README.md
Normal file
17
server/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
Tests that run on Travis have been split into jobs that run in their own VM in parallel. This reduces the overall running time but also is allowing recycling of a job when we get a flapper as opposed to have to recycle the whole test suite.
|
||||||
|
|
||||||
|
## JetStream Tests
|
||||||
|
|
||||||
|
For JetStream tests, we need to observe a naming convention so that no tests are omitted when running on Travis.
|
||||||
|
|
||||||
|
The script `runTestsOnTravis.sh` will run a given job based on the definition found in "`.travis.yml`".
|
||||||
|
|
||||||
|
As for the naming convention:
|
||||||
|
|
||||||
|
- All JetStream test name should start with `TestJetStream`
|
||||||
|
- Cluster tests should go into `jetstream_cluster_test.go` and start with `TestJetStreamCluster`
|
||||||
|
- Super-cluster tests should go into `jetstream_super_cluster_test.go` and start with `TestJetStreamSuperCluster`
|
||||||
|
|
||||||
|
Not following this convention means that some tests may not be executed on Travis.
|
||||||
356
server/archive/archive.go
Normal file
356
server/archive/archive.go
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// Copyright 2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MagicBytes = "NATSARC1"
|
||||||
|
|
||||||
|
// maxNameLen bounds the entry name length accepted when reading an archive,
|
||||||
|
// avoiding an unbounded allocation on corrupt or malicious input. The name is
|
||||||
|
// the only variable-length field read directly from the stream.
|
||||||
|
const maxNameLen = 1 << 20
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrClosed = errors.New("archive: closed")
|
||||||
|
ErrInvalidArchive = errors.New("archive: invalid archive stream")
|
||||||
|
ErrIncompleteEntry = errors.New("archive: entry not fully written")
|
||||||
|
ErrNoActiveEntry = errors.New("archive: no active entry")
|
||||||
|
ErrWriteTooLong = errors.New("archive: write exceeds declared entry size")
|
||||||
|
ErrNilHeader = errors.New("archive: nil header")
|
||||||
|
ErrNegativeEntrySize = errors.New("archive: negative entry size")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header describes one archive entry.
|
||||||
|
//
|
||||||
|
// On the wire each entry is the encoded header fields followed by the payload.
|
||||||
|
// HeaderSize and PayloadSize describe how that payload is split (e.g. message
|
||||||
|
// headers vs. body); the payload length is their sum and is not stored
|
||||||
|
// separately. Sequence is always encoded, with 0 meaning "unset".
|
||||||
|
type Header struct {
|
||||||
|
Name string
|
||||||
|
HeaderSize int64
|
||||||
|
PayloadSize int64
|
||||||
|
Timestamp int64
|
||||||
|
Sequence uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Header) clone() *Header {
|
||||||
|
if h == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := *h
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer writes a stream of archive entries.
|
||||||
|
type Writer struct {
|
||||||
|
w io.Writer
|
||||||
|
wroteMagic bool
|
||||||
|
header *Header
|
||||||
|
remaining int64
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter returns a new stream writer.
|
||||||
|
func NewWriter(w io.Writer) *Writer {
|
||||||
|
return &Writer{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader starts a new archive entry.
|
||||||
|
func (a *Writer) WriteHeader(hdr *Header) error {
|
||||||
|
if a.closed {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if hdr == nil {
|
||||||
|
return ErrNilHeader
|
||||||
|
}
|
||||||
|
if hdr.HeaderSize < 0 || hdr.PayloadSize < 0 {
|
||||||
|
return ErrNegativeEntrySize
|
||||||
|
}
|
||||||
|
if a.header != nil && a.remaining > 0 {
|
||||||
|
return ErrIncompleteEntry
|
||||||
|
}
|
||||||
|
totalPayloadSize := hdr.HeaderSize + hdr.PayloadSize
|
||||||
|
if totalPayloadSize < 0 {
|
||||||
|
return ErrNegativeEntrySize
|
||||||
|
}
|
||||||
|
if !a.wroteMagic {
|
||||||
|
if _, err := io.WriteString(a.w, MagicBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.wroteMagic = true
|
||||||
|
}
|
||||||
|
if err := a.writeUvarint(uint64(len(hdr.Name))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.writeVarint(hdr.Timestamp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.writeUvarint(hdr.Sequence); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.writeUvarint(uint64(hdr.HeaderSize)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.writeUvarint(uint64(hdr.PayloadSize)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(a.w, hdr.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.header = hdr.clone()
|
||||||
|
a.remaining = totalPayloadSize
|
||||||
|
if a.remaining == 0 {
|
||||||
|
a.header = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes payload bytes for the current entry.
|
||||||
|
func (a *Writer) Write(p []byte) (int, error) {
|
||||||
|
if a.closed {
|
||||||
|
return 0, ErrClosed
|
||||||
|
}
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if a.header == nil {
|
||||||
|
return 0, ErrNoActiveEntry
|
||||||
|
}
|
||||||
|
wanted := len(p)
|
||||||
|
if int64(wanted) > a.remaining {
|
||||||
|
wanted = int(a.remaining)
|
||||||
|
}
|
||||||
|
n, err := a.w.Write(p[:wanted])
|
||||||
|
a.remaining -= int64(n)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if n < wanted {
|
||||||
|
return n, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
if len(p) > wanted {
|
||||||
|
if a.remaining == 0 {
|
||||||
|
a.header = nil
|
||||||
|
}
|
||||||
|
return n, ErrWriteTooLong
|
||||||
|
}
|
||||||
|
if a.remaining == 0 {
|
||||||
|
a.header = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finalizes the stream writer.
|
||||||
|
func (a *Writer) Close() error {
|
||||||
|
if a.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a.closed = true
|
||||||
|
if a.header != nil && a.remaining > 0 {
|
||||||
|
return ErrIncompleteEntry
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush flushes the underlying writer if it supports flushing.
|
||||||
|
func (a *Writer) Flush() error {
|
||||||
|
if a.closed {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if f, ok := a.w.(interface{ Flush() error }); ok {
|
||||||
|
return f.Flush()
|
||||||
|
}
|
||||||
|
if f, ok := a.w.(interface{ Flush() }); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader reads a stream of archive entries.
|
||||||
|
type Reader struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
header *Header
|
||||||
|
remaining int64
|
||||||
|
seenMagic bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader returns a new stream reader.
|
||||||
|
func NewReader(r io.Reader) *Reader {
|
||||||
|
return &Reader{r: bufio.NewReader(r)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Reader) discardCurrent() error {
|
||||||
|
if u.remaining == 0 {
|
||||||
|
u.header = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := io.CopyN(io.Discard, u.r, u.remaining)
|
||||||
|
u.remaining = 0
|
||||||
|
u.header = nil
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
return ErrInvalidArchive
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next advances to the next entry and returns its header.
|
||||||
|
func (u *Reader) Next() (*Header, error) {
|
||||||
|
if u.err != nil {
|
||||||
|
return nil, u.err
|
||||||
|
}
|
||||||
|
if err := u.discardCurrent(); err != nil {
|
||||||
|
u.err = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !u.seenMagic {
|
||||||
|
magic := make([]byte, len(MagicBytes))
|
||||||
|
if _, err := io.ReadFull(u.r, magic); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
u.err = io.EOF
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
u.err = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if string(magic) != MagicBytes {
|
||||||
|
u.err = ErrInvalidArchive
|
||||||
|
return nil, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
u.seenMagic = true
|
||||||
|
}
|
||||||
|
// A clean io.EOF on the first field is the normal end of the stream.
|
||||||
|
nameLen, err := u.readUvarint()
|
||||||
|
if err != nil {
|
||||||
|
u.err = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
timestamp, err := u.readVarint()
|
||||||
|
if err != nil {
|
||||||
|
u.err = noEOF(err)
|
||||||
|
return nil, u.err
|
||||||
|
}
|
||||||
|
sequence, err := u.readUvarint()
|
||||||
|
if err != nil {
|
||||||
|
u.err = noEOF(err)
|
||||||
|
return nil, u.err
|
||||||
|
}
|
||||||
|
headerSize, err := u.readUvarint()
|
||||||
|
if err != nil {
|
||||||
|
u.err = noEOF(err)
|
||||||
|
return nil, u.err
|
||||||
|
}
|
||||||
|
payloadSize, err := u.readUvarint()
|
||||||
|
if err != nil {
|
||||||
|
u.err = noEOF(err)
|
||||||
|
return nil, u.err
|
||||||
|
}
|
||||||
|
// HeaderSize/PayloadSize are stored as int64: a value above MaxInt64
|
||||||
|
// converts to a negative int64, and an overflowing sum lands negative too,
|
||||||
|
// so the sign checks below cover both cases.
|
||||||
|
hdrSize, plSize := int64(headerSize), int64(payloadSize)
|
||||||
|
total := hdrSize + plSize
|
||||||
|
if nameLen > maxNameLen || hdrSize < 0 || plSize < 0 || total < 0 {
|
||||||
|
u.err = ErrInvalidArchive
|
||||||
|
return nil, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
name := make([]byte, nameLen)
|
||||||
|
if _, err := io.ReadFull(u.r, name); err != nil {
|
||||||
|
u.err = ErrInvalidArchive
|
||||||
|
return nil, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
u.header = &Header{
|
||||||
|
Name: string(name),
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Sequence: sequence,
|
||||||
|
HeaderSize: hdrSize,
|
||||||
|
PayloadSize: plSize,
|
||||||
|
}
|
||||||
|
u.remaining = total
|
||||||
|
return u.header.clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads bytes from the current entry payload.
|
||||||
|
func (u *Reader) Read(p []byte) (int, error) {
|
||||||
|
if u.err != nil {
|
||||||
|
return 0, u.err
|
||||||
|
}
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if u.header == nil || u.remaining == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(len(p)) > u.remaining {
|
||||||
|
p = p[:int(u.remaining)]
|
||||||
|
}
|
||||||
|
n, err := io.ReadFull(u.r, p)
|
||||||
|
u.remaining -= int64(n)
|
||||||
|
if err != nil {
|
||||||
|
u.err = ErrInvalidArchive
|
||||||
|
return n, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
if u.remaining == 0 {
|
||||||
|
u.header = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// noEOF converts a clean io.EOF into ErrInvalidArchive. It is used for header
|
||||||
|
// fields after the first: reaching EOF there means the entry was truncated
|
||||||
|
// rather than that the stream ended cleanly on an entry boundary.
|
||||||
|
func noEOF(err error) error {
|
||||||
|
if err != nil && errors.Is(err, io.EOF) {
|
||||||
|
return ErrInvalidArchive
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Writer) writeVarint(v int64) error {
|
||||||
|
var tmp [binary.MaxVarintLen64]byte
|
||||||
|
n := binary.PutVarint(tmp[:], v)
|
||||||
|
_, err := a.w.Write(tmp[:n])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Writer) writeUvarint(v uint64) error {
|
||||||
|
var tmp [binary.MaxVarintLen64]byte
|
||||||
|
n := binary.PutUvarint(tmp[:], v)
|
||||||
|
_, err := a.w.Write(tmp[:n])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Reader) readVarint() (int64, error) {
|
||||||
|
v, err := binary.ReadVarint(u.r)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return v, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Reader) readUvarint() (uint64, error) {
|
||||||
|
v, err := binary.ReadUvarint(u.r)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return v, ErrInvalidArchive
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
244
server/archive/archive_test.go
Normal file
244
server/archive/archive_test.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// Copyright 2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
w := NewWriter(&buf)
|
||||||
|
|
||||||
|
want := []struct {
|
||||||
|
hdr Header
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
hdr: Header{
|
||||||
|
Name: "alpha.txt",
|
||||||
|
Timestamp: 100,
|
||||||
|
HeaderSize: 4,
|
||||||
|
PayloadSize: 7,
|
||||||
|
},
|
||||||
|
data: "hello alpha",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hdr: Header{
|
||||||
|
Name: "nested/beta.txt",
|
||||||
|
Timestamp: 100,
|
||||||
|
Sequence: 42,
|
||||||
|
HeaderSize: 3,
|
||||||
|
PayloadSize: 9,
|
||||||
|
},
|
||||||
|
data: "beta payload",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range want {
|
||||||
|
if err := w.WriteHeader(&want[i].hdr); err != nil {
|
||||||
|
t.Fatalf("write header %d: %v", i, err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, want[i].data); err != nil {
|
||||||
|
t.Fatalf("write payload %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatalf("close writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
||||||
|
|
||||||
|
for i := range want {
|
||||||
|
hdr, err := r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("next %d: %v", i, err)
|
||||||
|
}
|
||||||
|
if hdr.Name != want[i].hdr.Name {
|
||||||
|
t.Fatalf("header %d name mismatch: got %q want %q", i, hdr.Name, want[i].hdr.Name)
|
||||||
|
}
|
||||||
|
if hdr.Timestamp != want[i].hdr.Timestamp {
|
||||||
|
t.Fatalf("header %d timestamp mismatch: got %d want %d", i, hdr.Timestamp, want[i].hdr.Timestamp)
|
||||||
|
}
|
||||||
|
if hdr.Sequence != want[i].hdr.Sequence {
|
||||||
|
t.Fatalf("header %d sequence mismatch: got %d want %d", i, hdr.Sequence, want[i].hdr.Sequence)
|
||||||
|
}
|
||||||
|
if hdr.HeaderSize != want[i].hdr.HeaderSize {
|
||||||
|
t.Fatalf("header %d header size mismatch: got %d want %d", i, hdr.HeaderSize, want[i].hdr.HeaderSize)
|
||||||
|
}
|
||||||
|
if hdr.PayloadSize != want[i].hdr.PayloadSize {
|
||||||
|
t.Fatalf("header %d payload size mismatch: got %d want %d", i, hdr.PayloadSize, want[i].hdr.PayloadSize)
|
||||||
|
}
|
||||||
|
got, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read payload %d: %v", i, err)
|
||||||
|
}
|
||||||
|
if string(got) != want[i].data {
|
||||||
|
t.Fatalf("payload %d mismatch: got %q want %q", i, got, want[i].data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatalf("expected EOF, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextSkipsUnreadPayload(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
w := NewWriter(&buf)
|
||||||
|
first := Header{Name: "first", HeaderSize: 0, PayloadSize: int64(len("abcdef"))}
|
||||||
|
if err := w.WriteHeader(&first); err != nil {
|
||||||
|
t.Fatalf("write first header: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "abcdef"); err != nil {
|
||||||
|
t.Fatalf("write first payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
second := Header{Name: "second", HeaderSize: 0, PayloadSize: int64(len("second-body"))}
|
||||||
|
if err := w.WriteHeader(&second); err != nil {
|
||||||
|
t.Fatalf("write second header: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "second-body"); err != nil {
|
||||||
|
t.Fatalf("write second payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
||||||
|
|
||||||
|
hdr, err := r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("next first: %v", err)
|
||||||
|
}
|
||||||
|
if hdr.Name != "first" {
|
||||||
|
t.Fatalf("unexpected first header name %q", hdr.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf1 := make([]byte, 3)
|
||||||
|
if _, err := io.ReadFull(r, buf1); err != nil {
|
||||||
|
t.Fatalf("read partial first payload: %v", err)
|
||||||
|
}
|
||||||
|
if string(buf1) != "abc" {
|
||||||
|
t.Fatalf("unexpected partial payload %q", buf1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr, err = r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("next second: %v", err)
|
||||||
|
}
|
||||||
|
if hdr.Name != "second" {
|
||||||
|
t.Fatalf("unexpected second header name %q", hdr.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read second payload: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "second-body" {
|
||||||
|
t.Fatalf("unexpected second payload %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validArchive returns a well-formed archive holding a single entry whose
|
||||||
|
// payload is the 6-byte string "abcdef" split as HeaderSize=2, PayloadSize=4.
|
||||||
|
func validArchive(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w := NewWriter(&buf)
|
||||||
|
hdr := Header{Name: "first", Timestamp: 7, Sequence: 3, HeaderSize: 2, PayloadSize: 4}
|
||||||
|
if err := w.WriteHeader(&hdr); err != nil {
|
||||||
|
t.Fatalf("write header: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "abcdef"); err != nil {
|
||||||
|
t.Fatalf("write payload: %v", err)
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatalf("close: %v", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyArchiveReturnsEOF(t *testing.T) {
|
||||||
|
r := NewReader(bytes.NewReader(nil))
|
||||||
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatalf("expected io.EOF on empty archive, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidMagicIsRejected(t *testing.T) {
|
||||||
|
r := NewReader(bytes.NewReader([]byte("not-a-valid-archive-stream")))
|
||||||
|
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
|
||||||
|
t.Fatalf("expected ErrInvalidArchive, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A clean EOF on the first header field means end-of-stream, but an EOF
|
||||||
|
// partway through the header means the entry was truncated.
|
||||||
|
func TestTruncatedHeaderIsInvalid(t *testing.T) {
|
||||||
|
full := validArchive(t)
|
||||||
|
// Keep the magic plus the single name-length byte, dropping the rest.
|
||||||
|
r := NewReader(bytes.NewReader(full[:len(MagicBytes)+1]))
|
||||||
|
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
|
||||||
|
t.Fatalf("expected ErrInvalidArchive, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncatedPayloadIsInvalid(t *testing.T) {
|
||||||
|
full := validArchive(t)
|
||||||
|
// Drop the last 3 payload bytes; the header still decodes cleanly.
|
||||||
|
r := NewReader(bytes.NewReader(full[:len(full)-3]))
|
||||||
|
hdr, err := r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("next: %v", err)
|
||||||
|
}
|
||||||
|
if hdr.HeaderSize+hdr.PayloadSize != 6 {
|
||||||
|
t.Fatalf("unexpected payload size %d", hdr.HeaderSize+hdr.PayloadSize)
|
||||||
|
}
|
||||||
|
if _, err := io.ReadAll(r); !errors.Is(err, ErrInvalidArchive) {
|
||||||
|
t.Fatalf("expected ErrInvalidArchive reading payload, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The header round-trips when its fields are read directly from the stream,
|
||||||
|
// including a large Sequence (uvarint) and a negative Timestamp (varint).
|
||||||
|
func TestHeaderFieldEncoding(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w := NewWriter(&buf)
|
||||||
|
want := Header{Name: "nested/x.dat", Timestamp: -123, Sequence: 1 << 60, HeaderSize: 0, PayloadSize: 0}
|
||||||
|
if err := w.WriteHeader(&want); err != nil {
|
||||||
|
t.Fatalf("write header: %v", err)
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatalf("close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
||||||
|
got, err := r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("next: %v", err)
|
||||||
|
}
|
||||||
|
if *got != want {
|
||||||
|
t.Fatalf("header round-trip mismatch: got %+v want %+v", *got, want)
|
||||||
|
}
|
||||||
|
// A zero-length payload yields immediate EOF, then a clean end-of-stream.
|
||||||
|
if n, err := r.Read(make([]byte, 4)); n != 0 || !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatalf("expected (0, EOF) reading empty payload, got (%d, %v)", n, err)
|
||||||
|
}
|
||||||
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatalf("expected io.EOF after last entry, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
server/ats/ats.go
Normal file
86
server/ats/ats.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// ats controls the go routines for the access time service.
|
||||||
|
// This allows more efficient unixnano operations for cache access times.
|
||||||
|
// We will have one per binary (usually per server).
|
||||||
|
package ats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update every 100ms for gathering access time in unix nano.
|
||||||
|
const TickInterval = 100 * time.Millisecond
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Our unix nano time.
|
||||||
|
utime atomic.Int64
|
||||||
|
// How may registered users do we have, controls lifetime of Go routine.
|
||||||
|
refs atomic.Int64
|
||||||
|
// To signal the shutdown of the Go routine.
|
||||||
|
done chan struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Initialize our done chan.
|
||||||
|
done = make(chan struct{}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register usage. This will happen on filestore creation.
|
||||||
|
func Register() {
|
||||||
|
if v := refs.Add(1); v == 1 {
|
||||||
|
// This is the first to register (could also go up and down),
|
||||||
|
// so spin up Go routine and grab initial time.
|
||||||
|
utime.Store(time.Now().UnixNano())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(TickInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
utime.Store(time.Now().UnixNano())
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister usage. We will shutdown the go routine if no more registered users.
|
||||||
|
func Unregister() {
|
||||||
|
if v := refs.Add(-1); v == 0 {
|
||||||
|
done <- struct{}{}
|
||||||
|
} else if v < 0 {
|
||||||
|
refs.Store(0)
|
||||||
|
panic("unbalanced unregister for access time state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will load the access time from an atomic.
|
||||||
|
// If no one has registered this will return 0 or stale data.
|
||||||
|
// It is the responsibility of the user to properly register and unregister.
|
||||||
|
func AccessTime() int64 {
|
||||||
|
// Return last updated time.
|
||||||
|
v := utime.Load()
|
||||||
|
if v == 0 {
|
||||||
|
// Always register a time, the worst case is a stale time.
|
||||||
|
// On startup, we can register in parallel and could previously panic.
|
||||||
|
v = time.Now().UnixNano()
|
||||||
|
utime.Store(v)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
98
server/ats/ats_test.go
Normal file
98
server/ats/ats_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotRunningValue(t *testing.T) {
|
||||||
|
// Set back to zero in case this test gets run multiple times via --count.
|
||||||
|
utime.Store(0)
|
||||||
|
at := AccessTime()
|
||||||
|
if at == 0 {
|
||||||
|
t.Fatal("Expected non-zero access time")
|
||||||
|
}
|
||||||
|
|
||||||
|
atn := AccessTime()
|
||||||
|
if atn != at {
|
||||||
|
t.Fatal("Did not expect updates to access time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterAndUnregister(t *testing.T) {
|
||||||
|
ngrp := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
Register()
|
||||||
|
if r := refs.Load(); r != 1 {
|
||||||
|
t.Fatalf("Expected refs to be 1, got %d", r)
|
||||||
|
}
|
||||||
|
// Make sure we have a non-zero time.
|
||||||
|
at := AccessTime()
|
||||||
|
if at == 0 {
|
||||||
|
t.Fatal("Expected non-zero access time")
|
||||||
|
}
|
||||||
|
// Make sure we are updating access time.
|
||||||
|
time.Sleep(2 * TickInterval)
|
||||||
|
atn := AccessTime()
|
||||||
|
if atn == at {
|
||||||
|
t.Fatal("Expected access time to be updated but it was not")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now unregister.
|
||||||
|
Unregister()
|
||||||
|
if r := refs.Load(); r != 0 {
|
||||||
|
t.Fatalf("Expected refs to be 0, got %d", r)
|
||||||
|
}
|
||||||
|
time.Sleep(TickInterval)
|
||||||
|
|
||||||
|
at = AccessTime()
|
||||||
|
time.Sleep(2 * TickInterval)
|
||||||
|
atn = AccessTime()
|
||||||
|
if atn != at {
|
||||||
|
t.Fatal("Did not expect updates to access time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have no additional go routines running.
|
||||||
|
ngra := runtime.NumGoroutine()
|
||||||
|
if ngra != ngrp {
|
||||||
|
t.Fatalf("Expected same number of go routines after removing all registered: %d vs %d", ngrp, ngra)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we spin back up on going from zero to one registered after spinning down.
|
||||||
|
Register()
|
||||||
|
defer Unregister()
|
||||||
|
|
||||||
|
at = AccessTime()
|
||||||
|
time.Sleep(2 * TickInterval)
|
||||||
|
atn = AccessTime()
|
||||||
|
if atn == at {
|
||||||
|
t.Fatal("Expected access time to be updated but it was not")
|
||||||
|
}
|
||||||
|
ngra = runtime.NumGoroutine()
|
||||||
|
if ngra != ngrp+1 {
|
||||||
|
t.Fatalf("Expected to see additional go routine: %d vs %d", ngrp, ngra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnbalancedUnregister(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("Expected function to panic, but it did not")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
Unregister()
|
||||||
|
}
|
||||||
1837
server/auth.go
Normal file
1837
server/auth.go
Normal file
File diff suppressed because it is too large
Load Diff
503
server/auth_callout.go
Normal file
503
server/auth_callout.go
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
// Copyright 2022-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/nats-io/jwt/v2"
|
||||||
|
"github.com/nats-io/nkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
|
||||||
|
AuthRequestSubject = "nats-authorization-request"
|
||||||
|
AuthRequestXKeyHeader = "Nats-Server-Xkey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func titleCase(m string) string {
|
||||||
|
r := []rune(m)
|
||||||
|
if len(r) == 0 {
|
||||||
|
return _EMPTY_
|
||||||
|
}
|
||||||
|
return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a callout on this client's behalf.
|
||||||
|
func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequired, trustedProxy bool, ujwt string) (authorized bool, errStr string) {
|
||||||
|
isOperatorMode := len(opts.TrustedKeys) > 0
|
||||||
|
|
||||||
|
// this is the account the user connected in, or the one running the callout
|
||||||
|
var acc *Account
|
||||||
|
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
|
||||||
|
aname := opts.AuthCallout.Account
|
||||||
|
var err error
|
||||||
|
acc, err = s.LookupAccount(aname)
|
||||||
|
if err != nil {
|
||||||
|
errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
|
||||||
|
s.Warnf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc = c.acc
|
||||||
|
}
|
||||||
|
if acc == nil {
|
||||||
|
// FIX for https://github.com/nats-io/nats-server/issues/7841
|
||||||
|
// hand rolled creds on leafnode became crasher here
|
||||||
|
errStr = fmt.Sprintf("%s not mapped to a callout account", c.kindString())
|
||||||
|
s.Warnf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have been requested to encrypt.
|
||||||
|
var xkp nkeys.KeyPair
|
||||||
|
var xkey string
|
||||||
|
var pubAccXKey string
|
||||||
|
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.XKey != _EMPTY_ {
|
||||||
|
pubAccXKey = opts.AuthCallout.XKey
|
||||||
|
} else if isOperatorMode {
|
||||||
|
pubAccXKey = acc.externalAuthXKey()
|
||||||
|
}
|
||||||
|
// If set grab server's xkey keypair and public key.
|
||||||
|
if pubAccXKey != _EMPTY_ {
|
||||||
|
// These are only set on creation, so lock not needed.
|
||||||
|
xkp, xkey = s.xkp, s.info.XKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a keypair for the user. We will expect this public user to be in the signed response.
|
||||||
|
// This prevents replay attacks.
|
||||||
|
ukp, _ := nkeys.CreateUser()
|
||||||
|
pub, _ := ukp.PublicKey()
|
||||||
|
|
||||||
|
reply := s.newRespInbox()
|
||||||
|
respCh := make(chan string, 1)
|
||||||
|
|
||||||
|
decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
|
||||||
|
account := acc.Name
|
||||||
|
_, msg := rc.msgParts(rmsg)
|
||||||
|
|
||||||
|
// This signals not authorized.
|
||||||
|
// Since this is an account subscription will always have "\r\n".
|
||||||
|
if len(msg) <= LEN_CR_LF {
|
||||||
|
return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
|
||||||
|
}
|
||||||
|
// Strip trailing CRLF.
|
||||||
|
msg = msg[:len(msg)-LEN_CR_LF]
|
||||||
|
encrypted := false
|
||||||
|
// If we sent an encrypted request the response could be encrypted as well.
|
||||||
|
// we are expecting the input to be `eyJ` if it is a JWT
|
||||||
|
if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
|
||||||
|
var err error
|
||||||
|
msg, err = xkp.Open(msg, pubAccXKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
|
||||||
|
}
|
||||||
|
encrypted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vr := jwt.CreateValidationResults()
|
||||||
|
cr.Validate(vr)
|
||||||
|
if len(vr.Issues) > 0 {
|
||||||
|
return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// the subject is the user id
|
||||||
|
if cr.Subject != pub {
|
||||||
|
return nil, errors.New("auth callout violation: auth callout response is not for expected user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the audience to be the server ID
|
||||||
|
if cr.Audience != s.info.ID {
|
||||||
|
return nil, errors.New("auth callout violation: auth callout response is not for server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if had an error message from the auth account
|
||||||
|
if cr.Error != _EMPTY_ {
|
||||||
|
return nil, fmt.Errorf("auth callout service returned an error: %v", cr.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if response is encrypted none of this is needed
|
||||||
|
if isOperatorMode && !encrypted {
|
||||||
|
pkStr := cr.Issuer
|
||||||
|
if cr.IssuerAccount != _EMPTY_ {
|
||||||
|
pkStr = cr.IssuerAccount
|
||||||
|
}
|
||||||
|
if pkStr != account {
|
||||||
|
if _, ok := acc.hasIssuer(pkStr); !ok {
|
||||||
|
return nil, errors.New("auth callout signing key is unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.DecodeUserClaims(cr.Jwt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
|
||||||
|
// only in operator mode we expect to receive `issuer_account`.
|
||||||
|
getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
|
||||||
|
// Make sure correct issuer.
|
||||||
|
var issuer string
|
||||||
|
if opts.AuthCallout != nil {
|
||||||
|
issuer = opts.AuthCallout.Issuer
|
||||||
|
} else {
|
||||||
|
// Operator mode is who we send the request on unless switching accounts.
|
||||||
|
issuer = acc.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// the jwt issuer can be a signing key
|
||||||
|
jwtIssuer := arc.Issuer
|
||||||
|
if arc.IssuerAccount != _EMPTY_ {
|
||||||
|
if !isOperatorMode {
|
||||||
|
// this should be invalid - effectively it would allow the auth callout
|
||||||
|
// to issue on another account which may be allowed given the configuration
|
||||||
|
// where the auth callout account can handle multiple different ones..
|
||||||
|
return _EMPTY_, fmt.Errorf("error non operator mode account %q: attempted to use issuer_account", account)
|
||||||
|
}
|
||||||
|
jwtIssuer = arc.IssuerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwtIssuer != issuer {
|
||||||
|
if !isOperatorMode {
|
||||||
|
return _EMPTY_, fmt.Errorf("wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
|
||||||
|
} else if !acc.isAllowedAcount(jwtIssuer) {
|
||||||
|
return _EMPTY_, fmt.Errorf("account %q not permitted as valid account option for auth callout for account %q",
|
||||||
|
arc.Issuer, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jwtIssuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
|
||||||
|
allowNow, expiration := validateTimes(arc)
|
||||||
|
if !allowNow {
|
||||||
|
c.Errorf("Outside connect times")
|
||||||
|
return 0, nil, fmt.Errorf("authorized user on account %q outside of valid connect times", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
|
||||||
|
if err != nil {
|
||||||
|
c.Debugf("%v", err)
|
||||||
|
if len(allowedConnTypes) == 0 {
|
||||||
|
return 0, nil, fmt.Errorf("authorized user on account %q using invalid connection type", account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expiration, allowedConnTypes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
|
||||||
|
// Apply to this client.
|
||||||
|
var err error
|
||||||
|
issuerAccount, err := getIssuerAccount(arc, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are not in operator mode, they can specify placement as a tag
|
||||||
|
var placement string
|
||||||
|
if !isOperatorMode {
|
||||||
|
// only allow placement if we are not in operator mode
|
||||||
|
placement = arc.Audience
|
||||||
|
} else {
|
||||||
|
placement = issuerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAcc, err := s.LookupAccount(placement)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("no valid account %q for auth callout response on account %q: %v", placement, account, err)
|
||||||
|
}
|
||||||
|
if isOperatorMode {
|
||||||
|
// this will validate the signing key that emitted the user, and if it is a signing
|
||||||
|
// key it assigns the permissions from the target account
|
||||||
|
if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
|
||||||
|
return nil, fmt.Errorf("user JWT issuer %q is not known", arc.Issuer)
|
||||||
|
} else if scope != nil {
|
||||||
|
// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
|
||||||
|
if err := scope.ValidateScopedSigner(arc); err != nil {
|
||||||
|
return nil, fmt.Errorf("user JWT is not valid: %v", err)
|
||||||
|
} else if uSc, ok := scope.(*jwt.UserScope); !ok {
|
||||||
|
return nil, fmt.Errorf("user JWT is not a valid scoped user")
|
||||||
|
} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
|
||||||
|
return nil, fmt.Errorf("user JWT generated invalid permissions: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetAcc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
|
||||||
|
arc, err := decodeResponse(rc, rmsg, racc)
|
||||||
|
if err != nil {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- titleCase(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If the caller had established that the user should go through a proxy,
|
||||||
|
// or if the `arc` JWT requires it, and we don't have a trusted proxy,
|
||||||
|
// reject the connection.
|
||||||
|
if (proxyRequired || arc.ProxyRequired) && !trustedProxy {
|
||||||
|
err = ErrAuthProxyRequired
|
||||||
|
c.setAuthError(err)
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- titleCase(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vr := jwt.CreateValidationResults()
|
||||||
|
arc.Validate(vr)
|
||||||
|
if len(vr.Issues) > 0 {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the user is what we requested.
|
||||||
|
if arc.Subject != pub {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- titleCase(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- titleCase(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// the JWT is cleared, because if in operator mode it may hold the JWT
|
||||||
|
// for the bearer token that connected to the callout if in operator mode
|
||||||
|
// the permissions are already set on the client, this prevents a decode
|
||||||
|
// on c.RegisterNKeyUser which would have wrong values
|
||||||
|
c.mu.Lock()
|
||||||
|
c.opts.JWT = _EMPTY_
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// Build internal user and bind to the targeted account.
|
||||||
|
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
|
||||||
|
if err := c.RegisterNkeyUser(nkuser); err != nil {
|
||||||
|
c.authViolation()
|
||||||
|
respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if the response wants to override the username.
|
||||||
|
if arc.Name != _EMPTY_ {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.opts.Username = arc.Name
|
||||||
|
// Clear any others.
|
||||||
|
c.opts.Nkey = _EMPTY_
|
||||||
|
c.pubKey = _EMPTY_
|
||||||
|
c.opts.Token = _EMPTY_
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to set an auth timer if the user jwt expires.
|
||||||
|
c.setExpiration(arc.Claims(), expiration)
|
||||||
|
|
||||||
|
respCh <- _EMPTY_
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a subscription to receive a response from the authcallout
|
||||||
|
sub, err := acc.subscribeInternal(reply, processReply)
|
||||||
|
if err != nil {
|
||||||
|
errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
|
||||||
|
s.Warnf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
defer acc.unsubscribeInternal(sub)
|
||||||
|
|
||||||
|
// Build our request claims - jwt subject should be nkey
|
||||||
|
jwtSub := acc.Name
|
||||||
|
if opts.AuthCallout != nil {
|
||||||
|
jwtSub = opts.AuthCallout.Issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// The public key of the server, if set is available on Varz.Key
|
||||||
|
// This means that when a service connects, it can now peer
|
||||||
|
// authenticate if it wants to - but that also means that it needs to be
|
||||||
|
// listening to cluster changes
|
||||||
|
claim := jwt.NewAuthorizationRequestClaims(jwtSub)
|
||||||
|
claim.Audience = AuthRequestSubject
|
||||||
|
// Set expected public user nkey.
|
||||||
|
claim.UserNkey = pub
|
||||||
|
|
||||||
|
s.mu.RLock()
|
||||||
|
claim.Server = jwt.ServerID{
|
||||||
|
Name: s.info.Name,
|
||||||
|
Host: s.info.Host,
|
||||||
|
ID: s.info.ID,
|
||||||
|
Version: s.info.Version,
|
||||||
|
Cluster: s.info.Cluster,
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
claim.Server.Tags = s.getOpts().Tags
|
||||||
|
|
||||||
|
// Check if we have been requested to encrypt.
|
||||||
|
// FIXME: possibly this public key also needs to be on the
|
||||||
|
// Varz, because then it can be peer verified?
|
||||||
|
if xkp != nil {
|
||||||
|
claim.Server.XKey = xkey
|
||||||
|
}
|
||||||
|
|
||||||
|
authTimeout := secondsToDuration(s.getOpts().AuthTimeout)
|
||||||
|
claim.Expires = time.Now().Add(time.Duration(authTimeout)).UTC().Unix()
|
||||||
|
|
||||||
|
// Grab client info for the request.
|
||||||
|
c.mu.Lock()
|
||||||
|
c.fillClientInfo(&claim.ClientInformation)
|
||||||
|
c.fillConnectOpts(&claim.ConnectOptions, ujwt)
|
||||||
|
// If we have a sig in the client opts, fill in nonce.
|
||||||
|
if claim.ConnectOptions.SignedNonce != _EMPTY_ {
|
||||||
|
claim.ClientInformation.Nonce = string(c.nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS
|
||||||
|
if c.flags.isSet(handshakeComplete) && c.nc != nil {
|
||||||
|
var ct jwt.ClientTLS
|
||||||
|
conn := c.nc.(*tls.Conn)
|
||||||
|
cs := conn.ConnectionState()
|
||||||
|
ct.Version = tlsVersion(cs.Version)
|
||||||
|
ct.Cipher = tls.CipherSuiteName(cs.CipherSuite)
|
||||||
|
// Check verified chains.
|
||||||
|
for _, vs := range cs.VerifiedChains {
|
||||||
|
var certs []string
|
||||||
|
for _, c := range vs {
|
||||||
|
blk := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: c.Raw,
|
||||||
|
}
|
||||||
|
certs = append(certs, string(pem.EncodeToMemory(blk)))
|
||||||
|
}
|
||||||
|
ct.VerifiedChains = append(ct.VerifiedChains, certs)
|
||||||
|
}
|
||||||
|
// If we do not have verified chains put in peer certs.
|
||||||
|
if len(ct.VerifiedChains) == 0 {
|
||||||
|
for _, c := range cs.PeerCertificates {
|
||||||
|
blk := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: c.Raw,
|
||||||
|
}
|
||||||
|
ct.Certs = append(ct.Certs, string(pem.EncodeToMemory(blk)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
claim.TLS = &ct
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
b, err := claim.Encode(s.kp)
|
||||||
|
if err != nil {
|
||||||
|
errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
|
||||||
|
s.Warnf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
req := []byte(b)
|
||||||
|
var hdr []byte
|
||||||
|
|
||||||
|
// Check if we have been asked to encrypt.
|
||||||
|
if xkp != nil {
|
||||||
|
req, err = xkp.Seal([]byte(req), pubAccXKey)
|
||||||
|
if err != nil {
|
||||||
|
errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
|
||||||
|
s.Warnf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
hdr = genHeader(hdr, AuthRequestXKeyHeader, xkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send out our request.
|
||||||
|
if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
|
||||||
|
errStr = fmt.Sprintf("Error sending authorization request: %v", err)
|
||||||
|
s.Debugf(errStr)
|
||||||
|
return false, errStr
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case errStr = <-respCh:
|
||||||
|
if authorized = errStr == _EMPTY_; !authorized {
|
||||||
|
s.Warnf(errStr)
|
||||||
|
}
|
||||||
|
case <-time.After(authTimeout):
|
||||||
|
s.Debugf(fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorized, errStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in client information for the request.
|
||||||
|
// Lock should be held.
|
||||||
|
func (c *client) fillClientInfo(ci *jwt.ClientInformation) {
|
||||||
|
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
|
||||||
|
*ci = jwt.ClientInformation{
|
||||||
|
Host: c.host,
|
||||||
|
ID: c.cid,
|
||||||
|
User: c.getRawAuthUser(),
|
||||||
|
Name: c.opts.Name,
|
||||||
|
Tags: c.tags,
|
||||||
|
NameTag: c.nameTag,
|
||||||
|
Kind: c.kindString(),
|
||||||
|
Type: c.clientTypeString(),
|
||||||
|
MQTT: c.getMQTTClientID(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in client options.
|
||||||
|
// Lock should be held.
|
||||||
|
func (c *client) fillConnectOpts(opts *jwt.ConnectOptions, ujwt string) {
|
||||||
|
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
o := c.opts
|
||||||
|
if ujwt == _EMPTY_ {
|
||||||
|
// The caller may supply a reconstructed JWT that should be sent to auth
|
||||||
|
// callout without storing it in c.opts.JWT. If not, fall back to the client
|
||||||
|
// option as before.
|
||||||
|
ujwt = o.JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
|
||||||
|
*opts = jwt.ConnectOptions{
|
||||||
|
JWT: ujwt,
|
||||||
|
Nkey: o.Nkey,
|
||||||
|
SignedNonce: o.Sig,
|
||||||
|
Token: o.Token,
|
||||||
|
Username: o.Username,
|
||||||
|
Password: o.Password,
|
||||||
|
Name: o.Name,
|
||||||
|
Lang: o.Lang,
|
||||||
|
Version: o.Version,
|
||||||
|
Protocol: o.Protocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
3258
server/auth_callout_test.go
Normal file
3258
server/auth_callout_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1246
server/auth_test.go
Normal file
1246
server/auth_test.go
Normal file
File diff suppressed because it is too large
Load Diff
198
server/avl/norace_test.go
Normal file
198
server/avl/norace_test.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// Copyright 2023-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !race && !skip_no_race_tests && !skip_no_race_1_tests
|
||||||
|
|
||||||
|
package avl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand/v2"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Print Results: go test -v --args --results
|
||||||
|
var printResults = flag.Bool("results", false, "Enable Results Logging")
|
||||||
|
|
||||||
|
// SequenceSet memory tests vs dmaps.
|
||||||
|
func TestNoRaceSeqSetSizeComparison(t *testing.T) {
|
||||||
|
// Create 5M random entries (dupes possible but ok for this test) out of 8M range.
|
||||||
|
num := 5_000_000
|
||||||
|
max := 7_000_000
|
||||||
|
|
||||||
|
seqs := make([]uint64, 0, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
n := uint64(rand.Int64N(int64(max + 1)))
|
||||||
|
seqs = append(seqs, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
// Disable to get stable results.
|
||||||
|
gcp := debug.SetGCPercent(-1)
|
||||||
|
defer debug.SetGCPercent(gcp)
|
||||||
|
|
||||||
|
mem := runtime.MemStats{}
|
||||||
|
runtime.ReadMemStats(&mem)
|
||||||
|
inUseBefore := mem.HeapInuse
|
||||||
|
|
||||||
|
dmap := make(map[uint64]struct{}, num)
|
||||||
|
for _, n := range seqs {
|
||||||
|
dmap[n] = struct{}{}
|
||||||
|
}
|
||||||
|
runtime.ReadMemStats(&mem)
|
||||||
|
dmapUse := mem.HeapInuse - inUseBefore
|
||||||
|
inUseBefore = mem.HeapInuse
|
||||||
|
|
||||||
|
// Now do SequenceSet on same dataset.
|
||||||
|
var sset SequenceSet
|
||||||
|
for _, n := range seqs {
|
||||||
|
sset.Insert(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.ReadMemStats(&mem)
|
||||||
|
seqSetUse := mem.HeapInuse - inUseBefore
|
||||||
|
|
||||||
|
if seqSetUse > 2*1024*1024 {
|
||||||
|
t.Fatalf("Expected SequenceSet size to be < 2M, got %v", friendlyBytes(seqSetUse))
|
||||||
|
}
|
||||||
|
if seqSetUse*50 > dmapUse {
|
||||||
|
t.Fatalf("Expected SequenceSet to be at least 50x better then dmap approach: %v vs %v",
|
||||||
|
friendlyBytes(seqSetUse),
|
||||||
|
friendlyBytes(dmapUse),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoRaceSeqSetEncodeLarge(t *testing.T) {
|
||||||
|
num := 2_500_000
|
||||||
|
max := 5_000_000
|
||||||
|
|
||||||
|
dmap := make(map[uint64]struct{}, num)
|
||||||
|
var ss SequenceSet
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
n := uint64(rand.Int64N(int64(max + 1)))
|
||||||
|
ss.Insert(n)
|
||||||
|
dmap[n] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable to get stable results.
|
||||||
|
gcp := debug.SetGCPercent(-1)
|
||||||
|
defer debug.SetGCPercent(gcp)
|
||||||
|
|
||||||
|
// In general should be about the same, but can see some variability.
|
||||||
|
expected := time.Millisecond
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
b := ss.Encode(nil)
|
||||||
|
|
||||||
|
if elapsed := time.Since(start); elapsed > expected {
|
||||||
|
t.Fatalf("Expected encode of %d items with encoded size %v to take less than %v, got %v",
|
||||||
|
num, friendlyBytes(len(b)), expected, elapsed)
|
||||||
|
} else {
|
||||||
|
logResults("Encode time for %d items was %v, encoded size is %v\n", num, elapsed, friendlyBytes(len(b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
ss2, _, err := Decode(b)
|
||||||
|
require_NoError(t, err)
|
||||||
|
if elapsed := time.Since(start); elapsed > expected {
|
||||||
|
t.Fatalf("Expected decode to take less than %v, got %v", expected, elapsed)
|
||||||
|
} else {
|
||||||
|
logResults("Decode time is %v\n", elapsed)
|
||||||
|
}
|
||||||
|
require_True(t, ss.Nodes() == ss2.Nodes())
|
||||||
|
require_True(t, ss.Size() == ss2.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoRaceSeqSetRelativeSpeed(t *testing.T) {
|
||||||
|
// Create 1M random entries (dupes possible but ok for this test) out of 3M range.
|
||||||
|
num := 1_000_000
|
||||||
|
max := 3_000_000
|
||||||
|
|
||||||
|
seqs := make([]uint64, 0, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
n := uint64(rand.Int64N(int64(max + 1)))
|
||||||
|
seqs = append(seqs, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
// Now do SequenceSet on same dataset.
|
||||||
|
var sset SequenceSet
|
||||||
|
for _, n := range seqs {
|
||||||
|
sset.Insert(n)
|
||||||
|
}
|
||||||
|
ssInsertElapsed := time.Since(start)
|
||||||
|
logResults("Inserts SequenceSet: %v for %d items\n", ssInsertElapsed, num)
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
for _, n := range seqs {
|
||||||
|
if ok := sset.Exists(n); !ok {
|
||||||
|
t.Fatalf("Should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ssLookupElapsed := time.Since(start)
|
||||||
|
logResults("Lookups: %v\n", ssLookupElapsed)
|
||||||
|
|
||||||
|
// Now do a map.
|
||||||
|
dmap := make(map[uint64]struct{})
|
||||||
|
start = time.Now()
|
||||||
|
for _, n := range seqs {
|
||||||
|
dmap[n] = struct{}{}
|
||||||
|
}
|
||||||
|
mapInsertElapsed := time.Since(start)
|
||||||
|
logResults("Inserts Map[uint64]: %v for %d items\n", mapInsertElapsed, num)
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
for _, n := range seqs {
|
||||||
|
if _, ok := dmap[n]; !ok {
|
||||||
|
t.Fatalf("Should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapLookupElapsed := time.Since(start)
|
||||||
|
logResults("Lookups: %v\n", mapLookupElapsed)
|
||||||
|
|
||||||
|
// In general we are between 1.5 and 1.75 times slower atm then a straight map.
|
||||||
|
// Let's test an upper bound of 2x for now.
|
||||||
|
if mapInsertElapsed*2 <= ssInsertElapsed {
|
||||||
|
t.Fatalf("Expected SequenceSet insert to be no more than 2x slower (%v vs %v)", mapInsertElapsed, ssInsertElapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapLookupElapsed*3 <= ssLookupElapsed {
|
||||||
|
t.Fatalf("Expected SequenceSet lookups to be no more than 3x slower (%v vs %v)", mapLookupElapsed, ssLookupElapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// friendlyBytes returns a string with the given bytes int64
|
||||||
|
// represented as a size, such as 1KB, 10MB, etc...
|
||||||
|
func friendlyBytes[T int | uint64 | int64](bytes T) string {
|
||||||
|
fbytes := float64(bytes)
|
||||||
|
base := 1024
|
||||||
|
pre := []string{"K", "M", "G", "T", "P", "E"}
|
||||||
|
if fbytes < float64(base) {
|
||||||
|
return fmt.Sprintf("%v B", fbytes)
|
||||||
|
}
|
||||||
|
exp := int(math.Log(fbytes) / math.Log(float64(base)))
|
||||||
|
index := exp - 1
|
||||||
|
return fmt.Sprintf("%.2f %sB", fbytes/math.Pow(float64(base), float64(exp)), pre[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
func logResults(format string, args ...any) {
|
||||||
|
if *printResults {
|
||||||
|
fmt.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
715
server/avl/seqset.go
Normal file
715
server/avl/seqset.go
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
// Copyright 2023-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package avl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SequenceSet is a memory and encoding optimized set for storing unsigned ints.
|
||||||
|
//
|
||||||
|
// SequenceSet is ~80-100 times more efficient memory wise than a map[uint64]struct{}.
|
||||||
|
// SequenceSet is ~1.75 times slower at inserts than the same map.
|
||||||
|
// SequenceSet is not thread safe.
|
||||||
|
//
|
||||||
|
// We use an AVL tree with nodes that hold bitmasks for set membership.
|
||||||
|
//
|
||||||
|
// Encoding will convert to a space optimized encoding using bitmasks.
|
||||||
|
type SequenceSet struct {
|
||||||
|
root *node // root node
|
||||||
|
size int // number of items
|
||||||
|
nodes int // number of nodes
|
||||||
|
// Having this here vs on the stack in Insert/Delete
|
||||||
|
// makes a difference in memory usage.
|
||||||
|
changed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert will insert the sequence into the set.
|
||||||
|
// The tree will be balanced inline.
|
||||||
|
func (ss *SequenceSet) Insert(seq uint64) {
|
||||||
|
// If a node covering seq already exists, setting a bit can not change the
|
||||||
|
// tree shape, so skip the recursive descent and rebalance checks.
|
||||||
|
for n := ss.root; n != nil; {
|
||||||
|
if seq < n.base {
|
||||||
|
n = n.l
|
||||||
|
} else if seq >= n.base+numEntries {
|
||||||
|
n = n.r
|
||||||
|
} else {
|
||||||
|
n.set(seq, &ss.changed)
|
||||||
|
if ss.changed {
|
||||||
|
ss.changed = false
|
||||||
|
ss.size++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ss.root = ss.root.insert(seq, &ss.changed, &ss.nodes); ss.changed {
|
||||||
|
ss.changed = false
|
||||||
|
ss.size++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists will return true iff the sequence is a member of this set.
|
||||||
|
func (ss *SequenceSet) Exists(seq uint64) bool {
|
||||||
|
for n := ss.root; n != nil; {
|
||||||
|
if seq < n.base {
|
||||||
|
n = n.l
|
||||||
|
continue
|
||||||
|
} else if seq >= n.base+numEntries {
|
||||||
|
n = n.r
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n.exists(seq)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInitialMin should be used to set the initial minimum sequence when known.
|
||||||
|
// This will more effectively utilize space versus self selecting.
|
||||||
|
// The set should be empty.
|
||||||
|
func (ss *SequenceSet) SetInitialMin(min uint64) error {
|
||||||
|
if !ss.IsEmpty() {
|
||||||
|
return ErrSetNotEmpty
|
||||||
|
}
|
||||||
|
ss.root, ss.nodes = &node{base: min, h: 1}, 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete will remove the sequence from the set.
|
||||||
|
// Will optionally remove nodes and rebalance.
|
||||||
|
// Returns where the sequence was set.
|
||||||
|
func (ss *SequenceSet) Delete(seq uint64) bool {
|
||||||
|
if ss == nil || ss.root == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ss.root = ss.root.delete(seq, &ss.changed, &ss.nodes)
|
||||||
|
if ss.changed {
|
||||||
|
ss.changed = false
|
||||||
|
ss.size--
|
||||||
|
if ss.size == 0 {
|
||||||
|
ss.Empty()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the number of items in the set.
|
||||||
|
func (ss *SequenceSet) Size() int {
|
||||||
|
return ss.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes returns the number of nodes in the tree.
|
||||||
|
func (ss *SequenceSet) Nodes() int {
|
||||||
|
return ss.nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty will clear all items from a set.
|
||||||
|
func (ss *SequenceSet) Empty() {
|
||||||
|
ss.root = nil
|
||||||
|
ss.size = 0
|
||||||
|
ss.nodes = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty is a fast check of the set being empty.
|
||||||
|
func (ss *SequenceSet) IsEmpty() bool {
|
||||||
|
if ss == nil || ss.root == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range will invoke the given function for each item in the set.
|
||||||
|
// They will range over the set in ascending order.
|
||||||
|
// If the callback returns false we terminate the iteration.
|
||||||
|
func (ss *SequenceSet) Range(f func(uint64) bool) {
|
||||||
|
ss.root.iter(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heights returns the left and right heights of the tree.
|
||||||
|
func (ss *SequenceSet) Heights() (l, r int) {
|
||||||
|
if ss.root == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
if ss.root.l != nil {
|
||||||
|
l = ss.root.l.h
|
||||||
|
}
|
||||||
|
if ss.root.r != nil {
|
||||||
|
r = ss.root.r.h
|
||||||
|
}
|
||||||
|
return l, r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns min, max and number of set items.
|
||||||
|
func (ss *SequenceSet) State() (min, max, num uint64) {
|
||||||
|
if ss == nil || ss.root == nil {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
min, max = ss.MinMax()
|
||||||
|
return min, max, uint64(ss.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinMax will return the minunum and maximum values in the set.
|
||||||
|
func (ss *SequenceSet) MinMax() (min, max uint64) {
|
||||||
|
if ss.root == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
for l := ss.root; l != nil; l = l.l {
|
||||||
|
if l.l == nil {
|
||||||
|
min = l.min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for r := ss.root; r != nil; r = r.r {
|
||||||
|
if r.r == nil {
|
||||||
|
max = r.max()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min, max
|
||||||
|
}
|
||||||
|
|
||||||
|
func clone(src *node, target **node) {
|
||||||
|
if src == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := &node{base: src.base, bits: src.bits, h: src.h}
|
||||||
|
*target = n
|
||||||
|
clone(src.l, &n.l)
|
||||||
|
clone(src.r, &n.r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone will return a clone of the given SequenceSet.
|
||||||
|
func (ss *SequenceSet) Clone() *SequenceSet {
|
||||||
|
if ss == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
css := &SequenceSet{nodes: ss.nodes, size: ss.size}
|
||||||
|
clone(ss.root, &css.root)
|
||||||
|
|
||||||
|
return css
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns whether the two sets contain exactly the same sequences.
|
||||||
|
func (ss *SequenceSet) Equal(other *SequenceSet) bool {
|
||||||
|
if ss.IsEmpty() || other.IsEmpty() {
|
||||||
|
return ss.IsEmpty() && other.IsEmpty()
|
||||||
|
}
|
||||||
|
if ss.size != other.size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Sizes are equal, so a one-way membership check suffices.
|
||||||
|
equal := true
|
||||||
|
ss.Range(func(seq uint64) bool {
|
||||||
|
equal = other.Exists(seq)
|
||||||
|
return equal
|
||||||
|
})
|
||||||
|
return equal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union will union this SequenceSet with ssa.
|
||||||
|
func (ss *SequenceSet) Union(ssa ...*SequenceSet) {
|
||||||
|
for _, sa := range ssa {
|
||||||
|
sa.root.nodeIter(func(n *node) {
|
||||||
|
for nb, b := range n.bits {
|
||||||
|
base := n.base + uint64(nb)*bitsPerBucket
|
||||||
|
for b != 0 {
|
||||||
|
ss.Insert(base + uint64(bits.TrailingZeros64(b)))
|
||||||
|
b &= b - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union will return a union of all sets.
|
||||||
|
func Union(ssa ...*SequenceSet) *SequenceSet {
|
||||||
|
if len(ssa) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Sort so we can clone largest.
|
||||||
|
slices.SortFunc(ssa, func(i, j *SequenceSet) int { return -cmp.Compare(i.Size(), j.Size()) }) // reverse order
|
||||||
|
ss := ssa[0].Clone()
|
||||||
|
|
||||||
|
// Insert the rest through range call.
|
||||||
|
for i := 1; i < len(ssa); i++ {
|
||||||
|
ssa[i].Range(func(n uint64) bool {
|
||||||
|
ss.Insert(n)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Magic is used to identify the encode binary state..
|
||||||
|
magic = uint8(22)
|
||||||
|
// Version
|
||||||
|
version = uint8(2)
|
||||||
|
// hdrLen
|
||||||
|
hdrLen = 2
|
||||||
|
// minimum length of an encoded SequenceSet.
|
||||||
|
minLen = 2 + 8 // magic + version + num nodes + num entries.
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeLen returns the bytes needed for encoding.
|
||||||
|
func (ss SequenceSet) EncodeLen() int {
|
||||||
|
return minLen + (ss.Nodes() * ((numBuckets+1)*8 + 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss SequenceSet) Encode(buf []byte) []byte {
|
||||||
|
nn, encLen := ss.Nodes(), ss.EncodeLen()
|
||||||
|
|
||||||
|
if cap(buf) < encLen {
|
||||||
|
buf = make([]byte, encLen)
|
||||||
|
} else {
|
||||||
|
buf = buf[:encLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(dlc) - Go 1.19 introduced Append to not have to keep track.
|
||||||
|
// Once 1.20 is out we could change this over.
|
||||||
|
// Also binary.Write() is way slower, do not use.
|
||||||
|
|
||||||
|
var le = binary.LittleEndian
|
||||||
|
buf[0], buf[1] = magic, version
|
||||||
|
i := hdrLen
|
||||||
|
le.PutUint32(buf[i:], uint32(nn))
|
||||||
|
le.PutUint32(buf[i+4:], uint32(ss.size))
|
||||||
|
i += 8
|
||||||
|
ss.root.nodeIter(func(n *node) {
|
||||||
|
le.PutUint64(buf[i:], n.base)
|
||||||
|
i += 8
|
||||||
|
for _, b := range n.bits {
|
||||||
|
le.PutUint64(buf[i:], b)
|
||||||
|
i += 8
|
||||||
|
}
|
||||||
|
le.PutUint16(buf[i:], uint16(n.h))
|
||||||
|
i += 2
|
||||||
|
})
|
||||||
|
return buf[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadEncoding is returned when we can not decode properly.
|
||||||
|
var (
|
||||||
|
ErrBadEncoding = errors.New("ss: bad encoding")
|
||||||
|
ErrBadVersion = errors.New("ss: bad version")
|
||||||
|
ErrSetNotEmpty = errors.New("ss: set not empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode returns the sequence set and number of bytes read from the buffer on success.
|
||||||
|
func Decode(buf []byte) (*SequenceSet, int, error) {
|
||||||
|
if len(buf) < minLen || buf[0] != magic {
|
||||||
|
return nil, -1, ErrBadEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := buf[1]; v {
|
||||||
|
case 1:
|
||||||
|
return decodev1(buf)
|
||||||
|
case 2:
|
||||||
|
return decodev2(buf)
|
||||||
|
default:
|
||||||
|
return nil, -1, ErrBadVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to decode v2.
|
||||||
|
func decodev2(buf []byte) (*SequenceSet, int, error) {
|
||||||
|
var le = binary.LittleEndian
|
||||||
|
index := 2
|
||||||
|
nn := int(le.Uint32(buf[index:]))
|
||||||
|
sz := int(le.Uint32(buf[index+4:]))
|
||||||
|
index += 8
|
||||||
|
|
||||||
|
// nn is decoded as a uint32 but held in an int. On 32-bit builds a value
|
||||||
|
// above MaxInt32 turns negative and nn*perNode below overflows, so the
|
||||||
|
// length check would pass for a short buffer and the following make/reads
|
||||||
|
// run off the end. Compare with division so the bound holds on every arch.
|
||||||
|
if nn < 0 || nn > (len(buf)-minLen)/((numBuckets+1)*8+2) {
|
||||||
|
return nil, -1, ErrBadEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
ss, nodes := SequenceSet{size: sz}, make([]node, nn)
|
||||||
|
|
||||||
|
for i := 0; i < nn; i++ {
|
||||||
|
n := &nodes[i]
|
||||||
|
n.base = le.Uint64(buf[index:])
|
||||||
|
index += 8
|
||||||
|
for bi := range n.bits {
|
||||||
|
n.bits[bi] = le.Uint64(buf[index:])
|
||||||
|
index += 8
|
||||||
|
}
|
||||||
|
n.h = int(le.Uint16(buf[index:]))
|
||||||
|
index += 2
|
||||||
|
ss.insertNode(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ss, index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to decode v1 into v2 which has fixed buckets of 32 vs 64 originally.
|
||||||
|
func decodev1(buf []byte) (*SequenceSet, int, error) {
|
||||||
|
var le = binary.LittleEndian
|
||||||
|
index := 2
|
||||||
|
nn := int(le.Uint32(buf[index:]))
|
||||||
|
sz := int(le.Uint32(buf[index+4:]))
|
||||||
|
index += 8
|
||||||
|
|
||||||
|
const v1NumBuckets = 64
|
||||||
|
|
||||||
|
// See decodev2: guard the node count without overflowing the multiply so
|
||||||
|
// the bound stays correct on 32-bit builds too.
|
||||||
|
if nn < 0 || nn > (len(buf)-minLen)/((v1NumBuckets+1)*8+2) {
|
||||||
|
return nil, -1, ErrBadEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
var ss SequenceSet
|
||||||
|
for i := 0; i < nn; i++ {
|
||||||
|
base := le.Uint64(buf[index:])
|
||||||
|
index += 8
|
||||||
|
for nb := uint64(0); nb < v1NumBuckets; nb++ {
|
||||||
|
n := le.Uint64(buf[index:])
|
||||||
|
// Walk all set bits and insert sequences manually for this decode from v1.
|
||||||
|
for n != 0 {
|
||||||
|
ss.Insert(base + (nb * uint64(bitsPerBucket)) + uint64(bits.TrailingZeros64(n)))
|
||||||
|
n &= n - 1
|
||||||
|
}
|
||||||
|
index += 8
|
||||||
|
}
|
||||||
|
// Skip over encoded height.
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
if ss.Size() != sz {
|
||||||
|
return nil, -1, ErrBadEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ss, index, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertNode places a decoded node into the tree.
|
||||||
|
// These should be done in tree order as defined by Encode()
|
||||||
|
// This allows us to not have to calculate height or do rebalancing.
|
||||||
|
// So much better performance this way.
|
||||||
|
func (ss *SequenceSet) insertNode(n *node) {
|
||||||
|
ss.nodes++
|
||||||
|
|
||||||
|
if ss.root == nil {
|
||||||
|
ss.root = n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Walk our way to the insertion point.
|
||||||
|
for p := ss.root; p != nil; {
|
||||||
|
if n.base < p.base {
|
||||||
|
if p.l == nil {
|
||||||
|
p.l = n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p = p.l
|
||||||
|
} else {
|
||||||
|
if p.r == nil {
|
||||||
|
p.r = n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p = p.r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
bitsPerBucket = 64 // bits in uint64
|
||||||
|
numBuckets = 32
|
||||||
|
numEntries = numBuckets * bitsPerBucket
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
//v dvalue
|
||||||
|
base uint64
|
||||||
|
bits [numBuckets]uint64
|
||||||
|
l *node
|
||||||
|
r *node
|
||||||
|
h int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the proper bit.
|
||||||
|
// seq should have already been qualified and inserted should be non nil.
|
||||||
|
func (n *node) set(seq uint64, inserted *bool) {
|
||||||
|
seq -= n.base
|
||||||
|
i := seq / bitsPerBucket
|
||||||
|
mask := uint64(1) << (seq % bitsPerBucket)
|
||||||
|
if (n.bits[i] & mask) == 0 {
|
||||||
|
n.bits[i] |= mask
|
||||||
|
*inserted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) insert(seq uint64, inserted *bool, nodes *int) *node {
|
||||||
|
if n == nil {
|
||||||
|
base := (seq / numEntries) * numEntries
|
||||||
|
n := &node{base: base, h: 1}
|
||||||
|
n.set(seq, inserted)
|
||||||
|
*nodes++
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
if seq < n.base {
|
||||||
|
n.l = n.l.insert(seq, inserted, nodes)
|
||||||
|
} else if seq >= n.base+numEntries {
|
||||||
|
n.r = n.r.insert(seq, inserted, nodes)
|
||||||
|
} else {
|
||||||
|
n.set(seq, inserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
|
||||||
|
// Don't make a function, impacts performance.
|
||||||
|
if bf := balanceF(n); bf > 1 {
|
||||||
|
// Left unbalanced.
|
||||||
|
if balanceF(n.l) < 0 {
|
||||||
|
n.l = n.l.rotateL()
|
||||||
|
}
|
||||||
|
return n.rotateR()
|
||||||
|
} else if bf < -1 {
|
||||||
|
// Right unbalanced.
|
||||||
|
if balanceF(n.r) > 0 {
|
||||||
|
n.r = n.r.rotateR()
|
||||||
|
}
|
||||||
|
return n.rotateL()
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) rotateL() *node {
|
||||||
|
r := n.r
|
||||||
|
if r != nil {
|
||||||
|
n.r = r.l
|
||||||
|
r.l = n
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
r.h = maxH(r) + 1
|
||||||
|
} else {
|
||||||
|
n.r = nil
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) rotateR() *node {
|
||||||
|
l := n.l
|
||||||
|
if l != nil {
|
||||||
|
n.l = l.r
|
||||||
|
l.r = n
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
l.h = maxH(l) + 1
|
||||||
|
} else {
|
||||||
|
n.l = nil
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func balanceF(n *node) int {
|
||||||
|
if n == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var lh, rh int
|
||||||
|
if n.l != nil {
|
||||||
|
lh = n.l.h
|
||||||
|
}
|
||||||
|
if n.r != nil {
|
||||||
|
rh = n.r.h
|
||||||
|
}
|
||||||
|
return lh - rh
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxH(n *node) int {
|
||||||
|
if n == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var lh, rh int
|
||||||
|
if n.l != nil {
|
||||||
|
lh = n.l.h
|
||||||
|
}
|
||||||
|
if n.r != nil {
|
||||||
|
rh = n.r.h
|
||||||
|
}
|
||||||
|
if lh > rh {
|
||||||
|
return lh
|
||||||
|
}
|
||||||
|
return rh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the proper bit.
|
||||||
|
// seq should have already been qualified and deleted should be non nil.
|
||||||
|
// Will return true if this node is now empty.
|
||||||
|
func (n *node) clear(seq uint64, deleted *bool) bool {
|
||||||
|
seq -= n.base
|
||||||
|
i := seq / bitsPerBucket
|
||||||
|
mask := uint64(1) << (seq % bitsPerBucket)
|
||||||
|
if (n.bits[i] & mask) == 0 {
|
||||||
|
// Nothing cleared, and nodes in the tree are never empty,
|
||||||
|
// so no need to scan the buckets.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n.bits[i] &^= mask
|
||||||
|
*deleted = true
|
||||||
|
for _, b := range n.bits {
|
||||||
|
if b != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) delete(seq uint64, deleted *bool, nodes *int) *node {
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if seq < n.base {
|
||||||
|
n.l = n.l.delete(seq, deleted, nodes)
|
||||||
|
} else if seq >= n.base+numEntries {
|
||||||
|
n.r = n.r.delete(seq, deleted, nodes)
|
||||||
|
} else if empty := n.clear(seq, deleted); empty {
|
||||||
|
*nodes--
|
||||||
|
if n.l == nil {
|
||||||
|
n = n.r
|
||||||
|
} else if n.r == nil {
|
||||||
|
n = n.l
|
||||||
|
} else {
|
||||||
|
// We have both children.
|
||||||
|
n.r = n.r.insertNodePrev(n.l)
|
||||||
|
n = n.r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != nil {
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check balance.
|
||||||
|
if bf := balanceF(n); bf > 1 {
|
||||||
|
// Left unbalanced.
|
||||||
|
if balanceF(n.l) < 0 {
|
||||||
|
n.l = n.l.rotateL()
|
||||||
|
}
|
||||||
|
return n.rotateR()
|
||||||
|
} else if bf < -1 {
|
||||||
|
// right unbalanced.
|
||||||
|
if balanceF(n.r) > 0 {
|
||||||
|
n.r = n.r.rotateR()
|
||||||
|
}
|
||||||
|
return n.rotateL()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will insert nn into the node assuming it is less than all other nodes in n.
|
||||||
|
// Will re-calculate height and balance.
|
||||||
|
func (n *node) insertNodePrev(nn *node) *node {
|
||||||
|
if n.l == nil {
|
||||||
|
n.l = nn
|
||||||
|
} else {
|
||||||
|
n.l = n.l.insertNodePrev(nn)
|
||||||
|
}
|
||||||
|
n.h = maxH(n) + 1
|
||||||
|
|
||||||
|
// Check balance.
|
||||||
|
if bf := balanceF(n); bf > 1 {
|
||||||
|
// Left unbalanced.
|
||||||
|
if balanceF(n.l) < 0 {
|
||||||
|
n.l = n.l.rotateL()
|
||||||
|
}
|
||||||
|
return n.rotateR()
|
||||||
|
} else if bf < -1 {
|
||||||
|
// right unbalanced.
|
||||||
|
if balanceF(n.r) > 0 {
|
||||||
|
n.r = n.r.rotateR()
|
||||||
|
}
|
||||||
|
return n.rotateL()
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) exists(seq uint64) bool {
|
||||||
|
seq -= n.base
|
||||||
|
i := seq / bitsPerBucket
|
||||||
|
mask := uint64(1) << (seq % bitsPerBucket)
|
||||||
|
return n.bits[i]&mask != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return minimum sequence in the set.
|
||||||
|
// This node can not be empty.
|
||||||
|
func (n *node) min() uint64 {
|
||||||
|
for i, b := range n.bits {
|
||||||
|
if b != 0 {
|
||||||
|
return n.base +
|
||||||
|
uint64(i*bitsPerBucket) +
|
||||||
|
uint64(bits.TrailingZeros64(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return maximum sequence in the set.
|
||||||
|
// This node can not be empty.
|
||||||
|
func (n *node) max() uint64 {
|
||||||
|
for i := numBuckets - 1; i >= 0; i-- {
|
||||||
|
if b := n.bits[i]; b != 0 {
|
||||||
|
return n.base +
|
||||||
|
uint64(i*bitsPerBucket) +
|
||||||
|
uint64(bitsPerBucket-bits.LeadingZeros64(b>>1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is done in tree order.
|
||||||
|
func (n *node) nodeIter(f func(n *node)) {
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f(n)
|
||||||
|
n.l.nodeIter(f)
|
||||||
|
n.r.nodeIter(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iter will iterate through the set's items in this node.
|
||||||
|
// If the supplied function returns false we terminate the iteration.
|
||||||
|
func (n *node) iter(f func(uint64) bool) bool {
|
||||||
|
if n == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := n.l.iter(f); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, b := range n.bits {
|
||||||
|
base := n.base + uint64(i)*bitsPerBucket
|
||||||
|
for b != 0 {
|
||||||
|
if ok := f(base + uint64(bits.TrailingZeros64(b))); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b &= b - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok := n.r.iter(f); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
556
server/avl/seqset_test.go
Normal file
556
server/avl/seqset_test.go
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
// Copyright 2023-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package avl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand/v2"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeqSetBasics(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
seqs := []uint64{22, 222, 2000, 2, 2, 4}
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Insert(seq)
|
||||||
|
require_True(t, ss.Exists(seq))
|
||||||
|
}
|
||||||
|
|
||||||
|
require_True(t, ss.Nodes() == 1)
|
||||||
|
require_True(t, ss.Size() == len(seqs)-1) // We have one dup in there.
|
||||||
|
lh, rh := ss.Heights()
|
||||||
|
require_True(t, lh == 0)
|
||||||
|
require_True(t, rh == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetLeftLean(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
for i := uint64(4 * numEntries); i > 0; i-- {
|
||||||
|
ss.Insert(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
require_True(t, ss.Nodes() == 5)
|
||||||
|
require_True(t, ss.Size() == 4*numEntries)
|
||||||
|
lh, rh := ss.Heights()
|
||||||
|
require_True(t, lh == 2)
|
||||||
|
require_True(t, rh == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetRightLean(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
for i := uint64(0); i < uint64(4*numEntries); i++ {
|
||||||
|
ss.Insert(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
require_True(t, ss.Nodes() == 4)
|
||||||
|
require_True(t, ss.Size() == 4*numEntries)
|
||||||
|
lh, rh := ss.Heights()
|
||||||
|
require_True(t, lh == 1)
|
||||||
|
require_True(t, rh == 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetCorrectness(t *testing.T) {
|
||||||
|
// Generate 100k sequences across 500k range.
|
||||||
|
num := 100_000
|
||||||
|
max := 500_000
|
||||||
|
|
||||||
|
set := make(map[uint64]struct{}, num)
|
||||||
|
var ss SequenceSet
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
n := uint64(rand.Int64N(int64(max + 1)))
|
||||||
|
ss.Insert(n)
|
||||||
|
set[n] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint64(0); i <= uint64(max); i++ {
|
||||||
|
_, exists := set[i]
|
||||||
|
require_True(t, ss.Exists(i) == exists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetRange(t *testing.T) {
|
||||||
|
num := 2*numEntries + 22
|
||||||
|
nums := make([]uint64, 0, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
nums = append(nums, uint64(i))
|
||||||
|
}
|
||||||
|
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
|
||||||
|
|
||||||
|
var ss SequenceSet
|
||||||
|
for _, n := range nums {
|
||||||
|
ss.Insert(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
nums = nums[:0]
|
||||||
|
ss.Range(func(n uint64) bool {
|
||||||
|
nums = append(nums, n)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require_True(t, len(nums) == num)
|
||||||
|
for i := uint64(0); i < uint64(num); i++ {
|
||||||
|
require_True(t, nums[i] == i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test truncating the range call.
|
||||||
|
nums = nums[:0]
|
||||||
|
ss.Range(func(n uint64) bool {
|
||||||
|
if n >= 10 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nums = append(nums, n)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require_True(t, len(nums) == 10)
|
||||||
|
for i := uint64(0); i < 10; i++ {
|
||||||
|
require_True(t, nums[i] == i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetDelete(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
// Simple single node.
|
||||||
|
seqs := []uint64{22, 222, 2222, 2, 2, 4}
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Delete(seq)
|
||||||
|
require_True(t, !ss.Exists(seq))
|
||||||
|
}
|
||||||
|
require_True(t, ss.root == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetInsertAndDeletePedantic(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
num := 50*numEntries + 22
|
||||||
|
nums := make([]uint64, 0, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
nums = append(nums, uint64(i))
|
||||||
|
}
|
||||||
|
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
|
||||||
|
|
||||||
|
// Make sure always balanced.
|
||||||
|
testBalanced := func() {
|
||||||
|
t.Helper()
|
||||||
|
// Check heights.
|
||||||
|
ss.root.nodeIter(func(n *node) {
|
||||||
|
if n != nil && n.h != maxH(n)+1 {
|
||||||
|
t.Fatalf("Node height is wrong: %+v", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Check balance factor.
|
||||||
|
if bf := balanceF(ss.root); bf > 1 || bf < -1 {
|
||||||
|
t.Fatalf("Unbalanced tree")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range nums {
|
||||||
|
ss.Insert(n)
|
||||||
|
testBalanced()
|
||||||
|
}
|
||||||
|
require_True(t, ss.root != nil)
|
||||||
|
|
||||||
|
for _, n := range nums {
|
||||||
|
ss.Delete(n)
|
||||||
|
testBalanced()
|
||||||
|
require_True(t, !ss.Exists(n))
|
||||||
|
if ss.Size() > 0 {
|
||||||
|
require_True(t, ss.root != nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require_True(t, ss.root == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetMinMax(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
// Simple single node.
|
||||||
|
seqs := []uint64{22, 222, 2222, 2, 2, 4}
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max := ss.MinMax()
|
||||||
|
require_True(t, min == 2 && max == 2222)
|
||||||
|
|
||||||
|
// Multi-node
|
||||||
|
ss.Empty()
|
||||||
|
|
||||||
|
num := 22*numEntries + 22
|
||||||
|
nums := make([]uint64, 0, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
nums = append(nums, uint64(i))
|
||||||
|
}
|
||||||
|
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
|
||||||
|
for _, n := range nums {
|
||||||
|
ss.Insert(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max = ss.MinMax()
|
||||||
|
require_True(t, min == 0 && max == uint64(num-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetClone(t *testing.T) {
|
||||||
|
// Generate 100k sequences across 500k range.
|
||||||
|
num := 100_000
|
||||||
|
max := 500_000
|
||||||
|
|
||||||
|
var ss SequenceSet
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
ss.Insert(uint64(rand.Int64N(int64(max + 1))))
|
||||||
|
}
|
||||||
|
|
||||||
|
ssc := ss.Clone()
|
||||||
|
require_True(t, ss.Size() == ssc.Size())
|
||||||
|
require_True(t, ss.Nodes() == ssc.Nodes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetEqual(t *testing.T) {
|
||||||
|
var ss1, ss2, ss3 SequenceSet
|
||||||
|
|
||||||
|
// Empty and nil sets are all equal.
|
||||||
|
var nilSet *SequenceSet
|
||||||
|
require_True(t, ss1.Equal(&ss2))
|
||||||
|
require_True(t, ss1.Equal(nilSet))
|
||||||
|
require_True(t, nilSet.Equal(&ss1))
|
||||||
|
|
||||||
|
// Same state (first, last, size) but different contents.
|
||||||
|
for _, seq := range []uint64{1, 3, 5} {
|
||||||
|
ss1.Insert(seq)
|
||||||
|
}
|
||||||
|
for _, seq := range []uint64{1, 4, 5} {
|
||||||
|
ss2.Insert(seq)
|
||||||
|
}
|
||||||
|
require_False(t, ss1.Equal(&ss2))
|
||||||
|
require_False(t, ss2.Equal(&ss1))
|
||||||
|
require_False(t, ss1.Equal(nilSet))
|
||||||
|
require_False(t, nilSet.Equal(&ss1))
|
||||||
|
|
||||||
|
// Identical contents, insertion order should not matter.
|
||||||
|
for _, seq := range []uint64{5, 1, 3} {
|
||||||
|
ss3.Insert(seq)
|
||||||
|
}
|
||||||
|
require_True(t, ss1.Equal(&ss3))
|
||||||
|
require_True(t, ss3.Equal(&ss1))
|
||||||
|
|
||||||
|
// Differing sizes.
|
||||||
|
ss3.Insert(7)
|
||||||
|
require_False(t, ss1.Equal(&ss3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetUnion(t *testing.T) {
|
||||||
|
var ss1, ss2 SequenceSet
|
||||||
|
|
||||||
|
seqs1 := []uint64{22, 222, 2222, 2, 2, 4}
|
||||||
|
for _, seq := range seqs1 {
|
||||||
|
ss1.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
seqs2 := []uint64{33, 333, 3333, 3, 33_333, 333_333}
|
||||||
|
for _, seq := range seqs2 {
|
||||||
|
ss2.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := Union(&ss1, &ss2)
|
||||||
|
require_True(t, ss.Size() == 11)
|
||||||
|
|
||||||
|
seqs := append(seqs1, seqs2...)
|
||||||
|
for _, n := range seqs {
|
||||||
|
require_True(t, ss.Exists(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetFirst(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
seqs := []uint64{22, 222, 2222, 222_222}
|
||||||
|
for _, seq := range seqs {
|
||||||
|
// Normal case where we pick first/base.
|
||||||
|
ss.Insert(seq)
|
||||||
|
require_True(t, ss.root.base == (seq/numEntries)*numEntries)
|
||||||
|
ss.Empty()
|
||||||
|
// Where we set the minimum start value.
|
||||||
|
ss.SetInitialMin(seq)
|
||||||
|
ss.Insert(seq)
|
||||||
|
require_True(t, ss.root.base == seq)
|
||||||
|
ss.Empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can union with nodes vs individual sequence insertion.
|
||||||
|
func TestSeqSetDistinctUnion(t *testing.T) {
|
||||||
|
// Distinct sets.
|
||||||
|
var ss1 SequenceSet
|
||||||
|
seqs1 := []uint64{1, 10, 100, 200}
|
||||||
|
for _, seq := range seqs1 {
|
||||||
|
ss1.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ss2 SequenceSet
|
||||||
|
seqs2 := []uint64{5000, 6100, 6200, 6222}
|
||||||
|
for _, seq := range seqs2 {
|
||||||
|
ss2.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := ss1.Clone()
|
||||||
|
allSeqs := append(seqs1, seqs2...)
|
||||||
|
|
||||||
|
ss.Union(&ss2)
|
||||||
|
require_True(t, ss.Size() == len(allSeqs))
|
||||||
|
for _, seq := range allSeqs {
|
||||||
|
require_True(t, ss.Exists(seq))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetDecodeV1(t *testing.T) {
|
||||||
|
// Encoding from v1 which was 64 buckets.
|
||||||
|
seqs := []uint64{22, 222, 2222, 222_222, 2_222_222}
|
||||||
|
encStr := `
|
||||||
|
FgEDAAAABQAAAABgAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADgIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA==
|
||||||
|
`
|
||||||
|
|
||||||
|
enc, err := base64.StdEncoding.DecodeString(encStr)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
ss, _, err := Decode(enc)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
require_True(t, ss.Size() == len(seqs))
|
||||||
|
for _, seq := range seqs {
|
||||||
|
require_True(t, ss.Exists(seq))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetDecodeNodeCountOverflow(t *testing.T) {
|
||||||
|
// A replicated stream state carries an avl-encoded delete map decoded from
|
||||||
|
// a peer. The node count is read as a uint32 into an int; on 32-bit builds
|
||||||
|
// a value above MaxInt32 turns negative (and a smaller value can overflow
|
||||||
|
// the nn*perNode multiply), defeating the length check so the decode then
|
||||||
|
// panics in make([]node, nn) or reads past the buffer. Both encodings must
|
||||||
|
// reject such counts on every architecture instead.
|
||||||
|
le := binary.LittleEndian
|
||||||
|
for _, ver := range []uint8{1, 2} {
|
||||||
|
buf := make([]byte, minLen)
|
||||||
|
buf[0], buf[1] = magic, ver
|
||||||
|
le.PutUint32(buf[2:], 0xFFFFFFFF) // node count, negative as a 32-bit int
|
||||||
|
if _, _, err := Decode(buf); err != ErrBadEncoding {
|
||||||
|
t.Fatalf("v%d: expected ErrBadEncoding for oversized node count, got %v", ver, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive on 32-bit but the multiply still overflows for a short buffer.
|
||||||
|
le.PutUint32(buf[2:], 0x00800000)
|
||||||
|
if _, _, err := Decode(buf); err != ErrBadEncoding {
|
||||||
|
t.Fatalf("v%d: expected ErrBadEncoding for overflowing node count, got %v", ver, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A well-formed single-node v2 buffer must still decode.
|
||||||
|
valid := make([]byte, minLen+(numBuckets+1)*8+2)
|
||||||
|
valid[0], valid[1] = magic, version
|
||||||
|
le.PutUint32(valid[2:], 1)
|
||||||
|
if _, _, err := Decode(valid); err != nil {
|
||||||
|
t.Fatalf("valid single-node buffer should decode, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetInsertExistingNodeFastPath(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
// Build a multi-node tree with only even sequences set.
|
||||||
|
for i := uint64(0); i < 8*numEntries; i += 2 {
|
||||||
|
ss.Insert(i)
|
||||||
|
}
|
||||||
|
nodes, size := ss.Nodes(), ss.Size()
|
||||||
|
|
||||||
|
// Inserting into existing nodes must not change the tree structure.
|
||||||
|
for i := uint64(1); i < 8*numEntries; i += 2 {
|
||||||
|
ss.Insert(i)
|
||||||
|
require_True(t, ss.Exists(i))
|
||||||
|
}
|
||||||
|
require_True(t, ss.Nodes() == nodes)
|
||||||
|
require_True(t, ss.Size() == size+4*numEntries)
|
||||||
|
|
||||||
|
// Duplicate inserts must not change the size.
|
||||||
|
for i := uint64(0); i < 8*numEntries; i++ {
|
||||||
|
ss.Insert(i)
|
||||||
|
}
|
||||||
|
require_True(t, ss.Size() == 8*numEntries)
|
||||||
|
|
||||||
|
// Heights and balance must still be intact.
|
||||||
|
ss.root.nodeIter(func(n *node) {
|
||||||
|
if n.h != maxH(n)+1 {
|
||||||
|
t.Fatalf("Node height is wrong: %+v", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if bf := balanceF(ss.root); bf > 1 || bf < -1 {
|
||||||
|
t.Fatalf("Unbalanced tree")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetInsertUnalignedBase(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
// SetInitialMin can create a node whose base is not aligned to numEntries.
|
||||||
|
require_NoError(t, ss.SetInitialMin(1000))
|
||||||
|
for _, seq := range []uint64{1000, 1001, 1000 + numEntries - 1} {
|
||||||
|
ss.Insert(seq)
|
||||||
|
require_True(t, ss.Exists(seq))
|
||||||
|
}
|
||||||
|
require_True(t, ss.Nodes() == 1)
|
||||||
|
require_True(t, ss.Size() == 3)
|
||||||
|
|
||||||
|
// Outside the node's range still creates a new node.
|
||||||
|
ss.Insert(1000 + numEntries)
|
||||||
|
require_True(t, ss.Exists(1000+numEntries))
|
||||||
|
require_True(t, ss.Nodes() == 2)
|
||||||
|
require_True(t, ss.Size() == 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetDeleteNotPresent(t *testing.T) {
|
||||||
|
var ss SequenceSet
|
||||||
|
|
||||||
|
seqs := []uint64{22, 222, 2222, 222_222}
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Insert(seq)
|
||||||
|
}
|
||||||
|
nodes, size := ss.Nodes(), ss.Size()
|
||||||
|
|
||||||
|
// Deleting sequences that are not set, both inside and outside
|
||||||
|
// existing node ranges, must be a no-op and report false.
|
||||||
|
for _, seq := range []uint64{0, 23, 2223, 5000, 100_000, 999_999} {
|
||||||
|
require_True(t, !ss.Delete(seq))
|
||||||
|
}
|
||||||
|
require_True(t, ss.Nodes() == nodes)
|
||||||
|
require_True(t, ss.Size() == size)
|
||||||
|
for _, seq := range seqs {
|
||||||
|
require_True(t, ss.Exists(seq))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real deletes still work and empty the set.
|
||||||
|
for _, seq := range seqs {
|
||||||
|
require_True(t, ss.Delete(seq))
|
||||||
|
}
|
||||||
|
require_True(t, ss.root == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeqSetRangeSparse(t *testing.T) {
|
||||||
|
// Cover bucket boundaries, node boundaries and large gaps.
|
||||||
|
seqs := []uint64{0, 1, 63, 64, 127, 128, 2047, 2048, 4095, 4096, 100_000, 1_000_000}
|
||||||
|
var ss SequenceSet
|
||||||
|
for _, seq := range seqs {
|
||||||
|
ss.Insert(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
var got []uint64
|
||||||
|
ss.Range(func(n uint64) bool {
|
||||||
|
got = append(got, n)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require_True(t, len(got) == len(seqs))
|
||||||
|
for i, seq := range seqs {
|
||||||
|
require_True(t, got[i] == seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminating mid-bucket should stop the iteration immediately.
|
||||||
|
got = got[:0]
|
||||||
|
ss.Range(func(n uint64) bool {
|
||||||
|
got = append(got, n)
|
||||||
|
return n != 64
|
||||||
|
})
|
||||||
|
require_True(t, len(got) == 4)
|
||||||
|
require_True(t, got[3] == 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSeqSetInsertDense(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var ss SequenceSet
|
||||||
|
for s := uint64(0); s < 200_000; s++ {
|
||||||
|
ss.Insert(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSeqSetInsertSparse(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var ss SequenceSet
|
||||||
|
for s := uint64(0); s < 200_000; s++ {
|
||||||
|
ss.Insert(s * 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSeqSetRangeDense(b *testing.B) {
|
||||||
|
var ss SequenceSet
|
||||||
|
for s := uint64(0); s < 200_000; s++ {
|
||||||
|
ss.Insert(s)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var sum uint64
|
||||||
|
ss.Range(func(n uint64) bool { sum += n; return true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSeqSetRangeSparse(b *testing.B) {
|
||||||
|
var ss SequenceSet
|
||||||
|
for s := uint64(0); s < 10_000; s++ {
|
||||||
|
ss.Insert(s * 211) // ~10 bits per 2048-entry node
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var sum uint64
|
||||||
|
ss.Range(func(n uint64) bool { sum += n; return true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSeqSetDeleteNotPresent(b *testing.B) {
|
||||||
|
var ss SequenceSet
|
||||||
|
for s := uint64(0); s < 200_000; s += 2 {
|
||||||
|
ss.Insert(s)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for s := uint64(1); s < 200_000; s += 2 {
|
||||||
|
ss.Delete(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func require_NoError(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("require no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func require_True(t *testing.T, b bool) {
|
||||||
|
t.Helper()
|
||||||
|
if !b {
|
||||||
|
t.Fatalf("require true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func require_False(t *testing.T, b bool) {
|
||||||
|
t.Helper()
|
||||||
|
if b {
|
||||||
|
t.Fatalf("require false")
|
||||||
|
}
|
||||||
|
}
|
||||||
185
server/benchmark_publish_test.go
Normal file
185
server/benchmark_publish_test.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// Copyright 2022-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkPublish(b *testing.B) {
|
||||||
|
|
||||||
|
const (
|
||||||
|
verbose = false
|
||||||
|
minMessages = 10_000
|
||||||
|
subject = "S"
|
||||||
|
queue = "Q"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KB = 1024
|
||||||
|
MB = KB * KB
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubscriberType string
|
||||||
|
const (
|
||||||
|
Async SubscriberType = "Async"
|
||||||
|
QueueAsync SubscriberType = "AsyncQueue"
|
||||||
|
None SubscriberType = "None"
|
||||||
|
)
|
||||||
|
|
||||||
|
benchmarksCases := []struct {
|
||||||
|
messageSize int
|
||||||
|
}{
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{32},
|
||||||
|
{128},
|
||||||
|
{512},
|
||||||
|
{4 * KB},
|
||||||
|
{32 * KB},
|
||||||
|
{128 * KB},
|
||||||
|
{512 * KB},
|
||||||
|
{1 * MB},
|
||||||
|
}
|
||||||
|
|
||||||
|
// All the cases above are run for each of the subscriber cases below
|
||||||
|
subscribersCases := []struct {
|
||||||
|
numSubs int
|
||||||
|
subType SubscriberType
|
||||||
|
}{
|
||||||
|
{0, None},
|
||||||
|
{1, Async},
|
||||||
|
{1, QueueAsync},
|
||||||
|
{10, Async},
|
||||||
|
{10, QueueAsync},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bc := range benchmarksCases {
|
||||||
|
bcName := fmt.Sprintf(
|
||||||
|
"MsgSz=%db",
|
||||||
|
bc.messageSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
b.Run(
|
||||||
|
bcName,
|
||||||
|
func(b *testing.B) {
|
||||||
|
|
||||||
|
for _, sc := range subscribersCases {
|
||||||
|
|
||||||
|
scName := fmt.Sprintf("Subs=%dx%v", sc.numSubs, sc.subType)
|
||||||
|
if sc.subType == None {
|
||||||
|
scName = fmt.Sprintf("Subs=%v", sc.subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run(
|
||||||
|
scName,
|
||||||
|
func(b *testing.B) {
|
||||||
|
// Skip short runs, benchmark gets re-executed with a larger N
|
||||||
|
if b.N < minMessages {
|
||||||
|
b.ResetTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
b.Logf("Running %s/%s with %d ops", bcName, scName, b.N)
|
||||||
|
}
|
||||||
|
|
||||||
|
subErrors := uint64(0)
|
||||||
|
handleSubError := func(_ *nats.Conn, _ *nats.Subscription, _ error) {
|
||||||
|
atomic.AddUint64(&subErrors, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start single server (no JS)
|
||||||
|
opts := DefaultTestOptions
|
||||||
|
opts.Port = -1
|
||||||
|
s := RunServer(&opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
// Create subscribers
|
||||||
|
for i := 0; i < sc.numSubs; i++ {
|
||||||
|
subConn, connErr := nats.Connect(s.ClientURL(), nats.ErrorHandler(handleSubError))
|
||||||
|
if connErr != nil {
|
||||||
|
b.Fatalf("Failed to connect: %v", connErr)
|
||||||
|
}
|
||||||
|
defer subConn.Close()
|
||||||
|
|
||||||
|
var sub *nats.Subscription
|
||||||
|
var subErr error
|
||||||
|
|
||||||
|
switch sc.subType {
|
||||||
|
case None:
|
||||||
|
// No subscription
|
||||||
|
case Async:
|
||||||
|
sub, subErr = subConn.Subscribe(subject, func(*nats.Msg) {})
|
||||||
|
case QueueAsync:
|
||||||
|
sub, subErr = subConn.QueueSubscribe(subject, queue, func(*nats.Msg) {})
|
||||||
|
default:
|
||||||
|
b.Fatalf("Unknow subscribers type: %v", sc.subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if subErr != nil {
|
||||||
|
b.Fatalf("Failed to subscribe: %v", subErr)
|
||||||
|
}
|
||||||
|
defer sub.Unsubscribe()
|
||||||
|
// Do not drop messages due to slow subscribers:
|
||||||
|
sub.SetPendingLimits(-1, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create publisher connection
|
||||||
|
nc, err := nats.Connect(s.ClientURL())
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to connect: %v", err)
|
||||||
|
}
|
||||||
|
defer nc.Close()
|
||||||
|
|
||||||
|
rng := rand.NewChaCha8([32]byte{42})
|
||||||
|
message := make([]byte, bc.messageSize)
|
||||||
|
var published, errors int
|
||||||
|
|
||||||
|
// Benchmark starts here
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
rng.Read(message)
|
||||||
|
pubErr := nc.Publish(subject, message)
|
||||||
|
if pubErr != nil {
|
||||||
|
errors++
|
||||||
|
} else {
|
||||||
|
published++
|
||||||
|
b.SetBytes(int64(bc.messageSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark ends here
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
if published+errors != b.N {
|
||||||
|
b.Fatalf("Something doesn't add up: %d + %d != %d", published, errors, b.N)
|
||||||
|
} else if subErrors > 0 {
|
||||||
|
b.Fatalf("Subscribers errors: %d", subErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ReportMetric(float64(errors)*100/float64(b.N), "%error")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
312
server/certidp/certidp.go
Normal file
312
server/certidp/certidp.go
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
// Copyright 2023-2024 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package certidp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultAllowedClockSkew = 30 * time.Second
|
||||||
|
DefaultOCSPResponderTimeout = 2 * time.Second
|
||||||
|
DefaultTTLUnsetNextUpdate = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusAssertion int
|
||||||
|
|
||||||
|
var (
|
||||||
|
StatusAssertionStrToVal = map[string]StatusAssertion{
|
||||||
|
"good": ocsp.Good,
|
||||||
|
"revoked": ocsp.Revoked,
|
||||||
|
"unknown": ocsp.Unknown,
|
||||||
|
}
|
||||||
|
StatusAssertionValToStr = map[StatusAssertion]string{
|
||||||
|
ocsp.Good: "good",
|
||||||
|
ocsp.Revoked: "revoked",
|
||||||
|
ocsp.Unknown: "unknown",
|
||||||
|
}
|
||||||
|
StatusAssertionIntToVal = map[int]StatusAssertion{
|
||||||
|
0: ocsp.Good,
|
||||||
|
1: ocsp.Revoked,
|
||||||
|
2: ocsp.Unknown,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetStatusAssertionStr returns the corresponding string representation of the StatusAssertion.
|
||||||
|
func GetStatusAssertionStr(sa int) string {
|
||||||
|
// If the provided status assertion value is not found in the map (StatusAssertionIntToVal),
|
||||||
|
// the function defaults to "unknown" to avoid defaulting to "good," which is the default iota value
|
||||||
|
// for the ocsp.StatusAssertion enumeration (https://pkg.go.dev/golang.org/x/crypto/ocsp#pkg-constants).
|
||||||
|
// This ensures that we don't unintentionally default to "good" when there's no map entry.
|
||||||
|
v, ok := StatusAssertionIntToVal[sa]
|
||||||
|
if !ok {
|
||||||
|
// set unknown as fallback
|
||||||
|
v = ocsp.Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusAssertionValToStr[v]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa StatusAssertion) MarshalJSON() ([]byte, error) {
|
||||||
|
// This ensures that we don't unintentionally default to "good" when there's no map entry.
|
||||||
|
// (see more details in the GetStatusAssertionStr() comment)
|
||||||
|
str, ok := StatusAssertionValToStr[sa]
|
||||||
|
if !ok {
|
||||||
|
// set unknown as fallback
|
||||||
|
str = StatusAssertionValToStr[ocsp.Unknown]
|
||||||
|
}
|
||||||
|
return json.Marshal(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *StatusAssertion) UnmarshalJSON(in []byte) error {
|
||||||
|
// This ensures that we don't unintentionally default to "good" when there's no map entry.
|
||||||
|
// (see more details in the GetStatusAssertionStr() comment)
|
||||||
|
v, ok := StatusAssertionStrToVal[strings.ReplaceAll(string(in), "\"", "")]
|
||||||
|
if !ok {
|
||||||
|
// set unknown as fallback
|
||||||
|
v = StatusAssertionStrToVal["unknown"]
|
||||||
|
}
|
||||||
|
*sa = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChainLink struct {
|
||||||
|
Leaf *x509.Certificate
|
||||||
|
Issuer *x509.Certificate
|
||||||
|
OCSPWebEndpoints *[]*url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCSPPeerConfig holds the parsed OCSP peer configuration section of TLS configuration
|
||||||
|
type OCSPPeerConfig struct {
|
||||||
|
Verify bool
|
||||||
|
Timeout float64
|
||||||
|
ClockSkew float64
|
||||||
|
WarnOnly bool
|
||||||
|
UnknownIsGood bool
|
||||||
|
AllowWhenCAUnreachable bool
|
||||||
|
TTLUnsetNextUpdate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCSPPeerConfig() *OCSPPeerConfig {
|
||||||
|
return &OCSPPeerConfig{
|
||||||
|
Verify: false,
|
||||||
|
Timeout: DefaultOCSPResponderTimeout.Seconds(),
|
||||||
|
ClockSkew: DefaultAllowedClockSkew.Seconds(),
|
||||||
|
WarnOnly: false,
|
||||||
|
UnknownIsGood: false,
|
||||||
|
AllowWhenCAUnreachable: false,
|
||||||
|
TTLUnsetNextUpdate: DefaultTTLUnsetNextUpdate.Seconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log is a neutral method of passing server loggers to plugins
|
||||||
|
type Log struct {
|
||||||
|
Debugf func(format string, v ...any)
|
||||||
|
Noticef func(format string, v ...any)
|
||||||
|
Warnf func(format string, v ...any)
|
||||||
|
Errorf func(format string, v ...any)
|
||||||
|
Tracef func(format string, v ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertInfo struct {
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Issuer string `json:"issuer,omitempty"`
|
||||||
|
Fingerprint string `json:"fingerprint,omitempty"`
|
||||||
|
Raw []byte `json:"raw,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var OCSPPeerUsage = `
|
||||||
|
For client, leaf spoke (remotes), and leaf hub connections, you may enable OCSP peer validation:
|
||||||
|
|
||||||
|
tls {
|
||||||
|
...
|
||||||
|
# mTLS must be enabled (with exception of Leaf remotes)
|
||||||
|
verify: true
|
||||||
|
...
|
||||||
|
# short form enables peer verify and takes option defaults
|
||||||
|
ocsp_peer: true
|
||||||
|
|
||||||
|
# long form includes settable options
|
||||||
|
ocsp_peer {
|
||||||
|
# Enable OCSP peer validation (default false)
|
||||||
|
verify: true
|
||||||
|
|
||||||
|
# OCSP responder timeout in seconds (may be fractional, default 2 seconds)
|
||||||
|
ca_timeout: 2
|
||||||
|
|
||||||
|
# Allowed skew between server and OCSP responder time in seconds (may be fractional, default 30 seconds)
|
||||||
|
allowed_clockskew: 30
|
||||||
|
|
||||||
|
# Warn-only and never reject connections (default false)
|
||||||
|
warn_only: false
|
||||||
|
|
||||||
|
# Treat response Unknown status as valid certificate (default false)
|
||||||
|
unknown_is_good: false
|
||||||
|
|
||||||
|
# Warn-only if no CA response can be obtained and no cached revocation exists (default false)
|
||||||
|
allow_when_ca_unreachable: false
|
||||||
|
|
||||||
|
# If response NextUpdate unset by CA, set a default cache TTL in seconds from ThisUpdate (default 1 hour)
|
||||||
|
cache_ttl_when_next_update_unset: 3600
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
Note: OCSP validation for route and gateway connections is enabled using the 'ocsp' configuration option.
|
||||||
|
`
|
||||||
|
|
||||||
|
// GenerateFingerprint returns a base64-encoded SHA256 hash of the raw certificate
|
||||||
|
func GenerateFingerprint(cert *x509.Certificate) string {
|
||||||
|
data := sha256.Sum256(cert.Raw)
|
||||||
|
return base64.StdEncoding.EncodeToString(data[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWebEndpoints(uris []string) []*url.URL {
|
||||||
|
var urls []*url.URL
|
||||||
|
for _, uri := range uris {
|
||||||
|
endpoint, err := url.ParseRequestURI(uri)
|
||||||
|
if err != nil {
|
||||||
|
// skip invalid URLs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if endpoint.Scheme != "http" && endpoint.Scheme != "https" {
|
||||||
|
// skip non-web URLs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
urls = append(urls, endpoint)
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubjectDNForm returns RDN sequence concatenation of the certificate's subject to be
|
||||||
|
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
|
||||||
|
func GetSubjectDNForm(cert *x509.Certificate) string {
|
||||||
|
if cert == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Subject.ToRDNSequence()), "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssuerDNForm returns RDN sequence concatenation of the certificate's issuer to be
|
||||||
|
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
|
||||||
|
func GetIssuerDNForm(cert *x509.Certificate) string {
|
||||||
|
if cert == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Issuer.ToRDNSequence()), "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertOCSPEligible checks if the certificate's issuer has populated AIA with OCSP responder endpoint(s)
|
||||||
|
// and is thus eligible for OCSP validation
|
||||||
|
func CertOCSPEligible(link *ChainLink) bool {
|
||||||
|
if link == nil || link.Leaf.Raw == nil || len(link.Leaf.Raw) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(link.Leaf.OCSPServer) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
urls := getWebEndpoints(link.Leaf.OCSPServer)
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
link.OCSPWebEndpoints = &urls
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeafIssuerCert returns the issuer certificate of the leaf (positional) certificate in the chain
|
||||||
|
func GetLeafIssuerCert(chain []*x509.Certificate, leafPos int) *x509.Certificate {
|
||||||
|
if len(chain) == 0 || leafPos < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// self-signed certificate or too-big leafPos
|
||||||
|
if leafPos >= len(chain)-1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// returns pointer to issuer cert or nil
|
||||||
|
return (chain)[leafPos+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCSPResponseCurrent checks if the OCSP response is current (i.e. not expired and not future effective)
|
||||||
|
func OCSPResponseCurrent(ocspr *ocsp.Response, opts *OCSPPeerConfig, log *Log) bool {
|
||||||
|
skew := time.Duration(opts.ClockSkew * float64(time.Second))
|
||||||
|
if skew < 0*time.Second {
|
||||||
|
skew = DefaultAllowedClockSkew
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
// Typical effectivity check based on CA response ThisUpdate and NextUpdate semantics
|
||||||
|
if !ocspr.NextUpdate.IsZero() && ocspr.NextUpdate.Before(now.Add(-1*skew)) {
|
||||||
|
t := ocspr.NextUpdate.Format(time.RFC3339Nano)
|
||||||
|
nt := now.Format(time.RFC3339Nano)
|
||||||
|
log.Debugf(DbgResponseExpired, t, nt, skew)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// CA responder can assert NextUpdate unset, in which case use config option to set a default cache TTL
|
||||||
|
if ocspr.NextUpdate.IsZero() {
|
||||||
|
ttl := time.Duration(opts.TTLUnsetNextUpdate * float64(time.Second))
|
||||||
|
if ttl < 0*time.Second {
|
||||||
|
ttl = DefaultTTLUnsetNextUpdate
|
||||||
|
}
|
||||||
|
expiryTime := ocspr.ThisUpdate.Add(ttl)
|
||||||
|
if expiryTime.Before(now.Add(-1 * skew)) {
|
||||||
|
t := expiryTime.Format(time.RFC3339Nano)
|
||||||
|
nt := now.Format(time.RFC3339Nano)
|
||||||
|
log.Debugf(DbgResponseTTLExpired, t, nt, skew)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ocspr.ThisUpdate.After(now.Add(skew)) {
|
||||||
|
t := ocspr.ThisUpdate.Format(time.RFC3339Nano)
|
||||||
|
nt := now.Format(time.RFC3339Nano)
|
||||||
|
log.Debugf(DbgResponseFutureDated, t, nt, skew)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidDelegationCheck checks if the CA OCSP Response was signed by a valid CA Issuer delegate as per (RFC 6960, section 4.2.2.2)
|
||||||
|
// If a valid delegate or direct-signed by CA Issuer, true returned.
|
||||||
|
func ValidDelegationCheck(iss *x509.Certificate, ocspr *ocsp.Response) bool {
|
||||||
|
// This call assumes prior successful parse and signature validation of the OCSP response
|
||||||
|
// The Go OCSP library (as of x/crypto/ocsp v0.9) will detect and perform a 1-level delegate signature check but does not
|
||||||
|
// implement the additional criteria for delegation specified in RFC 6960, section 4.2.2.2.
|
||||||
|
if iss == nil || ocspr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// not a delegation, no-op
|
||||||
|
if ocspr.Certificate == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// delegate is self-same with CA Issuer, not a delegation although response issued in that form
|
||||||
|
if ocspr.Certificate.Equal(iss) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// we need to verify CA Issuer stamped id-kp-OCSPSigning on delegate
|
||||||
|
delegatedSigner := false
|
||||||
|
for _, keyUseExt := range ocspr.Certificate.ExtKeyUsage {
|
||||||
|
if keyUseExt == x509.ExtKeyUsageOCSPSigning {
|
||||||
|
delegatedSigner = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delegatedSigner
|
||||||
|
}
|
||||||
56
server/certidp/certidp_test.go
Normal file
56
server/certidp/certidp_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2023 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package certidp
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Checks the return values of the function GetStatusAssertionStr
|
||||||
|
func TestGetStatusAssertionStr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GoodStatus",
|
||||||
|
input: 0,
|
||||||
|
expected: "good",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RevokedStatus",
|
||||||
|
input: 1,
|
||||||
|
expected: "revoked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UnknownStatus",
|
||||||
|
input: 2,
|
||||||
|
expected: "unknown",
|
||||||
|
},
|
||||||
|
// Invalid status assertion value.
|
||||||
|
{
|
||||||
|
name: "InvalidStatus",
|
||||||
|
input: 42,
|
||||||
|
expected: "unknown",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := GetStatusAssertionStr(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("Expected GetStatusAssertionStr: %v, got %v", tt.expected, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/certidp/messages.go
Normal file
106
server/certidp/messages.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2023 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package certidp
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Returned errors
|
||||||
|
ErrIllegalPeerOptsConfig = "expected map to define OCSP peer options, got [%T]"
|
||||||
|
ErrIllegalCacheOptsConfig = "expected map to define OCSP peer cache options, got [%T]"
|
||||||
|
ErrParsingPeerOptFieldGeneric = "error parsing tls peer config, unknown field [%q]"
|
||||||
|
ErrParsingPeerOptFieldTypeConversion = "error parsing tls peer config, conversion error: %s"
|
||||||
|
ErrParsingCacheOptFieldTypeConversion = "error parsing OCSP peer cache config, conversion error: %s"
|
||||||
|
ErrUnableToPlugTLSEmptyConfig = "unable to plug TLS verify connection, config is nil"
|
||||||
|
ErrMTLSRequired = "OCSP peer verification for client connections requires TLS verify (mTLS) to be enabled"
|
||||||
|
ErrUnableToPlugTLSClient = "unable to register client OCSP verification"
|
||||||
|
ErrUnableToPlugTLSServer = "unable to register server OCSP verification"
|
||||||
|
ErrCannotWriteCompressed = "error writing to compression writer: %w"
|
||||||
|
ErrCannotReadCompressed = "error reading compression reader: %w"
|
||||||
|
ErrTruncatedWrite = "short write on body (%d != %d)"
|
||||||
|
ErrCannotCloseWriter = "error closing compression writer: %w"
|
||||||
|
ErrParsingCacheOptFieldGeneric = "error parsing OCSP peer cache config, unknown field [%q]"
|
||||||
|
ErrUnknownCacheType = "error parsing OCSP peer cache config, unknown type [%s]"
|
||||||
|
ErrInvalidChainlink = "invalid chain link"
|
||||||
|
ErrBadResponderHTTPStatus = "bad OCSP responder http status: [%d]"
|
||||||
|
ErrNoAvailOCSPServers = "no available OCSP servers"
|
||||||
|
ErrFailedWithAllRequests = "exhausted OCSP responders: %w"
|
||||||
|
|
||||||
|
// Direct logged errors
|
||||||
|
ErrLoadCacheFail = "Unable to load OCSP peer cache: %s"
|
||||||
|
ErrSaveCacheFail = "Unable to save OCSP peer cache: %s"
|
||||||
|
ErrBadCacheTypeConfig = "Unimplemented OCSP peer cache type [%v]"
|
||||||
|
ErrResponseCompressFail = "Unable to compress OCSP response for key [%s]: %s"
|
||||||
|
ErrResponseDecompressFail = "Unable to decompress OCSP response for key [%s]: %s"
|
||||||
|
ErrPeerEmptyNoEvent = "Peer certificate is nil, cannot send OCSP peer reject event"
|
||||||
|
ErrPeerEmptyAutoReject = "Peer certificate is nil, rejecting OCSP peer"
|
||||||
|
|
||||||
|
// Debug information
|
||||||
|
DbgPlugTLSForKind = "Plugging TLS OCSP peer for [%s]"
|
||||||
|
DbgNumServerChains = "Peer OCSP enabled: %d TLS server chain(s) will be evaluated"
|
||||||
|
DbgNumClientChains = "Peer OCSP enabled: %d TLS client chain(s) will be evaluated"
|
||||||
|
DbgLinksInChain = "Chain [%d]: %d total link(s)"
|
||||||
|
DbgSelfSignedValid = "Chain [%d] is self-signed, thus peer is valid"
|
||||||
|
DbgValidNonOCSPChain = "Chain [%d] has no OCSP eligible links, thus peer is valid"
|
||||||
|
DbgChainIsOCSPEligible = "Chain [%d] has %d OCSP eligible link(s)"
|
||||||
|
DbgChainIsOCSPValid = "Chain [%d] is OCSP valid for all eligible links, thus peer is valid"
|
||||||
|
DbgNoOCSPValidChains = "No OCSP valid chains, thus peer is invalid"
|
||||||
|
DbgCheckingCacheForCert = "Checking OCSP peer cache for [%s], key [%s]"
|
||||||
|
DbgCurrentResponseCached = "Cached OCSP response is current, status [%s]"
|
||||||
|
DbgExpiredResponseCached = "Cached OCSP response is expired, status [%s]"
|
||||||
|
DbgOCSPValidPeerLink = "OCSP verify pass for [%s]"
|
||||||
|
DbgCachingResponse = "Caching OCSP response for [%s], key [%s]"
|
||||||
|
DbgAchievedCompression = "OCSP response compression ratio: [%f]"
|
||||||
|
DbgCacheHit = "OCSP peer cache hit for key [%s]"
|
||||||
|
DbgCacheMiss = "OCSP peer cache miss for key [%s]"
|
||||||
|
DbgPreservedRevocation = "Revoked OCSP response for key [%s] preserved by cache policy"
|
||||||
|
DbgDeletingCacheResponse = "Deleting OCSP peer cached response for key [%s]"
|
||||||
|
DbgStartingCache = "Starting OCSP peer cache"
|
||||||
|
DbgStoppingCache = "Stopping OCSP peer cache"
|
||||||
|
DbgLoadingCache = "Loading OCSP peer cache [%s]"
|
||||||
|
DbgNoCacheFound = "No OCSP peer cache found, starting with empty cache"
|
||||||
|
DbgSavingCache = "Saving OCSP peer cache [%s]"
|
||||||
|
DbgCacheSaved = "Saved OCSP peer cache successfully (%d bytes)"
|
||||||
|
DbgMakingCARequest = "Trying OCSP responder url [%s]"
|
||||||
|
DbgResponseExpired = "OCSP response NextUpdate [%s] is before now [%s] with clockskew [%s]"
|
||||||
|
DbgResponseTTLExpired = "OCSP response cache expiry [%s] is before now [%s] with clockskew [%s]"
|
||||||
|
DbgResponseFutureDated = "OCSP response ThisUpdate [%s] is before now [%s] with clockskew [%s]"
|
||||||
|
DbgCacheSaveTimerExpired = "OCSP peer cache save timer expired"
|
||||||
|
DbgCacheDirtySave = "OCSP peer cache is dirty, saving"
|
||||||
|
|
||||||
|
// Returned to peer as TLS reject reason
|
||||||
|
MsgTLSClientRejectConnection = "client not OCSP valid"
|
||||||
|
MsgTLSServerRejectConnection = "server not OCSP valid"
|
||||||
|
|
||||||
|
// Expected runtime errors (direct logged)
|
||||||
|
ErrCAResponderCalloutFail = "Attempt to obtain OCSP response from CA responder for [%s] failed: %s"
|
||||||
|
ErrNewCAResponseNotCurrent = "New OCSP CA response obtained for [%s] but not current"
|
||||||
|
ErrCAResponseParseFailed = "Could not parse OCSP CA response for [%s]: %s"
|
||||||
|
ErrOCSPInvalidPeerLink = "OCSP verify fail for [%s] with CA status [%s]"
|
||||||
|
|
||||||
|
// Policy override warnings (direct logged)
|
||||||
|
MsgAllowWhenCAUnreachableOccurred = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; no cached revocation so allowing"
|
||||||
|
MsgAllowWhenCAUnreachableOccurredCachedRevoke = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; cached revocation exists so rejecting"
|
||||||
|
MsgAllowWarnOnlyOccurred = "OCSP verify fail for [%s] but WarnOnly is true so allowing"
|
||||||
|
|
||||||
|
// Info (direct logged)
|
||||||
|
MsgCacheOnline = "OCSP peer cache online, type [%s]"
|
||||||
|
MsgCacheOffline = "OCSP peer cache offline, type [%s]"
|
||||||
|
|
||||||
|
// OCSP cert invalid reasons (debug and event reasons)
|
||||||
|
MsgFailedOCSPResponseFetch = "Failed OCSP response fetch"
|
||||||
|
MsgOCSPResponseNotEffective = "OCSP response not in effectivity window"
|
||||||
|
MsgFailedOCSPResponseParse = "Failed OCSP response parse"
|
||||||
|
MsgOCSPResponseInvalidStatus = "Invalid OCSP response status: %s"
|
||||||
|
MsgOCSPResponseDelegationInvalid = "Invalid OCSP response delegation: %s"
|
||||||
|
MsgCachedOCSPResponseInvalid = "Invalid cached OCSP response for [%s] with fingerprint [%s]"
|
||||||
|
)
|
||||||
92
server/certidp/ocsp_responder.go
Normal file
92
server/certidp/ocsp_responder.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2023-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package certidp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchOCSPResponse(link *ChainLink, opts *OCSPPeerConfig, log *Log) ([]byte, error) {
|
||||||
|
if link == nil || link.Leaf == nil || link.Issuer == nil || opts == nil || log == nil {
|
||||||
|
return nil, errors.New(ErrInvalidChainlink)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(opts.Timeout * float64(time.Second))
|
||||||
|
if timeout <= 0*time.Second {
|
||||||
|
timeout = DefaultOCSPResponderTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestBytes := func(u string, hc *http.Client) ([]byte, error) {
|
||||||
|
resp, err := hc.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf(ErrBadResponderHTTPStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request documentation:
|
||||||
|
// https://tools.ietf.org/html/rfc6960#appendix-A.1
|
||||||
|
|
||||||
|
reqDER, err := ocsp.CreateRequest(link.Leaf, link.Issuer, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqEnc := encodeOCSPRequest(reqDER)
|
||||||
|
|
||||||
|
responders := *link.OCSPWebEndpoints
|
||||||
|
|
||||||
|
if len(responders) == 0 {
|
||||||
|
return nil, errors.New(ErrNoAvailOCSPServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw []byte
|
||||||
|
hc := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
for _, u := range responders {
|
||||||
|
responderURL := u.String()
|
||||||
|
log.Debugf(DbgMakingCARequest, responderURL)
|
||||||
|
responderURL = strings.TrimSuffix(responderURL, "/")
|
||||||
|
raw, err = getRequestBytes(fmt.Sprintf("%s/%s", responderURL, reqEnc), hc)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedWithAllRequests, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeOCSPRequest encodes the OCSP request in base64 and URL-encodes it.
|
||||||
|
// This is needed to fulfill the OCSP responder's requirements for the request format. (X.690)
|
||||||
|
func encodeOCSPRequest(reqDER []byte) string {
|
||||||
|
reqEnc := base64.StdEncoding.EncodeToString(reqDER)
|
||||||
|
return url.QueryEscape(reqEnc)
|
||||||
|
}
|
||||||
29
server/certidp/ocsp_responder_test.go
Normal file
29
server/certidp/ocsp_responder_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package certidp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeOCSPRequest(t *testing.T) {
|
||||||
|
data := []byte("test data for OCSP request")
|
||||||
|
encoded := encodeOCSPRequest(data)
|
||||||
|
|
||||||
|
if strings.ContainsAny(encoded, "+/=") {
|
||||||
|
t.Errorf("URL contains unescaped characters: %s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedURL, err := url.QueryUnescape(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to url-decode request: %v", err)
|
||||||
|
}
|
||||||
|
decodedData, err := base64.StdEncoding.DecodeString(decodedURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to base64-decode request: %v", err)
|
||||||
|
}
|
||||||
|
if string(decodedData) != string(data) {
|
||||||
|
t.Errorf("Decoded data does not match original: got %s, want %s", decodedData, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
server/certstore/certstore.go
Normal file
104
server/certstore/certstore.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright 2022-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package certstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreType int
|
||||||
|
|
||||||
|
const MATCHBYEMPTY = 0
|
||||||
|
const STOREEMPTY = 0
|
||||||
|
|
||||||
|
const (
|
||||||
|
windowsCurrentUser StoreType = iota + 1
|
||||||
|
windowsLocalMachine
|
||||||
|
)
|
||||||
|
|
||||||
|
var StoreMap = map[string]StoreType{
|
||||||
|
"windowscurrentuser": windowsCurrentUser,
|
||||||
|
"windowslocalmachine": windowsLocalMachine,
|
||||||
|
}
|
||||||
|
|
||||||
|
var StoreOSMap = map[StoreType]string{
|
||||||
|
windowsCurrentUser: "windows",
|
||||||
|
windowsLocalMachine: "windows",
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchByType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
matchByIssuer MatchByType = iota + 1
|
||||||
|
matchBySubject
|
||||||
|
matchByThumbprint
|
||||||
|
)
|
||||||
|
|
||||||
|
var MatchByMap = map[string]MatchByType{
|
||||||
|
"issuer": matchByIssuer,
|
||||||
|
"subject": matchBySubject,
|
||||||
|
"thumbprint": matchByThumbprint,
|
||||||
|
}
|
||||||
|
|
||||||
|
var Usage = `
|
||||||
|
In place of cert_file and key_file you may use the windows certificate store:
|
||||||
|
|
||||||
|
tls {
|
||||||
|
cert_store: "WindowsCurrentUser"
|
||||||
|
cert_match_by: "Subject"
|
||||||
|
cert_match: "MyServer123"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func ParseCertStore(certStore string) (StoreType, error) {
|
||||||
|
certStoreType, exists := StoreMap[strings.ToLower(certStore)]
|
||||||
|
if !exists {
|
||||||
|
return 0, ErrBadCertStore
|
||||||
|
}
|
||||||
|
validOS, exists := StoreOSMap[certStoreType]
|
||||||
|
if !exists || validOS != runtime.GOOS {
|
||||||
|
return 0, ErrOSNotCompatCertStore
|
||||||
|
}
|
||||||
|
return certStoreType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCertMatchBy(certMatchBy string) (MatchByType, error) {
|
||||||
|
certMatchByType, exists := MatchByMap[strings.ToLower(certMatchBy)]
|
||||||
|
if !exists {
|
||||||
|
return 0, ErrBadMatchByType
|
||||||
|
}
|
||||||
|
return certMatchByType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLeafIssuer(leaf *x509.Certificate, vOpts x509.VerifyOptions) (issuer *x509.Certificate) {
|
||||||
|
chains, err := leaf.Verify(vOpts)
|
||||||
|
if err != nil || len(chains) == 0 {
|
||||||
|
issuer = nil
|
||||||
|
} else {
|
||||||
|
issuer = chains[0][1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// credential provides access to a public key and is a crypto.Signer.
|
||||||
|
type credential interface {
|
||||||
|
// Public returns the public key corresponding to the leaf certificate.
|
||||||
|
Public() crypto.PublicKey
|
||||||
|
// Sign signs digest with the private key.
|
||||||
|
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
|
||||||
|
}
|
||||||
45
server/certstore/certstore_other.go
Normal file
45
server/certstore/certstore_other.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2022-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package certstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = MATCHBYEMPTY
|
||||||
|
|
||||||
|
// otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore
|
||||||
|
type otherKey struct{}
|
||||||
|
|
||||||
|
func TLSConfig(_ StoreType, _ MatchByType, _ string, _ []string, _ bool, _ *tls.Config) error {
|
||||||
|
return ErrOSNotCompatCertStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public always returns nil public key since this is a stub on non-supported platform
|
||||||
|
func (k otherKey) Public() crypto.PublicKey {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign always returns a nil signature since this is a stub on non-supported platform
|
||||||
|
func (k otherKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
||||||
|
_, _, _ = rand, digest, opts
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify interface conformance.
|
||||||
|
var _ credential = &otherKey{}
|
||||||
965
server/certstore/certstore_windows.go
Normal file
965
server/certstore/certstore_windows.go
Normal file
@@ -0,0 +1,965 @@
|
|||||||
|
// Copyright 2022-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Adapted, updated, and enhanced from CertToStore, https://github.com/google/certtostore/releases/tag/v1.0.2
|
||||||
|
// Apache License, Version 2.0, Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
package certstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/cryptobyte/asn1"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// wincrypt.h constants
|
||||||
|
winAcquireCached = windows.CRYPT_ACQUIRE_CACHE_FLAG
|
||||||
|
winAcquireSilent = windows.CRYPT_ACQUIRE_SILENT_FLAG
|
||||||
|
winAcquireOnlyNCryptKey = windows.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG
|
||||||
|
winEncodingX509ASN = windows.X509_ASN_ENCODING
|
||||||
|
winEncodingPKCS7 = windows.PKCS_7_ASN_ENCODING
|
||||||
|
winCertStoreProvSystem = windows.CERT_STORE_PROV_SYSTEM
|
||||||
|
winCertStoreCurrentUser = windows.CERT_SYSTEM_STORE_CURRENT_USER
|
||||||
|
winCertStoreLocalMachine = windows.CERT_SYSTEM_STORE_LOCAL_MACHINE
|
||||||
|
winCertStoreReadOnly = windows.CERT_STORE_READONLY_FLAG
|
||||||
|
winInfoIssuerFlag = windows.CERT_INFO_ISSUER_FLAG
|
||||||
|
winInfoSubjectFlag = windows.CERT_INFO_SUBJECT_FLAG
|
||||||
|
winCompareNameStrW = windows.CERT_COMPARE_NAME_STR_W
|
||||||
|
winCompareShift = windows.CERT_COMPARE_SHIFT
|
||||||
|
|
||||||
|
// Reference https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
|
||||||
|
winFindIssuerStr = windows.CERT_FIND_ISSUER_STR_W
|
||||||
|
winFindSubjectStr = windows.CERT_FIND_SUBJECT_STR_W
|
||||||
|
winFindHashStr = windows.CERT_FIND_HASH_STR
|
||||||
|
|
||||||
|
winNcryptKeySpec = windows.CERT_NCRYPT_KEY_SPEC
|
||||||
|
|
||||||
|
winBCryptPadPKCS1 uintptr = 0x2
|
||||||
|
winBCryptPadPSS uintptr = 0x8 // Modern TLS 1.2+
|
||||||
|
winBCryptPadPSSSalt uint32 = 32 // default 20, 32 optimal for typical SHA256 hash
|
||||||
|
|
||||||
|
winRSA1Magic = 0x31415352 // "RSA1" BCRYPT_RSAPUBLIC_MAGIC
|
||||||
|
|
||||||
|
winECS1Magic = 0x31534345 // "ECS1" BCRYPT_ECDSA_PUBLIC_P256_MAGIC
|
||||||
|
winECS3Magic = 0x33534345 // "ECS3" BCRYPT_ECDSA_PUBLIC_P384_MAGIC
|
||||||
|
winECS5Magic = 0x35534345 // "ECS5" BCRYPT_ECDSA_PUBLIC_P521_MAGIC
|
||||||
|
|
||||||
|
winECK1Magic = 0x314B4345 // "ECK1" BCRYPT_ECDH_PUBLIC_P256_MAGIC
|
||||||
|
winECK3Magic = 0x334B4345 // "ECK3" BCRYPT_ECDH_PUBLIC_P384_MAGIC
|
||||||
|
winECK5Magic = 0x354B4345 // "ECK5" BCRYPT_ECDH_PUBLIC_P521_MAGIC
|
||||||
|
|
||||||
|
winCryptENotFound = windows.CRYPT_E_NOT_FOUND
|
||||||
|
|
||||||
|
providerMSSoftware = "Microsoft Software Key Storage Provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
winBCryptRSAPublicBlob = winWide("RSAPUBLICBLOB")
|
||||||
|
winBCryptECCPublicBlob = winWide("ECCPUBLICBLOB")
|
||||||
|
|
||||||
|
winNCryptAlgorithmGroupProperty = winWide("Algorithm Group") // NCRYPT_ALGORITHM_GROUP_PROPERTY
|
||||||
|
winNCryptUniqueNameProperty = winWide("Unique Name") // NCRYPT_UNIQUE_NAME_PROPERTY
|
||||||
|
winNCryptECCCurveNameProperty = winWide("ECCCurveName") // NCRYPT_ECC_CURVE_NAME_PROPERTY
|
||||||
|
|
||||||
|
winCurveIDs = map[uint32]elliptic.Curve{
|
||||||
|
winECS1Magic: elliptic.P256(), // BCRYPT_ECDSA_PUBLIC_P256_MAGIC
|
||||||
|
winECS3Magic: elliptic.P384(), // BCRYPT_ECDSA_PUBLIC_P384_MAGIC
|
||||||
|
winECS5Magic: elliptic.P521(), // BCRYPT_ECDSA_PUBLIC_P521_MAGIC
|
||||||
|
winECK1Magic: elliptic.P256(), // BCRYPT_ECDH_PUBLIC_P256_MAGIC
|
||||||
|
winECK3Magic: elliptic.P384(), // BCRYPT_ECDH_PUBLIC_P384_MAGIC
|
||||||
|
winECK5Magic: elliptic.P521(), // BCRYPT_ECDH_PUBLIC_P521_MAGIC
|
||||||
|
}
|
||||||
|
|
||||||
|
winCurveNames = map[string]elliptic.Curve{
|
||||||
|
"nistP256": elliptic.P256(), // BCRYPT_ECC_CURVE_NISTP256
|
||||||
|
"nistP384": elliptic.P384(), // BCRYPT_ECC_CURVE_NISTP384
|
||||||
|
"nistP521": elliptic.P521(), // BCRYPT_ECC_CURVE_NISTP521
|
||||||
|
}
|
||||||
|
|
||||||
|
winAlgIDs = map[crypto.Hash]*uint16{
|
||||||
|
crypto.SHA1: winWide("SHA1"), // BCRYPT_SHA1_ALGORITHM
|
||||||
|
crypto.SHA256: winWide("SHA256"), // BCRYPT_SHA256_ALGORITHM
|
||||||
|
crypto.SHA384: winWide("SHA384"), // BCRYPT_SHA384_ALGORITHM
|
||||||
|
crypto.SHA512: winWide("SHA512"), // BCRYPT_SHA512_ALGORITHM
|
||||||
|
}
|
||||||
|
|
||||||
|
// MY is well-known system store on Windows that holds personal certificates. Read
|
||||||
|
// More about the CA locations here:
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/wcf/certificate-of-clientcertificate-element?redirectedfrom=MSDN
|
||||||
|
// https://superuser.com/questions/217719/what-are-the-windows-system-certificate-stores
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-stores
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations
|
||||||
|
// https://stackoverflow.com/questions/63286085/which-x509-storename-refers-to-the-certificates-stored-beneath-trusted-root-cert#:~:text=4-,StoreName.,is%20%22Intermediate%20Certification%20Authorities%22.
|
||||||
|
winMyStore = winWide("MY")
|
||||||
|
winIntermediateCAStore = winWide("CA")
|
||||||
|
winRootStore = winWide("Root")
|
||||||
|
winAuthRootStore = winWide("AuthRoot")
|
||||||
|
|
||||||
|
// These DLLs must be available on all Windows hosts
|
||||||
|
winCrypt32 = windows.NewLazySystemDLL("crypt32.dll")
|
||||||
|
winNCrypt = windows.NewLazySystemDLL("ncrypt.dll")
|
||||||
|
|
||||||
|
winCertFindCertificateInStore = winCrypt32.NewProc("CertFindCertificateInStore")
|
||||||
|
winCertVerifyTimeValidity = winCrypt32.NewProc("CertVerifyTimeValidity")
|
||||||
|
winCryptAcquireCertificatePrivateKey = winCrypt32.NewProc("CryptAcquireCertificatePrivateKey")
|
||||||
|
winNCryptExportKey = winNCrypt.NewProc("NCryptExportKey")
|
||||||
|
winNCryptOpenStorageProvider = winNCrypt.NewProc("NCryptOpenStorageProvider")
|
||||||
|
winNCryptGetProperty = winNCrypt.NewProc("NCryptGetProperty")
|
||||||
|
winNCryptSignHash = winNCrypt.NewProc("NCryptSignHash")
|
||||||
|
|
||||||
|
winFnGetProperty = winGetProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, d := range []*windows.LazyDLL{
|
||||||
|
winCrypt32, winNCrypt,
|
||||||
|
} {
|
||||||
|
if err := d.Load(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range []*windows.LazyProc{
|
||||||
|
winCertFindCertificateInStore, winCryptAcquireCertificatePrivateKey,
|
||||||
|
winNCryptExportKey, winNCryptOpenStorageProvider,
|
||||||
|
winNCryptGetProperty, winNCryptSignHash,
|
||||||
|
} {
|
||||||
|
if err := p.Find(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type winPKCS1PaddingInfo struct {
|
||||||
|
pszAlgID *uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type winPSSPaddingInfo struct {
|
||||||
|
pszAlgID *uint16
|
||||||
|
cbSalt uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCACertsPool generates a CertPool from the Windows certificate store,
|
||||||
|
// adding all matching certificates from the caCertsMatch array to the pool.
|
||||||
|
// All matching certificates (vs first) are added to the pool based on a user
|
||||||
|
// request. If no certificates are found an error is returned.
|
||||||
|
func createCACertsPool(cs *winCertStore, storeType uint32, caCertsMatch []string, skipInvalid bool) (*x509.CertPool, error) {
|
||||||
|
var errs []error
|
||||||
|
caPool := x509.NewCertPool()
|
||||||
|
for _, s := range caCertsMatch {
|
||||||
|
lfs, err := cs.caCertsBySubjectMatch(s, storeType, skipInvalid)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
for _, lf := range lfs {
|
||||||
|
caPool.AddCert(lf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If every lookup failed return the errors.
|
||||||
|
if len(errs) == len(caCertsMatch) {
|
||||||
|
return nil, fmt.Errorf("unable to match any CA certificate: %v", errs)
|
||||||
|
}
|
||||||
|
return caPool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConfig fulfills the same function as reading cert and key pair from
|
||||||
|
// pem files but sources the Windows certificate store instead. The
|
||||||
|
// certMatchBy and certMatch fields search the "MY" certificate location
|
||||||
|
// for the first certificate that matches the certMatch field. The
|
||||||
|
// caCertsMatch field is used to search the Trusted Root, Third Party Root,
|
||||||
|
// and Intermediate Certificate Authority locations for certificates with
|
||||||
|
// Subjects matching the provided strings. If a match is found, the
|
||||||
|
// certificate is added to the pool that is used to verify the certificate
|
||||||
|
// chain.
|
||||||
|
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, skipInvalid bool, config *tls.Config) error {
|
||||||
|
var (
|
||||||
|
leaf *x509.Certificate
|
||||||
|
leafCtx *windows.CertContext
|
||||||
|
pk *winKey
|
||||||
|
vOpts = x509.VerifyOptions{}
|
||||||
|
chains [][]*x509.Certificate
|
||||||
|
chain []*x509.Certificate
|
||||||
|
rawChain [][]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
// By StoreType, open a store
|
||||||
|
if certStore == windowsCurrentUser || certStore == windowsLocalMachine {
|
||||||
|
var scope uint32
|
||||||
|
cs, err := winOpenCertStore(providerMSSoftware)
|
||||||
|
if err != nil || cs == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if certStore == windowsCurrentUser {
|
||||||
|
scope = winCertStoreCurrentUser
|
||||||
|
}
|
||||||
|
if certStore == windowsLocalMachine {
|
||||||
|
scope = winCertStoreLocalMachine
|
||||||
|
}
|
||||||
|
|
||||||
|
// certByIssuer or certBySubject
|
||||||
|
if certMatchBy == matchBySubject || certMatchBy == MATCHBYEMPTY {
|
||||||
|
leaf, leafCtx, err = cs.certBySubject(certMatch, scope, skipInvalid)
|
||||||
|
} else if certMatchBy == matchByIssuer {
|
||||||
|
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope, skipInvalid)
|
||||||
|
} else if certMatchBy == matchByThumbprint {
|
||||||
|
leaf, leafCtx, err = cs.certByThumbprint(certMatch, scope, skipInvalid)
|
||||||
|
} else {
|
||||||
|
return ErrBadMatchByType
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// pass through error from cert search
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if leaf == nil || leafCtx == nil {
|
||||||
|
return ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
pk, err = cs.certKey(leafCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pk == nil {
|
||||||
|
return ErrNoPrivateKeyStoreRef
|
||||||
|
}
|
||||||
|
// Look for CA Certificates
|
||||||
|
if len(caCertsMatch) != 0 {
|
||||||
|
caPool, err := createCACertsPool(cs, scope, caCertsMatch, skipInvalid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ClientCAs = caPool
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ErrBadCertStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get intermediates in the cert store for the found leaf IFF there is a full chain of trust in the store
|
||||||
|
// otherwise just use leaf as the final chain.
|
||||||
|
//
|
||||||
|
// Using std lib Verify as a reliable way to get valid chains out of the win store for the leaf; however,
|
||||||
|
// using empty options since server TLS stanza could be TLS role as server identity or client identity.
|
||||||
|
chains, err := leaf.Verify(vOpts)
|
||||||
|
if err != nil || len(chains) == 0 {
|
||||||
|
chains = append(chains, []*x509.Certificate{leaf})
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have at least one verified chain so pop the first chain and remove the self-signed CA cert (if present)
|
||||||
|
// from the end of the chain
|
||||||
|
chain = chains[0]
|
||||||
|
if len(chain) > 1 {
|
||||||
|
chain = chain[:len(chain)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// For tls.Certificate.Certificate need a [][]byte from []*x509.Certificate
|
||||||
|
// Approximate capacity for efficiency
|
||||||
|
rawChain = make([][]byte, 0, len(chain))
|
||||||
|
for _, link := range chain {
|
||||||
|
rawChain = append(rawChain, link.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCert := tls.Certificate{
|
||||||
|
Certificate: rawChain,
|
||||||
|
PrivateKey: pk,
|
||||||
|
Leaf: leaf,
|
||||||
|
}
|
||||||
|
config.Certificates = []tls.Certificate{tlsCert}
|
||||||
|
|
||||||
|
// note: pk is a windows pointer (not freed by Go) but needs to live the life of the server for Signing.
|
||||||
|
// The cert context (leafCtx) windows pointer must not be freed underneath the pk so also life of the server.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// winWide returns a pointer to uint16 representing the equivalent
|
||||||
|
// to a Windows LPCWSTR.
|
||||||
|
func winWide(s string) *uint16 {
|
||||||
|
w := utf16.Encode([]rune(s))
|
||||||
|
w = append(w, 0)
|
||||||
|
return &w[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// winOpenProvider gets a provider handle for subsequent calls
|
||||||
|
func winOpenProvider(provider string) (uintptr, error) {
|
||||||
|
var hProv uintptr
|
||||||
|
pname := winWide(provider)
|
||||||
|
// Open the provider, the last parameter is not used
|
||||||
|
r, _, err := winNCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&hProv)), uintptr(unsafe.Pointer(pname)), 0)
|
||||||
|
if r == 0 {
|
||||||
|
return hProv, nil
|
||||||
|
}
|
||||||
|
return hProv, fmt.Errorf("NCryptOpenStorageProvider returned %X: %v", r, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// winFindCert wraps the CertFindCertificateInStore library call. Note that any cert context passed
|
||||||
|
// into prev will be freed. If no certificate was found, nil will be returned.
|
||||||
|
func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) {
|
||||||
|
h, _, err := winCertFindCertificateInStore.Call(
|
||||||
|
uintptr(store),
|
||||||
|
uintptr(enc),
|
||||||
|
uintptr(findFlags),
|
||||||
|
uintptr(findType),
|
||||||
|
uintptr(unsafe.Pointer(para)),
|
||||||
|
uintptr(unsafe.Pointer(prev)),
|
||||||
|
)
|
||||||
|
if h == 0 {
|
||||||
|
// Actual error, or simply not found?
|
||||||
|
if errno, ok := err.(syscall.Errno); ok && errno == syscall.Errno(winCryptENotFound) {
|
||||||
|
return nil, ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
return nil, ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
// nolint:govet
|
||||||
|
return (*windows.CertContext)(unsafe.Pointer(h)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// winVerifyCertValid wraps the CertVerifyTimeValidity and simply returns true if the certificate is valid
|
||||||
|
func winVerifyCertValid(timeToVerify *windows.Filetime, certInfo *windows.CertInfo) bool {
|
||||||
|
// this function does not document returning errors / setting lasterror
|
||||||
|
r, _, _ := winCertVerifyTimeValidity.Call(
|
||||||
|
uintptr(unsafe.Pointer(timeToVerify)),
|
||||||
|
uintptr(unsafe.Pointer(certInfo)),
|
||||||
|
)
|
||||||
|
return r == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// winCertStore is a store implementation for the Windows Certificate Store
|
||||||
|
type winCertStore struct {
|
||||||
|
Prov uintptr
|
||||||
|
ProvName string
|
||||||
|
stores map[string]*winStoreHandle
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// winOpenCertStore creates a winCertStore
|
||||||
|
func winOpenCertStore(provider string) (*winCertStore, error) {
|
||||||
|
cngProv, err := winOpenProvider(provider)
|
||||||
|
if err != nil {
|
||||||
|
// pass through error from winOpenProvider
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wcs := &winCertStore{
|
||||||
|
Prov: cngProv,
|
||||||
|
ProvName: provider,
|
||||||
|
stores: make(map[string]*winStoreHandle),
|
||||||
|
}
|
||||||
|
|
||||||
|
return wcs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// winCertContextToX509 creates an x509.Certificate from a Windows cert context.
|
||||||
|
func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
|
||||||
|
var der []byte
|
||||||
|
slice := (*reflect.SliceHeader)(unsafe.Pointer(&der))
|
||||||
|
slice.Data = uintptr(unsafe.Pointer(ctx.EncodedCert))
|
||||||
|
slice.Len = int(ctx.Length)
|
||||||
|
slice.Cap = int(ctx.Length)
|
||||||
|
return x509.ParseCertificate(der)
|
||||||
|
}
|
||||||
|
|
||||||
|
// certByIssuer matches and returns the first certificate found by passed issuer.
|
||||||
|
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
|
||||||
|
// current user's personal certs or local machine's personal certs using storeType.
|
||||||
|
// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
|
||||||
|
func (w *winCertStore) certByIssuer(issuer string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
|
||||||
|
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType, skipInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// certBySubject matches and returns the first certificate found by passed subject field.
|
||||||
|
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
|
||||||
|
// current user's personal certs or local machine's personal certs using storeType.
|
||||||
|
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
|
||||||
|
func (w *winCertStore) certBySubject(subject string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
|
||||||
|
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType, skipInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// certByThumbprint matches and returns the first certificate found by passed SHA1 thumbprint.
|
||||||
|
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
|
||||||
|
// current user's personal certs or local machine's personal certs using storeType.
|
||||||
|
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
|
||||||
|
func (w *winCertStore) certByThumbprint(hash string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
|
||||||
|
return w.certSearch(winFindHashStr, hash, winMyStore, storeType, skipInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// caCertsBySubjectMatch matches and returns all matching certificates of the subject field.
|
||||||
|
//
|
||||||
|
// The following locations are searched:
|
||||||
|
// 1) Root (Trusted Root Certification Authorities)
|
||||||
|
// 2) AuthRoot (Third-Party Root Certification Authorities)
|
||||||
|
// 3) CA (Intermediate Certification Authorities)
|
||||||
|
//
|
||||||
|
// Caller specifies current user's personal certs or local machine's personal certs using storeType.
|
||||||
|
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
|
||||||
|
func (w *winCertStore) caCertsBySubjectMatch(subject string, storeType uint32, skipInvalid bool) ([]*x509.Certificate, error) {
|
||||||
|
var (
|
||||||
|
leaf *x509.Certificate
|
||||||
|
searchLocations = [3]*uint16{winRootStore, winAuthRootStore, winIntermediateCAStore}
|
||||||
|
rv []*x509.Certificate
|
||||||
|
)
|
||||||
|
// surprisingly, an empty string returns a result. We'll treat this as an error.
|
||||||
|
if subject == "" {
|
||||||
|
return nil, ErrBadCaCertMatchField
|
||||||
|
}
|
||||||
|
for _, sr := range searchLocations {
|
||||||
|
var err error
|
||||||
|
if leaf, _, err = w.certSearch(winFindSubjectStr, subject, sr, storeType, skipInvalid); err == nil {
|
||||||
|
rv = append(rv, leaf)
|
||||||
|
} else {
|
||||||
|
// Ignore the failed search from a single location. Errors we catch include
|
||||||
|
// ErrFailedX509Extract (resulting from a malformed certificate) and errors
|
||||||
|
// around invalid attributes, unsupported algorithms, etc. These are corner
|
||||||
|
// cases as certificates with these errors shouldn't have been allowed
|
||||||
|
// to be added to the store in the first place.
|
||||||
|
if err != ErrFailedCertSearch {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not found anywhere
|
||||||
|
if len(rv) == 0 {
|
||||||
|
return nil, ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// certSearch is a helper function to lookup certificates based on search type and match value.
|
||||||
|
// store is used to specify which store to perform the lookup in (system or user).
|
||||||
|
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
|
||||||
|
// store handle to "MY" store
|
||||||
|
h, err := w.storeHandle(store, searchRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var prev *windows.CertContext
|
||||||
|
var cert *x509.Certificate
|
||||||
|
|
||||||
|
i, err := windows.UTF16PtrFromString(matchValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass 0 as the third parameter because it is not used
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx
|
||||||
|
|
||||||
|
for {
|
||||||
|
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if nc != nil {
|
||||||
|
// certificate found
|
||||||
|
prev = nc
|
||||||
|
|
||||||
|
var now *windows.Filetime
|
||||||
|
if skipInvalid && !winVerifyCertValid(now, nc.CertInfo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the DER-encoded certificate from the cert context
|
||||||
|
xc, err := winCertContextToX509(nc)
|
||||||
|
if err == nil {
|
||||||
|
cert = xc
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
return nil, nil, ErrFailedX509Extract
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, ErrFailedCertSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert == nil {
|
||||||
|
return nil, nil, ErrFailedX509Extract
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, prev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type winStoreHandle struct {
|
||||||
|
handle *windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func winNewStoreHandle(provider uint32, store *uint16) (*winStoreHandle, error) {
|
||||||
|
var s winStoreHandle
|
||||||
|
if s.handle != nil {
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
st, err := windows.CertOpenStore(
|
||||||
|
winCertStoreProvSystem,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
provider|winCertStoreReadOnly,
|
||||||
|
uintptr(unsafe.Pointer(store)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrBadCryptoStoreProvider
|
||||||
|
}
|
||||||
|
s.handle = &st
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// winKey implements crypto.Signer and crypto.Decrypter for key based operations.
|
||||||
|
type winKey struct {
|
||||||
|
handle uintptr
|
||||||
|
pub crypto.PublicKey
|
||||||
|
Container string
|
||||||
|
AlgorithmGroup string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public exports a public key to implement crypto.Signer
|
||||||
|
func (k winKey) Public() crypto.PublicKey {
|
||||||
|
return k.pub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign returns the signature of a hash to implement crypto.Signer
|
||||||
|
func (k winKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||||
|
switch k.AlgorithmGroup {
|
||||||
|
case "ECDSA", "ECDH":
|
||||||
|
return winSignECDSA(k.handle, digest)
|
||||||
|
case "RSA":
|
||||||
|
hf := opts.HashFunc()
|
||||||
|
algID, ok := winAlgIDs[hf]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrBadRSAHashAlgorithm
|
||||||
|
}
|
||||||
|
switch opts.(type) {
|
||||||
|
case *rsa.PSSOptions:
|
||||||
|
return winSignRSAPSSPadding(k.handle, digest, algID)
|
||||||
|
default:
|
||||||
|
return winSignRSAPKCS1Padding(k.handle, digest, algID)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrBadSigningAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func winSignECDSA(kh uintptr, digest []byte) ([]byte, error) {
|
||||||
|
var size uint32
|
||||||
|
// Obtain the size of the signature
|
||||||
|
r, _, _ := winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreECDSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the signature data
|
||||||
|
buf := make([]byte, size)
|
||||||
|
r, _, _ = winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreECDSASigningError
|
||||||
|
}
|
||||||
|
if len(buf) != int(size) {
|
||||||
|
return nil, ErrStoreECDSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
return winPackECDSASigValue(bytes.NewReader(buf[:size]), int(size/2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func winPackECDSASigValue(r io.Reader, digestLength int) ([]byte, error) {
|
||||||
|
sigR := make([]byte, digestLength)
|
||||||
|
if _, err := io.ReadFull(r, sigR); err != nil {
|
||||||
|
return nil, ErrStoreECDSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
sigS := make([]byte, digestLength)
|
||||||
|
if _, err := io.ReadFull(r, sigS); err != nil {
|
||||||
|
return nil, ErrStoreECDSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddASN1BigInt(new(big.Int).SetBytes(sigR))
|
||||||
|
b.AddASN1BigInt(new(big.Int).SetBytes(sigS))
|
||||||
|
})
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func winSignRSAPKCS1Padding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
|
||||||
|
// PKCS#1 v1.5 padding for some TLS 1.2
|
||||||
|
padInfo := winPKCS1PaddingInfo{pszAlgID: algID}
|
||||||
|
var size uint32
|
||||||
|
// Obtain the size of the signature
|
||||||
|
r, _, _ := winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(&padInfo)),
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
winBCryptPadPKCS1)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreRSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the signature data
|
||||||
|
sig := make([]byte, size)
|
||||||
|
r, _, _ = winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(&padInfo)),
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
uintptr(unsafe.Pointer(&sig[0])),
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
winBCryptPadPKCS1)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreRSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
return sig[:size], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
|
||||||
|
// PSS padding for TLS 1.3 and some TLS 1.2
|
||||||
|
padInfo := winPSSPaddingInfo{pszAlgID: algID, cbSalt: winBCryptPadPSSSalt}
|
||||||
|
|
||||||
|
var size uint32
|
||||||
|
// Obtain the size of the signature
|
||||||
|
r, _, _ := winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(&padInfo)),
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
winBCryptPadPSS)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreRSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the signature data
|
||||||
|
sig := make([]byte, size)
|
||||||
|
r, _, _ = winNCryptSignHash.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(&padInfo)),
|
||||||
|
uintptr(unsafe.Pointer(&digest[0])),
|
||||||
|
uintptr(len(digest)),
|
||||||
|
uintptr(unsafe.Pointer(&sig[0])),
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
winBCryptPadPSS)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrStoreRSASigningError
|
||||||
|
}
|
||||||
|
|
||||||
|
return sig[:size], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private
|
||||||
|
// key of a known certificate and returns a pointer to a winKey which implements
|
||||||
|
// both crypto.Signer. When a nil cert context is passed
|
||||||
|
// a nil key is intentionally returned, to model the expected behavior of a
|
||||||
|
// non-existent cert having no private key.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey
|
||||||
|
func (w *winCertStore) certKey(cert *windows.CertContext) (*winKey, error) {
|
||||||
|
// Return early if a nil cert was passed.
|
||||||
|
if cert == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
kh uintptr
|
||||||
|
spec uint32
|
||||||
|
mustFree int
|
||||||
|
)
|
||||||
|
r, _, _ := winCryptAcquireCertificatePrivateKey.Call(
|
||||||
|
uintptr(unsafe.Pointer(cert)),
|
||||||
|
winAcquireCached|winAcquireSilent|winAcquireOnlyNCryptKey,
|
||||||
|
0, // Reserved, must be null.
|
||||||
|
uintptr(unsafe.Pointer(&kh)),
|
||||||
|
uintptr(unsafe.Pointer(&spec)),
|
||||||
|
uintptr(unsafe.Pointer(&mustFree)),
|
||||||
|
)
|
||||||
|
// If the function succeeds, the return value is nonzero (TRUE).
|
||||||
|
if r == 0 {
|
||||||
|
return nil, ErrNoPrivateKeyStoreRef
|
||||||
|
}
|
||||||
|
if mustFree != 0 {
|
||||||
|
return nil, ErrNoPrivateKeyStoreRef
|
||||||
|
}
|
||||||
|
if spec != winNcryptKeySpec {
|
||||||
|
return nil, ErrNoPrivateKeyStoreRef
|
||||||
|
}
|
||||||
|
|
||||||
|
return winKeyMetadata(kh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func winKeyMetadata(kh uintptr) (*winKey, error) {
|
||||||
|
// uc is used to populate the unique container name attribute of the private key
|
||||||
|
uc, err := winGetPropertyStr(kh, winNCryptUniqueNameProperty)
|
||||||
|
if err != nil {
|
||||||
|
// unable to determine key unique name
|
||||||
|
return nil, ErrExtractingPrivateKeyMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
alg, err := winGetPropertyStr(kh, winNCryptAlgorithmGroupProperty)
|
||||||
|
if err != nil {
|
||||||
|
// unable to determine key algorithm
|
||||||
|
return nil, ErrExtractingPrivateKeyMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub crypto.PublicKey
|
||||||
|
|
||||||
|
switch alg {
|
||||||
|
case "ECDSA", "ECDH":
|
||||||
|
buf, err := winExport(kh, winBCryptECCPublicBlob)
|
||||||
|
if err != nil {
|
||||||
|
// failed to export ECC public key
|
||||||
|
return nil, ErrExtractingECCPublicKey
|
||||||
|
}
|
||||||
|
pub, err = unmarshalECC(buf, kh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrExtractingECCPublicKey
|
||||||
|
}
|
||||||
|
case "RSA":
|
||||||
|
buf, err := winExport(kh, winBCryptRSAPublicBlob)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
pub, err = winUnmarshalRSA(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrBadPublicKeyAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
return &winKey{handle: kh, pub: pub, Container: uc, AlgorithmGroup: alg}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winGetProperty(kh uintptr, property *uint16) ([]byte, error) {
|
||||||
|
var strSize uint32
|
||||||
|
r, _, _ := winNCryptGetProperty.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(property)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&strSize)),
|
||||||
|
0,
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrExtractPropertyFromKey
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, strSize)
|
||||||
|
r, _, _ = winNCryptGetProperty.Call(
|
||||||
|
kh,
|
||||||
|
uintptr(unsafe.Pointer(property)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(strSize),
|
||||||
|
uintptr(unsafe.Pointer(&strSize)),
|
||||||
|
0,
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrExtractPropertyFromKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winGetPropertyStr(kh uintptr, property *uint16) (string, error) {
|
||||||
|
buf, err := winFnGetProperty(kh, property)
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrExtractPropertyFromKey
|
||||||
|
}
|
||||||
|
uc := bytes.ReplaceAll(buf, []byte{0x00}, []byte(""))
|
||||||
|
return string(uc), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winExport(kh uintptr, blobType *uint16) ([]byte, error) {
|
||||||
|
var size uint32
|
||||||
|
// When obtaining the size of a public key, most parameters are not required
|
||||||
|
r, _, _ := winNCryptExportKey.Call(
|
||||||
|
kh,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(blobType)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrExtractingPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the exported key in buf now that we know the size required
|
||||||
|
buf := make([]byte, size)
|
||||||
|
r, _, _ = winNCryptExportKey.Call(
|
||||||
|
kh,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(blobType)),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0)
|
||||||
|
if r != 0 {
|
||||||
|
return nil, ErrExtractingPublicKey
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalECC(buf []byte, kh uintptr) (*ecdsa.PublicKey, error) {
|
||||||
|
// BCRYPT_ECCKEY_BLOB from bcrypt.h
|
||||||
|
header := struct {
|
||||||
|
Magic uint32
|
||||||
|
Key uint32
|
||||||
|
}{}
|
||||||
|
|
||||||
|
r := bytes.NewReader(buf)
|
||||||
|
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
|
||||||
|
return nil, ErrExtractingECCPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
curve, ok := winCurveIDs[header.Magic]
|
||||||
|
if !ok {
|
||||||
|
// Fix for b/185945636, where despite specifying the curve, nCrypt returns
|
||||||
|
// an incorrect response with BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC.
|
||||||
|
var err error
|
||||||
|
curve, err = winCurveName(kh)
|
||||||
|
if err != nil {
|
||||||
|
// unsupported header magic or cannot match the curve by name
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyX := make([]byte, header.Key)
|
||||||
|
if n, err := r.Read(keyX); n != int(header.Key) || err != nil {
|
||||||
|
// failed to read key X
|
||||||
|
return nil, ErrExtractingECCPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
keyY := make([]byte, header.Key)
|
||||||
|
if n, err := r.Read(keyY); n != int(header.Key) || err != nil {
|
||||||
|
// failed to read key Y
|
||||||
|
return nil, ErrExtractingECCPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := &ecdsa.PublicKey{
|
||||||
|
Curve: curve,
|
||||||
|
X: new(big.Int).SetBytes(keyX),
|
||||||
|
Y: new(big.Int).SetBytes(keyY),
|
||||||
|
}
|
||||||
|
return pub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// winCurveName reads the curve name property and returns the corresponding curve.
|
||||||
|
func winCurveName(kh uintptr) (elliptic.Curve, error) {
|
||||||
|
cn, err := winGetPropertyStr(kh, winNCryptECCCurveNameProperty)
|
||||||
|
if err != nil {
|
||||||
|
// unable to determine the curve property name
|
||||||
|
return nil, ErrExtractPropertyFromKey
|
||||||
|
}
|
||||||
|
curve, ok := winCurveNames[cn]
|
||||||
|
if !ok {
|
||||||
|
// unknown curve name
|
||||||
|
return nil, ErrBadECCCurveName
|
||||||
|
}
|
||||||
|
return curve, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winUnmarshalRSA(buf []byte) (*rsa.PublicKey, error) {
|
||||||
|
// BCRYPT_RSA_BLOB from bcrypt.h
|
||||||
|
header := struct {
|
||||||
|
Magic uint32
|
||||||
|
BitLength uint32
|
||||||
|
PublicExpSize uint32
|
||||||
|
ModulusSize uint32
|
||||||
|
UnusedPrime1 uint32
|
||||||
|
UnusedPrime2 uint32
|
||||||
|
}{}
|
||||||
|
|
||||||
|
r := bytes.NewReader(buf)
|
||||||
|
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if header.Magic != winRSA1Magic {
|
||||||
|
// invalid header magic
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if header.PublicExpSize > 8 {
|
||||||
|
// unsupported public exponent size
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
exp := make([]byte, 8)
|
||||||
|
if n, err := r.Read(exp[8-header.PublicExpSize:]); n != int(header.PublicExpSize) || err != nil {
|
||||||
|
// failed to read public exponent
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := make([]byte, header.ModulusSize)
|
||||||
|
if n, err := r.Read(mod); n != int(header.ModulusSize) || err != nil {
|
||||||
|
// failed to read modulus
|
||||||
|
return nil, ErrExtractingRSAPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := &rsa.PublicKey{
|
||||||
|
N: new(big.Int).SetBytes(mod),
|
||||||
|
E: int(binary.BigEndian.Uint64(exp)),
|
||||||
|
}
|
||||||
|
return pub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeHandle returns a handle to a given cert store, opening the handle as needed.
|
||||||
|
func (w *winCertStore) storeHandle(provider uint32, store *uint16) (windows.Handle, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%d%s", provider, windows.UTF16PtrToString(store))
|
||||||
|
var err error
|
||||||
|
if w.stores[key] == nil {
|
||||||
|
w.stores[key], err = winNewStoreHandle(provider, store)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ErrBadCryptoStoreProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return *w.stores[key].handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify interface conformance.
|
||||||
|
var _ credential = &winKey{}
|
||||||
79
server/certstore/errors.go
Normal file
79
server/certstore/errors.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package certstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrBadCryptoStoreProvider represents inablity to establish link with a certificate store
|
||||||
|
ErrBadCryptoStoreProvider = errors.New("unable to open certificate store or store not available")
|
||||||
|
|
||||||
|
// ErrBadRSAHashAlgorithm represents a bad or unsupported RSA hash algorithm
|
||||||
|
ErrBadRSAHashAlgorithm = errors.New("unsupported RSA hash algorithm")
|
||||||
|
|
||||||
|
// ErrBadSigningAlgorithm represents a bad or unsupported signing algorithm
|
||||||
|
ErrBadSigningAlgorithm = errors.New("unsupported signing algorithm")
|
||||||
|
|
||||||
|
// ErrStoreRSASigningError represents an error returned from store during RSA signature
|
||||||
|
ErrStoreRSASigningError = errors.New("unable to obtain RSA signature from store")
|
||||||
|
|
||||||
|
// ErrStoreECDSASigningError represents an error returned from store during ECDSA signature
|
||||||
|
ErrStoreECDSASigningError = errors.New("unable to obtain ECDSA signature from store")
|
||||||
|
|
||||||
|
// ErrNoPrivateKeyStoreRef represents an error getting a handle to a private key in store
|
||||||
|
ErrNoPrivateKeyStoreRef = errors.New("unable to obtain private key handle from store")
|
||||||
|
|
||||||
|
// ErrExtractingPrivateKeyMetadata represents a family of errors extracting metadata about the private key in store
|
||||||
|
ErrExtractingPrivateKeyMetadata = errors.New("unable to extract private key metadata")
|
||||||
|
|
||||||
|
// ErrExtractingECCPublicKey represents an error exporting ECC-type public key from store
|
||||||
|
ErrExtractingECCPublicKey = errors.New("unable to extract ECC public key from store")
|
||||||
|
|
||||||
|
// ErrExtractingRSAPublicKey represents an error exporting RSA-type public key from store
|
||||||
|
ErrExtractingRSAPublicKey = errors.New("unable to extract RSA public key from store")
|
||||||
|
|
||||||
|
// ErrExtractingPublicKey represents a general error exporting public key from store
|
||||||
|
ErrExtractingPublicKey = errors.New("unable to extract public key from store")
|
||||||
|
|
||||||
|
// ErrBadPublicKeyAlgorithm represents a bad or unsupported public key algorithm
|
||||||
|
ErrBadPublicKeyAlgorithm = errors.New("unsupported public key algorithm")
|
||||||
|
|
||||||
|
// ErrExtractPropertyFromKey represents a general failure to extract a metadata property field
|
||||||
|
ErrExtractPropertyFromKey = errors.New("unable to extract property from key")
|
||||||
|
|
||||||
|
// ErrBadECCCurveName represents an ECC signature curve name that is bad or unsupported
|
||||||
|
ErrBadECCCurveName = errors.New("unsupported ECC curve name")
|
||||||
|
|
||||||
|
// ErrFailedCertSearch represents not able to find certificate in store
|
||||||
|
ErrFailedCertSearch = errors.New("unable to find certificate in store")
|
||||||
|
|
||||||
|
// ErrFailedX509Extract represents not being able to extract x509 certificate from found cert in store
|
||||||
|
ErrFailedX509Extract = errors.New("unable to extract x509 from certificate")
|
||||||
|
|
||||||
|
// ErrBadMatchByType represents unknown CERT_MATCH_BY passed
|
||||||
|
ErrBadMatchByType = errors.New("cert match by type not implemented")
|
||||||
|
|
||||||
|
// ErrBadCertStore represents unknown CERT_STORE passed
|
||||||
|
ErrBadCertStore = errors.New("cert store type not implemented")
|
||||||
|
|
||||||
|
// ErrConflictCertFileAndStore represents ambiguous configuration of both file and store
|
||||||
|
ErrConflictCertFileAndStore = errors.New("'cert_file' and 'cert_store' may not both be configured")
|
||||||
|
|
||||||
|
// ErrBadCertStoreField represents malformed cert_store option
|
||||||
|
ErrBadCertStoreField = errors.New("expected 'cert_store' to be a valid non-empty string")
|
||||||
|
|
||||||
|
// ErrBadCertMatchByField represents malformed cert_match_by option
|
||||||
|
ErrBadCertMatchByField = errors.New("expected 'cert_match_by' to be a valid non-empty string")
|
||||||
|
|
||||||
|
// ErrBadCertMatchField represents malformed cert_match option
|
||||||
|
ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string")
|
||||||
|
|
||||||
|
// ErrBadCaCertMatchField represents malformed cert_match option
|
||||||
|
ErrBadCaCertMatchField = errors.New("expected 'ca_certs_match' to be a valid non-empty string array")
|
||||||
|
|
||||||
|
// ErrBadCertMatchSkipInvalidField represents malformed cert_match_skip_invalid option
|
||||||
|
ErrBadCertMatchSkipInvalidField = errors.New("expected 'cert_match_skip_invalid' to be a boolean")
|
||||||
|
|
||||||
|
// ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS
|
||||||
|
ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system")
|
||||||
|
)
|
||||||
350
server/certstore_windows_test.go
Normal file
350
server/certstore_windows_test.go
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
// Copyright 2022-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runPowershellScript(scriptFile string, args []string) error {
|
||||||
|
psExec, _ := exec.LookPath("powershell.exe")
|
||||||
|
|
||||||
|
execArgs := []string{psExec, "-command", fmt.Sprintf("& '%s'", scriptFile)}
|
||||||
|
if len(args) > 0 {
|
||||||
|
execArgs = append(execArgs, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdImport := &exec.Cmd{
|
||||||
|
Path: psExec,
|
||||||
|
Args: execArgs,
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
return cmdImport.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy string, match string, caMatch string, expectedLeafCount int) {
|
||||||
|
|
||||||
|
// Fire up the leaf
|
||||||
|
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", hubPort))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configStr := fmt.Sprintf(`
|
||||||
|
port: -1
|
||||||
|
leaf {
|
||||||
|
remotes [
|
||||||
|
{
|
||||||
|
url: "%s"
|
||||||
|
tls {
|
||||||
|
cert_store: "%s"
|
||||||
|
cert_match_by: "%s"
|
||||||
|
cert_match: "%s"
|
||||||
|
ca_certs_match: %s
|
||||||
|
|
||||||
|
# Test settings that succeed should be equivalent to:
|
||||||
|
# cert_file: "../test/configs/certs/tlsauth/client.pem"
|
||||||
|
# key_file: "../test/configs/certs/tlsauth/client-key.pem"
|
||||||
|
# ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
||||||
|
timeout: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`, u.String(), certStore, matchBy, match, caMatch)
|
||||||
|
|
||||||
|
leafConfig := createConfFile(t, []byte(configStr))
|
||||||
|
defer removeFile(t, leafConfig)
|
||||||
|
leafServer, _ := RunServerWithConfig(leafConfig)
|
||||||
|
defer leafServer.Shutdown()
|
||||||
|
|
||||||
|
// After client verify, hub will match by SAN email, SAN dns, and Subject (in that order)
|
||||||
|
// Our test client specifies Subject only so we should match on that...
|
||||||
|
|
||||||
|
// A little settle time
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
checkLeafNodeConnectedCount(t, leafServer, expectedLeafCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLeafTLSWindowsCertStore tests the topology of two NATS Servers connected as leaf and hub with authentication of
|
||||||
|
// leaf to hub via mTLS with leaf's certificate and signing key provisioned in the Windows certificate store.
|
||||||
|
func TestLeafTLSWindowsCertStore(t *testing.T) {
|
||||||
|
|
||||||
|
// Client Identity (client.pem)
|
||||||
|
// Issuer: O = NATS CA, OU = NATS.io, CN = localhost
|
||||||
|
// Subject: OU = NATS.io, CN = example.com
|
||||||
|
|
||||||
|
// Make sure windows cert store is reset to avoid conflict with other tests
|
||||||
|
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision Windows cert store with client cert and secret
|
||||||
|
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-client.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision CA to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire up the hub
|
||||||
|
hubConfig := createConfFile(t, []byte(`
|
||||||
|
port: -1
|
||||||
|
leaf {
|
||||||
|
listen: "127.0.0.1:-1"
|
||||||
|
tls {
|
||||||
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
||||||
|
cert_file: "../test/configs/certs/tlsauth/server.pem"
|
||||||
|
key_file: "../test/configs/certs/tlsauth/server-key.pem"
|
||||||
|
timeout: 5
|
||||||
|
verify_and_map: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts: {
|
||||||
|
AcctA: {
|
||||||
|
users: [ {user: "OU = NATS.io, CN = example.com"} ]
|
||||||
|
},
|
||||||
|
AcctB: {
|
||||||
|
users: [ {user: UserB1} ]
|
||||||
|
},
|
||||||
|
SYS: {
|
||||||
|
users: [ {user: System} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
system_account: "SYS"
|
||||||
|
`))
|
||||||
|
defer removeFile(t, hubConfig)
|
||||||
|
hubServer, hubOptions := RunServerWithConfig(hubConfig)
|
||||||
|
defer hubServer.Shutdown()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
certStore string
|
||||||
|
certMatchBy string
|
||||||
|
certMatch string
|
||||||
|
caCertsMatch string
|
||||||
|
expectedLeafCount int
|
||||||
|
}{
|
||||||
|
// Test subject and issuer
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "\"NATS CA\"", 1},
|
||||||
|
{"WindowsCurrentUser", "Issuer", "NATS CA", "\"NATS CA\"", 1},
|
||||||
|
{"WindowsCurrentUser", "Issuer", "Frodo Baggins, Inc.", "\"NATS CA\"", 0},
|
||||||
|
{"WindowsCurrentUser", "Thumbprint", "7e44f478114a2e29b98b00beb1b3687d8dc0e481", "\"NATS CA\"", 0},
|
||||||
|
// Test CAs, NATS CA is valid, others are missing
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\"]", 1},
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"GlobalSign\"]", 0},
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing NATS Cert\"]", 0},
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\", \"Missing NATS Cert1\"]", 1},
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing Cert2\",\"NATS CA\"]", 1},
|
||||||
|
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing, Cert3\",\"Missing NATS Cert4\"]", 0},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
testName := fmt.Sprintf("%s by %s match %s", tc.certStore, tc.certMatchBy, tc.certMatch)
|
||||||
|
t.Run(fmt.Sprintf(testName, tc.certStore, tc.certMatchBy, tc.certMatch, tc.caCertsMatch), func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if tc.expectedLeafCount != 0 {
|
||||||
|
t.Fatalf("did not expect panic: %s", testName)
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(fmt.Sprintf("%v", r), "Error processing configuration file") {
|
||||||
|
t.Fatalf("did not expect unknown panic: %s", testName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
runConfiguredLeaf(t, hubOptions.LeafNode.Port,
|
||||||
|
tc.certStore, tc.certMatchBy, tc.certMatch,
|
||||||
|
tc.caCertsMatch, tc.expectedLeafCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerTLSWindowsCertStore tests the topology of a NATS server requiring TLS and gettings it own server
|
||||||
|
// cert identity (as used when accepting NATS client connections and negotiating TLS) from Windows certificate store.
|
||||||
|
func TestServerTLSWindowsCertStore(t *testing.T) {
|
||||||
|
|
||||||
|
// Server Identity (server.pem)
|
||||||
|
// Issuer: O = NATS CA, OU = NATS.io, CN = localhost
|
||||||
|
// Subject: OU = NATS.io Operators, CN = localhost
|
||||||
|
|
||||||
|
// Make sure windows cert store is reset to avoid conflict with other tests
|
||||||
|
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision Windows cert store with server cert and secret
|
||||||
|
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision CA to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire up the server
|
||||||
|
srvConfig := createConfFile(t, []byte(`
|
||||||
|
listen: "localhost:-1"
|
||||||
|
tls {
|
||||||
|
cert_store: "WindowsCurrentUser"
|
||||||
|
cert_match_by: "Subject"
|
||||||
|
cert_match: "NATS.io Operators"
|
||||||
|
ca_certs_match: ["NATS CA"]
|
||||||
|
timeout: 5
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
defer removeFile(t, srvConfig)
|
||||||
|
srvServer, _ := RunServerWithConfig(srvConfig)
|
||||||
|
if srvServer == nil {
|
||||||
|
t.Fatalf("expected to be able start server with cert store configuration")
|
||||||
|
}
|
||||||
|
defer srvServer.Shutdown()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
clientCA string
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{"../test/configs/certs/tlsauth/ca.pem", true},
|
||||||
|
{"../test/configs/certs/tlsauth/client.pem", false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("Client CA: %s", tc.clientCA), func(t *testing.T) {
|
||||||
|
nc, _ := nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA))
|
||||||
|
err := nc.Publish("foo", []byte("hello TLS server-authenticated server"))
|
||||||
|
if (err != nil) == tc.expect {
|
||||||
|
t.Fatalf("expected publish result %v to TLS authenticated server", tc.expect)
|
||||||
|
}
|
||||||
|
nc.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
nc, _ = nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA))
|
||||||
|
err = nc.Publish("foo", []byte("hello TLS server-authenticated server"))
|
||||||
|
if (err != nil) == tc.expect {
|
||||||
|
t.Fatalf("expected repeated connection result %v to TLS authenticated server", tc.expect)
|
||||||
|
}
|
||||||
|
nc.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerIgnoreExpiredCerts tests if the server skips expired certificates in configuration, and finds non-expired ones
|
||||||
|
func TestServerIgnoreExpiredCerts(t *testing.T) {
|
||||||
|
|
||||||
|
// Server Identities: expired.pem; not-expired.pem
|
||||||
|
// Issuer: OU = NATS.io, CN = localhost
|
||||||
|
// Subject: OU = NATS.io Operators, CN = localhost
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
certFile string
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{"expired.p12", false},
|
||||||
|
{"not-expired.p12", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("Server certificate: %s", tc.certFile), func(t *testing.T) {
|
||||||
|
// Make sure windows cert store is reset to avoid conflict with other tests
|
||||||
|
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision Windows cert store with server cert and secret
|
||||||
|
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", []string{tc.certFile})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
|
||||||
|
}
|
||||||
|
// Fire up the server
|
||||||
|
srvConfig := createConfFile(t, []byte(`
|
||||||
|
listen: "localhost:-1"
|
||||||
|
tls {
|
||||||
|
cert_store: "WindowsCurrentUser"
|
||||||
|
cert_match_by: "Subject"
|
||||||
|
cert_match: "NATS.io Operators"
|
||||||
|
cert_match_skip_invalid: true
|
||||||
|
timeout: 5
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
defer removeFile(t, srvConfig)
|
||||||
|
cfg, _ := ProcessConfigFile(srvConfig)
|
||||||
|
if (cfg != nil) == tc.expect {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tc.expect == false {
|
||||||
|
t.Fatalf("expected server start to fail with expired certificate")
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected server to start with non expired certificate")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsTLS12ECDSA(t *testing.T) {
|
||||||
|
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", []string{"ecdsa_server.pfx"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected powershell provision to succeed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := createConfFile(t, []byte(`
|
||||||
|
listen: "localhost:-1"
|
||||||
|
tls {
|
||||||
|
cert_store: "WindowsCurrentUser"
|
||||||
|
cert_match_by: "Thumbprint"
|
||||||
|
cert_match: "4F8AF21756E5DBBD54619BBB6F3CC5D455ED4468"
|
||||||
|
cert_match_skip_invalid: true
|
||||||
|
timeout: 5
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
defer removeFile(t, config)
|
||||||
|
|
||||||
|
srv, _ := RunServerWithConfig(config)
|
||||||
|
if srv == nil {
|
||||||
|
t.Fatalf("expected to be able start server with cert store configuration")
|
||||||
|
}
|
||||||
|
defer srv.Shutdown()
|
||||||
|
|
||||||
|
for name, version := range map[string]uint16{
|
||||||
|
"TLS 1.3": tls.VersionTLS13,
|
||||||
|
"TLS 1.2": tls.VersionTLS12,
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
tc := &tls.Config{MaxVersion: version, MinVersion: version, InsecureSkipVerify: true}
|
||||||
|
|
||||||
|
if _, err = nats.Connect(srv.clientConnectURLs[0], nats.Secure(tc)); err != nil {
|
||||||
|
t.Fatalf("connection with %s: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("successful connection with %s", name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
79
server/ciphersuites.go
Normal file
79
server/ciphersuites.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2016-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/fips140"
|
||||||
|
"crypto/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, cs := range tls.CipherSuites() {
|
||||||
|
cipherMap[cs.Name] = cs
|
||||||
|
cipherMapByID[cs.ID] = cs
|
||||||
|
}
|
||||||
|
for _, cs := range tls.InsecureCipherSuites() {
|
||||||
|
cipherMap[cs.Name] = cs
|
||||||
|
cipherMapByID[cs.ID] = cs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cipherMap = map[string]*tls.CipherSuite{}
|
||||||
|
var cipherMapByID = map[uint16]*tls.CipherSuite{}
|
||||||
|
|
||||||
|
// Where we maintain available curve preferences
|
||||||
|
var curvePreferenceMap = map[string]tls.CurveID{
|
||||||
|
"SecP384r1MLKEM1024": tls.SecP384r1MLKEM1024,
|
||||||
|
"SecP256r1MLKEM768": tls.SecP256r1MLKEM768,
|
||||||
|
"X25519MLKEM768": tls.X25519MLKEM768,
|
||||||
|
"X25519": tls.X25519,
|
||||||
|
"CurveP256": tls.CurveP256,
|
||||||
|
"CurveP384": tls.CurveP384,
|
||||||
|
"CurveP521": tls.CurveP521,
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultCipherSuites() []uint16 {
|
||||||
|
ciphers := tls.CipherSuites()
|
||||||
|
defaults := make([]uint16, 0, len(ciphers))
|
||||||
|
for _, cs := range ciphers {
|
||||||
|
defaults = append(defaults, cs.ID)
|
||||||
|
}
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
// reorder to default to the highest level of security. See:
|
||||||
|
// https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
|
||||||
|
func defaultCurvePreferences() []tls.CurveID {
|
||||||
|
if fips140.Enabled() {
|
||||||
|
// X25519 is not FIPS-approved by itself, but it is when combined with MLKEM768.
|
||||||
|
// SecP256r1MLKEM768 and SecP384r1MLKEM1024 are both FIPS-approved hybrids.
|
||||||
|
return []tls.CurveID{
|
||||||
|
tls.X25519MLKEM768, // post-quantum
|
||||||
|
tls.SecP384r1MLKEM1024, // post-quantum
|
||||||
|
tls.SecP256r1MLKEM768, // post-quantum
|
||||||
|
tls.CurveP256,
|
||||||
|
tls.CurveP384,
|
||||||
|
tls.CurveP521,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []tls.CurveID{
|
||||||
|
tls.X25519MLKEM768, // post-quantum
|
||||||
|
tls.SecP384r1MLKEM1024, // post-quantum
|
||||||
|
tls.SecP256r1MLKEM768, // post-quantum
|
||||||
|
tls.X25519, // faster than P256, arguably more secure
|
||||||
|
tls.CurveP256,
|
||||||
|
tls.CurveP384,
|
||||||
|
tls.CurveP521,
|
||||||
|
}
|
||||||
|
}
|
||||||
415
server/client_proxyproto.go
Normal file
415
server/client_proxyproto.go
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
// Copyright 2025-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PROXY protocol v2 constants
|
||||||
|
const (
|
||||||
|
// Protocol signature (12 bytes)
|
||||||
|
proxyProtoV2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
|
||||||
|
|
||||||
|
// Version and command byte format: version(4 bits) | command(4 bits)
|
||||||
|
proxyProtoV2VerMask = 0xF0
|
||||||
|
proxyProtoV2Ver = 0x20 // Version 2
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
proxyProtoCmdMask = 0x0F
|
||||||
|
proxyProtoCmdLocal = 0x00 // LOCAL command (health check, use original connection)
|
||||||
|
proxyProtoCmdProxy = 0x01 // PROXY command (proxied connection)
|
||||||
|
|
||||||
|
// Address family and protocol byte format: family(4 bits) | protocol(4 bits)
|
||||||
|
proxyProtoFamilyMask = 0xF0
|
||||||
|
proxyProtoFamilyUnspec = 0x00 // Unspecified
|
||||||
|
proxyProtoFamilyInet = 0x10 // IPv4
|
||||||
|
proxyProtoFamilyInet6 = 0x20 // IPv6
|
||||||
|
proxyProtoFamilyUnix = 0x30 // Unix socket
|
||||||
|
proxyProtoProtoMask = 0x0F
|
||||||
|
proxyProtoProtoUnspec = 0x00 // Unspecified
|
||||||
|
proxyProtoProtoStream = 0x01 // TCP/STREAM
|
||||||
|
proxyProtoProtoDatagram = 0x02 // UDP/DGRAM
|
||||||
|
|
||||||
|
// Address sizes
|
||||||
|
proxyProtoAddrSizeIPv4 = 12 // 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port)
|
||||||
|
proxyProtoAddrSizeIPv6 = 36 // 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port)
|
||||||
|
|
||||||
|
// Header sizes
|
||||||
|
proxyProtoV2HeaderSize = 16 // Fixed header: 12 (sig) + 1 (ver/cmd) + 1 (fam/proto) + 2 (addr len)
|
||||||
|
|
||||||
|
// Timeout for reading PROXY protocol header
|
||||||
|
proxyProtoReadTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// PROXY protocol v1 constants
|
||||||
|
const (
|
||||||
|
proxyProtoV1Prefix = "PROXY "
|
||||||
|
proxyProtoV1MaxLineLen = 107 // Maximum line length including CRLF
|
||||||
|
proxyProtoV1TCP4 = "TCP4"
|
||||||
|
proxyProtoV1TCP6 = "TCP6"
|
||||||
|
proxyProtoV1Unknown = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Errors
|
||||||
|
errProxyProtoInvalid = errors.New("invalid PROXY protocol header")
|
||||||
|
errProxyProtoUnsupported = errors.New("unsupported PROXY protocol feature")
|
||||||
|
errProxyProtoTimeout = errors.New("timeout reading PROXY protocol header")
|
||||||
|
errProxyProtoUnrecognized = errors.New("unrecognized PROXY protocol format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// proxyProtoAddr contains the address information extracted from PROXY protocol header
|
||||||
|
type proxyProtoAddr struct {
|
||||||
|
srcIP net.IP
|
||||||
|
srcPort uint16
|
||||||
|
dstIP net.IP
|
||||||
|
dstPort uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements net.Addr interface
|
||||||
|
func (p *proxyProtoAddr) String() string {
|
||||||
|
return net.JoinHostPort(p.srcIP.String(), fmt.Sprintf("%d", p.srcPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network implements net.Addr interface
|
||||||
|
func (p *proxyProtoAddr) Network() string {
|
||||||
|
if p.srcIP.To4() != nil {
|
||||||
|
return "tcp4"
|
||||||
|
}
|
||||||
|
return "tcp6"
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyConn wraps a net.Conn to override RemoteAddr() with the address
|
||||||
|
// extracted from the PROXY protocol header
|
||||||
|
type proxyConn struct {
|
||||||
|
net.Conn
|
||||||
|
remoteAddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteAddr returns the original client address extracted from PROXY protocol
|
||||||
|
func (pc *proxyConn) RemoteAddr() net.Addr {
|
||||||
|
return pc.remoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectProxyProtoVersion reads the first bytes and determines protocol version.
|
||||||
|
// Returns 1 for v1, 2 for v2, or error.
|
||||||
|
// The first 6 bytes read are returned so they can be used by the parser.
|
||||||
|
func detectProxyProtoVersion(conn net.Conn) (version int, header []byte, err error) {
|
||||||
|
// Read first 6 bytes to check for "PROXY " or v2 signature
|
||||||
|
header = make([]byte, 6)
|
||||||
|
if _, err = io.ReadFull(conn, header); err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to read protocol version: %w", err)
|
||||||
|
}
|
||||||
|
switch bytesToString(header) {
|
||||||
|
case proxyProtoV1Prefix:
|
||||||
|
return 1, header, nil
|
||||||
|
case proxyProtoV2Sig[:6]:
|
||||||
|
return 2, header, nil
|
||||||
|
default:
|
||||||
|
// Return the consumed bytes so the caller can replay them into the
|
||||||
|
// next protocol layer instead of discarding them.
|
||||||
|
return 0, header, errProxyProtoUnrecognized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readProxyProtoV1Header parses PROXY protocol v1 text format.
|
||||||
|
// Expects the "PROXY " prefix (6 bytes) to have already been consumed.
|
||||||
|
// Returns any bytes that were read past the trailing CRLF so the caller can
|
||||||
|
// replay them into the next protocol layer.
|
||||||
|
func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, []byte, error) {
|
||||||
|
// Read rest of line (max 107 bytes total, already read 6)
|
||||||
|
maxRemaining := proxyProtoV1MaxLineLen - 6
|
||||||
|
|
||||||
|
// Read up to maxRemaining bytes at once (more efficient than byte-by-byte)
|
||||||
|
buf := make([]byte, maxRemaining)
|
||||||
|
var line []byte
|
||||||
|
var remaining []byte
|
||||||
|
|
||||||
|
for len(line) < maxRemaining {
|
||||||
|
// Read available data
|
||||||
|
n, err := conn.Read(buf[len(line):])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to read v1 line: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
line = buf[:len(line)+n]
|
||||||
|
|
||||||
|
// Look for CRLF in what we've read so far
|
||||||
|
for i := 0; i < len(line)-1; i++ {
|
||||||
|
if line[i] == '\r' && line[i+1] == '\n' {
|
||||||
|
// Found CRLF - keep any over-read bytes for the client parser.
|
||||||
|
remaining = append(remaining, line[i+2:]...)
|
||||||
|
line = line[:i]
|
||||||
|
goto foundCRLF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exceeded max length without finding CRLF
|
||||||
|
return nil, nil, fmt.Errorf("%w: v1 line too long", errProxyProtoInvalid)
|
||||||
|
|
||||||
|
foundCRLF:
|
||||||
|
// Get parts from the protocol
|
||||||
|
parts := strings.Fields(string(line))
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle UNKNOWN (health check, like v2 LOCAL)
|
||||||
|
if parts[0] == proxyProtoV1Unknown {
|
||||||
|
return nil, remaining, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have exactly 5 parts: protocol, src-ip, dst-ip, src-port, dst-port
|
||||||
|
if len(parts) != 5 {
|
||||||
|
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := parts[0]
|
||||||
|
srcIP := net.ParseIP(parts[1])
|
||||||
|
dstIP := net.ParseIP(parts[2])
|
||||||
|
|
||||||
|
if srcIP == nil || dstIP == nil {
|
||||||
|
return nil, nil, fmt.Errorf("%w: invalid address", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ports
|
||||||
|
srcPort, err := strconv.ParseUint(parts[3], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid source port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstPort, err := strconv.ParseUint(parts[4], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid dest port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate protocol matches IP version. The textual form determines the
|
||||||
|
// family: TCP4 requires dotted-quad addresses, TCP6 requires IPv6
|
||||||
|
// addresses. IPv4-mapped IPv6 addresses (e.g. "::ffff:192.0.2.1") are
|
||||||
|
// valid for TCP6 since dual-stack proxies can emit those for IPv4
|
||||||
|
// clients on IPv6 sockets, matching the v2 parser behavior.
|
||||||
|
srcIsV6 := strings.Contains(parts[1], ":")
|
||||||
|
dstIsV6 := strings.Contains(parts[2], ":")
|
||||||
|
if protocol == proxyProtoV1TCP4 && (srcIsV6 || dstIsV6) {
|
||||||
|
return nil, nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
if protocol == proxyProtoV1TCP6 && (!srcIsV6 || !dstIsV6) {
|
||||||
|
return nil, nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
if protocol != proxyProtoV1TCP4 && protocol != proxyProtoV1TCP6 {
|
||||||
|
return nil, nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proxyProtoAddr{
|
||||||
|
srcIP: srcIP,
|
||||||
|
srcPort: uint16(srcPort),
|
||||||
|
dstIP: dstIP,
|
||||||
|
dstPort: uint16(dstPort),
|
||||||
|
}, remaining, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readProxyProtoHeader reads and parses PROXY protocol (v1 or v2) from the connection.
|
||||||
|
// Automatically detects version and routes to appropriate parser.
|
||||||
|
// If the command is LOCAL/UNKNOWN (health check), it returns nil for addr and no error.
|
||||||
|
// If the command is PROXY, it returns the parsed address information.
|
||||||
|
// It also returns any bytes that were read past the v1 header terminator so the
|
||||||
|
// caller can replay them into the normal client parser.
|
||||||
|
// The connection must be fresh (no data read yet).
|
||||||
|
func readProxyProtoHeader(conn net.Conn) (*proxyProtoAddr, []byte, error) {
|
||||||
|
// Set read deadline to prevent hanging on slow/malicious clients
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
// Detect version.
|
||||||
|
// On errProxyProtoUnrecognized, firstBytes holds the bytes that were
|
||||||
|
// consumed so the caller can replay them.
|
||||||
|
version, firstBytes, err := detectProxyProtoVersion(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, firstBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch version {
|
||||||
|
case 1:
|
||||||
|
// v1 parser expects "PROXY " prefix already consumed
|
||||||
|
return readProxyProtoV1Header(conn)
|
||||||
|
case 2:
|
||||||
|
// Read rest of v2 signature (bytes 6-11, total 6 more bytes)
|
||||||
|
remaining := make([]byte, 6)
|
||||||
|
if _, err := io.ReadFull(conn, remaining); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to read v2 signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify full signature
|
||||||
|
fullSig := string(firstBytes) + string(remaining)
|
||||||
|
if fullSig != proxyProtoV2Sig {
|
||||||
|
return nil, nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read rest of header: ver/cmd, fam/proto, addr-len (4 bytes)
|
||||||
|
header := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to read v2 header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with parsing
|
||||||
|
addr, err := parseProxyProtoV2Header(conn, header)
|
||||||
|
return addr, nil, err
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readProxyProtoV2Header is kept for backward compatibility and direct testing.
|
||||||
|
// It reads and parses a PROXY protocol v2 header from the connection.
|
||||||
|
// If the command is LOCAL (health check), it returns nil for addr and no error.
|
||||||
|
// If the command is PROXY, it returns the parsed address information.
|
||||||
|
// The connection must be fresh (no data read yet).
|
||||||
|
func readProxyProtoV2Header(conn net.Conn) (*proxyProtoAddr, error) {
|
||||||
|
// Set read deadline to prevent hanging on slow/malicious clients
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
// Read fixed header (16 bytes)
|
||||||
|
header := make([]byte, proxyProtoV2HeaderSize)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
return nil, errProxyProtoTimeout
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read PROXY protocol header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate signature (first 12 bytes)
|
||||||
|
if string(header[:12]) != proxyProtoV2Sig {
|
||||||
|
return nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with parsing after signature
|
||||||
|
return parseProxyProtoV2Header(conn, header[12:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseProxyProtoV2Header parses v2 protocol after signature has been validated.
|
||||||
|
// header contains the 4 bytes: ver/cmd, fam/proto, addr-len (2 bytes).
|
||||||
|
func parseProxyProtoV2Header(conn net.Conn, header []byte) (*proxyProtoAddr, error) {
|
||||||
|
// Parse version and command
|
||||||
|
verCmd := header[0]
|
||||||
|
version := verCmd & proxyProtoV2VerMask
|
||||||
|
command := verCmd & proxyProtoCmdMask
|
||||||
|
|
||||||
|
if version != proxyProtoV2Ver {
|
||||||
|
return nil, fmt.Errorf("%w: invalid version 0x%02x", errProxyProtoInvalid, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse address family and protocol
|
||||||
|
famProto := header[1]
|
||||||
|
family := famProto & proxyProtoFamilyMask
|
||||||
|
protocol := famProto & proxyProtoProtoMask
|
||||||
|
|
||||||
|
// Parse address length (big-endian uint16)
|
||||||
|
addrLen := binary.BigEndian.Uint16(header[2:4])
|
||||||
|
|
||||||
|
// Handle LOCAL command (health check)
|
||||||
|
if command == proxyProtoCmdLocal {
|
||||||
|
// For LOCAL, we should skip the address data if any
|
||||||
|
if addrLen > 0 {
|
||||||
|
// Discard the address data
|
||||||
|
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discard LOCAL command address data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil // nil addr indicates LOCAL command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PROXY command
|
||||||
|
if command != proxyProtoCmdProxy {
|
||||||
|
return nil, fmt.Errorf("unknown PROXY protocol command: 0x%02x", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate protocol (we only support STREAM/TCP)
|
||||||
|
if protocol != proxyProtoProtoStream {
|
||||||
|
return nil, fmt.Errorf("%w: only STREAM protocol supported", errProxyProtoUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse address data based on family
|
||||||
|
var addr *proxyProtoAddr
|
||||||
|
var err error
|
||||||
|
switch family {
|
||||||
|
case proxyProtoFamilyInet:
|
||||||
|
addr, err = parseIPv4Addr(conn, addrLen)
|
||||||
|
case proxyProtoFamilyInet6:
|
||||||
|
addr, err = parseIPv6Addr(conn, addrLen)
|
||||||
|
case proxyProtoFamilyUnspec:
|
||||||
|
// UNSPEC family with PROXY command is valid but rare
|
||||||
|
// Just skip the address data
|
||||||
|
if addrLen > 0 {
|
||||||
|
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discard UNSPEC address address data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: unsupported address family 0x%02x", errProxyProtoUnsupported, family)
|
||||||
|
}
|
||||||
|
return addr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIPv4Addr parses IPv4 address data from PROXY protocol header
|
||||||
|
func parseIPv4Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
|
||||||
|
// IPv4: 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port) = 12 bytes minimum
|
||||||
|
if addrLen < proxyProtoAddrSizeIPv4 {
|
||||||
|
return nil, fmt.Errorf("IPv4 address data too short: %d bytes", addrLen)
|
||||||
|
}
|
||||||
|
addrData := make([]byte, addrLen)
|
||||||
|
if _, err := io.ReadFull(conn, addrData); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read IPv4 address data: %w", err)
|
||||||
|
}
|
||||||
|
return &proxyProtoAddr{
|
||||||
|
srcIP: net.IP(addrData[0:4]),
|
||||||
|
dstIP: net.IP(addrData[4:8]),
|
||||||
|
srcPort: binary.BigEndian.Uint16(addrData[8:10]),
|
||||||
|
dstPort: binary.BigEndian.Uint16(addrData[10:12]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIPv6Addr parses IPv6 address data from PROXY protocol header
|
||||||
|
func parseIPv6Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
|
||||||
|
// IPv6: 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port) = 36 bytes minimum
|
||||||
|
if addrLen < proxyProtoAddrSizeIPv6 {
|
||||||
|
return nil, fmt.Errorf("IPv6 address data too short: %d bytes", addrLen)
|
||||||
|
}
|
||||||
|
addrData := make([]byte, addrLen)
|
||||||
|
if _, err := io.ReadFull(conn, addrData); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read IPv6 address data: %w", err)
|
||||||
|
}
|
||||||
|
return &proxyProtoAddr{
|
||||||
|
srcIP: net.IP(addrData[0:16]),
|
||||||
|
dstIP: net.IP(addrData[16:32]),
|
||||||
|
srcPort: binary.BigEndian.Uint16(addrData[32:34]),
|
||||||
|
dstPort: binary.BigEndian.Uint16(addrData[34:36]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
925
server/client_proxyproto_test.go
Normal file
925
server/client_proxyproto_test.go
Normal file
@@ -0,0 +1,925 @@
|
|||||||
|
// Copyright 2025-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockConn is a mock net.Conn for testing
|
||||||
|
type mockConn struct {
|
||||||
|
net.Conn
|
||||||
|
readBuf *bytes.Buffer
|
||||||
|
writeBuf *bytes.Buffer
|
||||||
|
closed bool
|
||||||
|
deadline time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockConn(data []byte) *mockConn {
|
||||||
|
return &mockConn{
|
||||||
|
readBuf: bytes.NewBuffer(data),
|
||||||
|
writeBuf: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) Read(b []byte) (int, error) {
|
||||||
|
if m.closed {
|
||||||
|
return 0, fmt.Errorf("connection closed")
|
||||||
|
}
|
||||||
|
if !m.deadline.IsZero() && time.Now().After(m.deadline) {
|
||||||
|
return 0, &net.OpError{Op: "read", Err: fmt.Errorf("timeout")}
|
||||||
|
}
|
||||||
|
return m.readBuf.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) Write(b []byte) (int, error) {
|
||||||
|
if m.closed {
|
||||||
|
return 0, fmt.Errorf("connection closed")
|
||||||
|
}
|
||||||
|
return m.writeBuf.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) Close() error {
|
||||||
|
m.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) LocalAddr() net.Addr {
|
||||||
|
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4222}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) RemoteAddr() net.Addr {
|
||||||
|
return &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) SetDeadline(t time.Time) error {
|
||||||
|
m.deadline = t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) SetReadDeadline(t time.Time) error {
|
||||||
|
m.deadline = t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProxyV2Header builds a valid PROXY protocol v2 header
|
||||||
|
func buildProxyV2Header(t *testing.T, srcIP, dstIP string, srcPort, dstPort uint16, family byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig) // Write signature
|
||||||
|
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy) // Write version and command (version 2, PROXY command)
|
||||||
|
buf.WriteByte(family | proxyProtoProtoStream) // Write family and protocol
|
||||||
|
|
||||||
|
src := net.ParseIP(srcIP)
|
||||||
|
dst := net.ParseIP(dstIP)
|
||||||
|
var addrData []byte
|
||||||
|
switch family {
|
||||||
|
case proxyProtoFamilyInet:
|
||||||
|
// IPv4: 12 bytes
|
||||||
|
addrData = make([]byte, proxyProtoAddrSizeIPv4)
|
||||||
|
copy(addrData[0:4], src.To4())
|
||||||
|
copy(addrData[4:8], dst.To4())
|
||||||
|
binary.BigEndian.PutUint16(addrData[8:10], srcPort)
|
||||||
|
binary.BigEndian.PutUint16(addrData[10:12], dstPort)
|
||||||
|
case proxyProtoFamilyInet6:
|
||||||
|
// IPv6: 36 bytes
|
||||||
|
addrData = make([]byte, proxyProtoAddrSizeIPv6)
|
||||||
|
copy(addrData[0:16], src.To16())
|
||||||
|
copy(addrData[16:32], dst.To16())
|
||||||
|
binary.BigEndian.PutUint16(addrData[32:34], srcPort)
|
||||||
|
binary.BigEndian.PutUint16(addrData[34:36], dstPort)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported address family: %d", family)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrLen := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(addrLen, uint16(len(addrData)))
|
||||||
|
buf.Write(addrLen)
|
||||||
|
buf.Write(addrData)
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProxyV2LocalHeader builds a PROXY protocol v2 LOCAL command header
|
||||||
|
func buildProxyV2LocalHeader() []byte {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig) // // Write signature
|
||||||
|
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdLocal) // // Write version and command (version 2, LOCAL command)
|
||||||
|
buf.WriteByte(proxyProtoFamilyUnspec | proxyProtoProtoUnspec) // // Write family and protocol (UNSPEC)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProxyV1Header builds a valid PROXY protocol v1 header (text format)
|
||||||
|
func buildProxyV1Header(t *testing.T, protocol, srcIP, dstIP string, srcPort, dstPort uint16) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if protocol != "TCP4" && protocol != "TCP6" && protocol != "UNKNOWN" {
|
||||||
|
t.Fatalf("invalid protocol: %s", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
var line string
|
||||||
|
if protocol == "UNKNOWN" {
|
||||||
|
line = "PROXY UNKNOWN\r\n"
|
||||||
|
} else {
|
||||||
|
line = fmt.Sprintf("PROXY %s %s %s %d %d\r\n", protocol, srcIP, dstIP, srcPort, dstPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2ParseIPv4(t *testing.T) {
|
||||||
|
header := buildProxyV2Header(t, "192.168.1.50", "10.0.0.1", 12345, 4222, proxyProtoFamilyInet)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, err := readProxyProtoV2Header(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
|
||||||
|
require_Equal(t, addr.srcIP.String(), "192.168.1.50")
|
||||||
|
require_Equal(t, addr.srcPort, 12345)
|
||||||
|
|
||||||
|
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
|
||||||
|
require_Equal(t, addr.dstPort, 4222)
|
||||||
|
|
||||||
|
// Test String() and Network() methods
|
||||||
|
require_Equal(t, addr.String(), "192.168.1.50:12345")
|
||||||
|
require_Equal(t, addr.Network(), "tcp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2ParseIPv6(t *testing.T) {
|
||||||
|
header := buildProxyV2Header(t, "2001:db8::1", "2001:db8::2", 54321, 4222, proxyProtoFamilyInet6)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, err := readProxyProtoV2Header(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
|
||||||
|
require_Equal(t, addr.srcIP.String(), "2001:db8::1")
|
||||||
|
require_Equal(t, addr.srcPort, 54321)
|
||||||
|
|
||||||
|
require_Equal(t, addr.dstIP.String(), "2001:db8::2")
|
||||||
|
require_Equal(t, addr.dstPort, 4222)
|
||||||
|
|
||||||
|
// Test Network() method for IPv6
|
||||||
|
require_Equal(t, addr.String(), "[2001:db8::1]:54321")
|
||||||
|
require_Equal(t, addr.Network(), "tcp6")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2ParseLocalCommand(t *testing.T) {
|
||||||
|
header := buildProxyV2LocalHeader()
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, err := readProxyProtoV2Header(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, addr == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2InvalidSignature(t *testing.T) {
|
||||||
|
// Create invalid signature
|
||||||
|
header := []byte("INVALID_SIG_")
|
||||||
|
header = append(header, []byte{0x20, 0x11, 0x00, 0x0C}...)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2InvalidVersion(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig)
|
||||||
|
buf.WriteByte(0x10 | proxyProtoCmdProxy) // Version 1 instead of 2
|
||||||
|
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoStream)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
|
||||||
|
conn := newMockConn(buf.Bytes())
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2UnsupportedFamily(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig)
|
||||||
|
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
|
||||||
|
buf.WriteByte(proxyProtoFamilyUnix | proxyProtoProtoStream) // Unix socket family
|
||||||
|
buf.WriteByte(0)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
|
||||||
|
conn := newMockConn(buf.Bytes())
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, errProxyProtoUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2UnsupportedProtocol(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig)
|
||||||
|
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
|
||||||
|
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoDatagram) // UDP instead of TCP
|
||||||
|
buf.WriteByte(0)
|
||||||
|
buf.WriteByte(12)
|
||||||
|
|
||||||
|
conn := newMockConn(buf.Bytes())
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, errProxyProtoUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2TruncatedHeader(t *testing.T) {
|
||||||
|
header := buildProxyV2Header(t, "192.168.1.50", "10.0.0.1", 12345, 4222, proxyProtoFamilyInet)
|
||||||
|
// Only send first 10 bytes (incomplete header)
|
||||||
|
conn := newMockConn(header[:10])
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, io.ErrUnexpectedEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2ShortAddressData(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString(proxyProtoV2Sig)
|
||||||
|
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
|
||||||
|
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoStream)
|
||||||
|
// Set address length to 12 but don't provide data
|
||||||
|
buf.WriteByte(0)
|
||||||
|
buf.WriteByte(12)
|
||||||
|
// Only provide 5 bytes instead of 12
|
||||||
|
buf.Write([]byte{1, 2, 3, 4, 5})
|
||||||
|
|
||||||
|
conn := newMockConn(buf.Bytes())
|
||||||
|
|
||||||
|
_, err := readProxyProtoV2Header(conn)
|
||||||
|
require_Error(t, err, io.ErrUnexpectedEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxyConnRemoteAddr(t *testing.T) {
|
||||||
|
// Create a real TCP connection for testing
|
||||||
|
originalAddr := &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}
|
||||||
|
|
||||||
|
// Create proxy address
|
||||||
|
proxyAddr := &proxyProtoAddr{
|
||||||
|
srcIP: net.ParseIP("10.0.0.50"),
|
||||||
|
srcPort: 12345,
|
||||||
|
dstIP: net.ParseIP("10.0.0.1"),
|
||||||
|
dstPort: 4222,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockConn := newMockConn(nil)
|
||||||
|
wrapped := &proxyConn{
|
||||||
|
Conn: mockConn,
|
||||||
|
remoteAddr: proxyAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify RemoteAddr returns the proxied address
|
||||||
|
addr := wrapped.RemoteAddr()
|
||||||
|
require_Equal(t, addr.String(), "10.0.0.50:12345")
|
||||||
|
require_Equal(t, mockConn.RemoteAddr().String(), originalAddr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2EndToEnd(t *testing.T) {
|
||||||
|
// Start a test server with PROXY protocol enabled
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1 // Random port
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
// Get the server's listening port
|
||||||
|
addr := s.Addr().String()
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
require_NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Send PROXY protocol header
|
||||||
|
clientIP := "203.0.113.50"
|
||||||
|
clientPort := uint16(54321)
|
||||||
|
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
|
||||||
|
|
||||||
|
_, err = conn.Write(header)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
// Send CONNECT message
|
||||||
|
connectMsg := "CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n"
|
||||||
|
_, err = conn.Write([]byte(connectMsg))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
// Read INFO and +OK
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
response := string(buf[:n])
|
||||||
|
require_True(t, strings.Contains(response, "INFO"))
|
||||||
|
|
||||||
|
// Give server time to process
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check server's client list to verify the IP was extracted correctly
|
||||||
|
s.mu.Lock()
|
||||||
|
clients := s.clients
|
||||||
|
s.mu.Unlock()
|
||||||
|
require_True(t, len(clients) != 0)
|
||||||
|
|
||||||
|
// Find our client
|
||||||
|
var foundClient *client
|
||||||
|
for _, c := range clients {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.host == clientIP && c.port == clientPort {
|
||||||
|
foundClient = c
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
if foundClient != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require_NotNil(t, foundClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV2LocalCommandEndToEnd(t *testing.T) {
|
||||||
|
// Start a test server with PROXY protocol enabled
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1 // Random port
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
// Get the server's listening port
|
||||||
|
addr := s.Addr().String()
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
require_NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Send PROXY protocol LOCAL header (health check)
|
||||||
|
header := buildProxyV2LocalHeader()
|
||||||
|
|
||||||
|
_, err = conn.Write(header)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
// Send CONNECT message
|
||||||
|
connectMsg := "CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n"
|
||||||
|
_, err = conn.Write([]byte(connectMsg))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
// Read INFO and +OK
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
response := string(buf[:n])
|
||||||
|
require_True(t, strings.Contains(response, "INFO"))
|
||||||
|
|
||||||
|
// Connection should work normally with LOCAL command
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify at least one client is connected
|
||||||
|
s.mu.Lock()
|
||||||
|
numClients := len(s.clients)
|
||||||
|
s.mu.Unlock()
|
||||||
|
require_NotEqual(t, numClients, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PROXY Protocol v1 Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1ParseTCP4(t *testing.T) {
|
||||||
|
header := buildProxyV1Header(t, "TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
|
||||||
|
require_Equal(t, addr.srcIP.String(), "192.168.1.50")
|
||||||
|
require_Equal(t, addr.srcPort, 12345)
|
||||||
|
|
||||||
|
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
|
||||||
|
require_Equal(t, addr.dstPort, 4222)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1ParseTCP6(t *testing.T) {
|
||||||
|
header := buildProxyV1Header(t, "TCP6", "2001:db8::1", "2001:db8::2", 54321, 4222)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
|
||||||
|
require_Equal(t, addr.srcIP.String(), "2001:db8::1")
|
||||||
|
require_Equal(t, addr.srcPort, 54321)
|
||||||
|
|
||||||
|
require_Equal(t, addr.dstIP.String(), "2001:db8::2")
|
||||||
|
require_Equal(t, addr.dstPort, 4222)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1ParseTCP6IPv4Mapped(t *testing.T) {
|
||||||
|
// Dual-stack proxies can emit IPv4-mapped IPv6 addresses for TCP6
|
||||||
|
// (an IPv4 client connecting to an IPv6 socket). These are valid IPv6
|
||||||
|
// textual addresses and must be accepted, matching the v2 parser.
|
||||||
|
header := buildProxyV1Header(t, "TCP6", "::ffff:192.0.2.1", "::ffff:10.0.0.1", 1234, 4222)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
|
||||||
|
require_Equal(t, addr.srcIP.String(), "192.0.2.1")
|
||||||
|
require_Equal(t, addr.srcPort, 1234)
|
||||||
|
|
||||||
|
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
|
||||||
|
require_Equal(t, addr.dstPort, 4222)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1MismatchedDestProtocol(t *testing.T) {
|
||||||
|
// TCP4 with IPv6 destination address
|
||||||
|
header := buildProxyV1Header(t, "TCP4", "192.168.1.1", "2001:db8::2", 12345, 443)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
|
||||||
|
// TCP6 with IPv4 destination address
|
||||||
|
header2 := buildProxyV1Header(t, "TCP6", "2001:db8::1", "10.0.0.1", 12345, 443)
|
||||||
|
conn2 := newMockConn(header2)
|
||||||
|
|
||||||
|
_, _, err = readProxyProtoHeader(conn2)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1ParseUnknown(t *testing.T) {
|
||||||
|
header := buildProxyV1Header(t, "UNKNOWN", "", "", 0, 0)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
addr, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, addr == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1PreservesCoalescedClientBytes(t *testing.T) {
|
||||||
|
header := buildProxyV1Header(t, "TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222)
|
||||||
|
connect := []byte("CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n")
|
||||||
|
conn := newMockConn(append(header, connect...))
|
||||||
|
|
||||||
|
addr, remaining, err := readProxyProtoHeader(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr)
|
||||||
|
require_Equal(t, string(remaining), string(connect))
|
||||||
|
|
||||||
|
rest, err := io.ReadAll(conn)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_Equal(t, len(rest), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1InvalidFormat(t *testing.T) {
|
||||||
|
// Missing fields
|
||||||
|
header := []byte("PROXY TCP4 192.168.1.1\r\n")
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1LineTooLong(t *testing.T) {
|
||||||
|
// Create a line longer than 107 bytes
|
||||||
|
longIP := strings.Repeat("1234567890", 12) // 120 chars
|
||||||
|
header := fmt.Appendf(nil, "PROXY TCP4 %s 10.0.0.1 12345 443\r\n", longIP)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1InvalidIP(t *testing.T) {
|
||||||
|
header := []byte("PROXY TCP4 not.an.ip.addr 10.0.0.1 12345 443\r\n")
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1MismatchedProtocol(t *testing.T) {
|
||||||
|
// TCP4 with IPv6 address
|
||||||
|
header := buildProxyV1Header(t, "TCP4", "2001:db8::1", "2001:db8::2", 12345, 443)
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
|
||||||
|
// TCP6 with IPv4 address
|
||||||
|
header2 := buildProxyV1Header(t, "TCP6", "192.168.1.1", "10.0.0.1", 12345, 443)
|
||||||
|
conn2 := newMockConn(header2)
|
||||||
|
|
||||||
|
_, _, err = readProxyProtoHeader(conn2)
|
||||||
|
require_Error(t, err, errProxyProtoInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1InvalidPort(t *testing.T) {
|
||||||
|
header := []byte("PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n")
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_True(t, err != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoV1EndToEnd(t *testing.T) {
|
||||||
|
// Start a test server with PROXY protocol enabled
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1 // Random port
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
// Get the server's listening port
|
||||||
|
addr := s.Addr().String()
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
require_NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Send the PROXY protocol header and first client commands in one write so
|
||||||
|
// the parser must preserve bytes read past the end of the v1 header.
|
||||||
|
clientIP, clientPort := "203.0.113.50", uint16(54321)
|
||||||
|
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
|
||||||
|
payload := append(header, []byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n")...)
|
||||||
|
|
||||||
|
_, err = conn.Write(payload)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
require_NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
|
||||||
|
cr := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
line, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "INFO "))
|
||||||
|
|
||||||
|
line, err = cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
|
||||||
|
expectPong(t, cr)
|
||||||
|
|
||||||
|
// Give server time to process
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check server's client list to verify the IP was extracted correctly
|
||||||
|
s.mu.Lock()
|
||||||
|
clients := s.clients
|
||||||
|
var foundClient *client
|
||||||
|
for _, c := range clients {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.host == clientIP && c.port == clientPort {
|
||||||
|
foundClient = c
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
if foundClient != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
require_NotNil(t, foundClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mixed Protocol Version Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
func TestClientProxyProtoVersionDetection(t *testing.T) {
|
||||||
|
// Test v1 detection
|
||||||
|
v1Header := buildProxyV1Header(t, "TCP4", "192.168.1.1", "10.0.0.1", 12345, 443)
|
||||||
|
conn1 := newMockConn(v1Header)
|
||||||
|
|
||||||
|
addr1, _, err := readProxyProtoHeader(conn1)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr1)
|
||||||
|
require_Equal(t, addr1.srcIP.String(), "192.168.1.1")
|
||||||
|
|
||||||
|
// Test v2 detection
|
||||||
|
v2Header := buildProxyV2Header(t, "192.168.1.2", "10.0.0.1", 54321, 443, proxyProtoFamilyInet)
|
||||||
|
conn2 := newMockConn(v2Header)
|
||||||
|
|
||||||
|
addr2, _, err := readProxyProtoHeader(conn2)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NotNil(t, addr2)
|
||||||
|
require_Equal(t, addr2.srcIP.String(), "192.168.1.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoUnrecognizedVersion(t *testing.T) {
|
||||||
|
// Invalid header that doesn't match v1 or v2
|
||||||
|
header := []byte("HELLO WORLD\r\n")
|
||||||
|
conn := newMockConn(header)
|
||||||
|
|
||||||
|
_, _, err := readProxyProtoHeader(conn)
|
||||||
|
require_Error(t, err, errProxyProtoUnrecognized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PROXY Protocol + TLS Tests (regression for issue #7571)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// runProxyProtoTLSServer starts a server with proxy_protocol and a required
|
||||||
|
// TLS listener (no allow_non_tls, no tls_first).
|
||||||
|
func runProxyProtoTLSServer(t *testing.T) *Server {
|
||||||
|
t.Helper()
|
||||||
|
tc := &TLSConfigOpts{
|
||||||
|
CertFile: "../test/configs/certs/server-cert.pem",
|
||||||
|
KeyFile: "../test/configs/certs/server-key.pem",
|
||||||
|
CaFile: "../test/configs/certs/ca.pem",
|
||||||
|
}
|
||||||
|
tlsConfig, err := GenTLSConfig(tc)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
opts.TLSConfig = tlsConfig
|
||||||
|
opts.TLSTimeout = 2
|
||||||
|
return RunServer(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyProtoTLSClientConfig builds a tls.Config trusting the test CA. The
|
||||||
|
// server cert is issued for "localhost" / 127.0.0.1, so ServerName must match.
|
||||||
|
func proxyProtoTLSClientConfig(t *testing.T) *tls.Config {
|
||||||
|
t.Helper()
|
||||||
|
caPEM, err := os.ReadFile("../test/configs/certs/ca.pem")
|
||||||
|
require_NoError(t, err)
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
require_True(t, pool.AppendCertsFromPEM(caPEM))
|
||||||
|
return &tls.Config{
|
||||||
|
RootCAs: pool,
|
||||||
|
ServerName: "localhost",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProxyProtoClient finds a connected client whose host/port matches what
|
||||||
|
// was advertised in the PROXY header.
|
||||||
|
func findProxyProtoClient(t *testing.T, s *Server, clientIP string, clientPort uint16) *client {
|
||||||
|
t.Helper()
|
||||||
|
s.mu.Lock()
|
||||||
|
clients := make([]*client, 0, len(s.clients))
|
||||||
|
for _, c := range s.clients {
|
||||||
|
clients = append(clients, c)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, c := range clients {
|
||||||
|
c.mu.Lock()
|
||||||
|
host, port := c.host, c.port
|
||||||
|
c.mu.Unlock()
|
||||||
|
if host == clientIP && port == clientPort {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectProxyProtoTLS mirrors what an L4 proxy and a TLS-enabled NATS client
|
||||||
|
// do over a single TCP connection: prepend the PROXY header, read the initial
|
||||||
|
// plaintext INFO, then upgrade to TLS. Returns the buffered reader and the
|
||||||
|
// TLS-wrapped connection for subsequent protocol writes.
|
||||||
|
func connectProxyProtoTLS(t *testing.T, addr string, header []byte) (*bufio.Reader, *tls.Conn) {
|
||||||
|
t.Helper()
|
||||||
|
rawConn, err := net.Dial("tcp", addr)
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
|
||||||
|
_, err = rawConn.Write(header)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
// Server emits INFO over plaintext before the TLS upgrade. Read it now
|
||||||
|
// so the bytes don't end up as input to the TLS handshake.
|
||||||
|
plainReader := bufio.NewReader(rawConn)
|
||||||
|
infoLine, err := plainReader.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(infoLine, "INFO "))
|
||||||
|
require_Equal(t, plainReader.Buffered(), 0)
|
||||||
|
|
||||||
|
tlsConn := tls.Client(rawConn, proxyProtoTLSClientConfig(t))
|
||||||
|
require_NoError(t, tlsConn.Handshake())
|
||||||
|
return bufio.NewReader(tlsConn), tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientProxyProtoV1WithRequiredTLS verifies that with proxy_protocol and
|
||||||
|
// required TLS, a client may send the PROXY v1 header followed by the TLS
|
||||||
|
// upgrade on a single TCP connection. Regression for issue #7571.
|
||||||
|
func TestClientProxyProtoV1WithRequiredTLS(t *testing.T) {
|
||||||
|
s := runProxyProtoTLSServer(t)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
clientIP, clientPort := "203.0.113.50", uint16(54321)
|
||||||
|
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
|
||||||
|
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
|
||||||
|
defer tlsConn.Close()
|
||||||
|
|
||||||
|
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
line, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
expectPong(t, cr)
|
||||||
|
|
||||||
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
||||||
|
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
|
||||||
|
return fmt.Errorf("no client with PROXY-supplied host/port yet")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientProxyProtoV2WithRequiredTLS is the v2 counterpart to
|
||||||
|
// TestClientProxyProtoV1WithRequiredTLS.
|
||||||
|
func TestClientProxyProtoV2WithRequiredTLS(t *testing.T) {
|
||||||
|
s := runProxyProtoTLSServer(t)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
clientIP, clientPort := "203.0.113.51", uint16(54322)
|
||||||
|
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
|
||||||
|
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
|
||||||
|
defer tlsConn.Close()
|
||||||
|
|
||||||
|
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
line, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
expectPong(t, cr)
|
||||||
|
|
||||||
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
||||||
|
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
|
||||||
|
return fmt.Errorf("no client with PROXY-supplied host/port yet")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientProxyProtoNonProxiedTLSFirstFallback(t *testing.T) {
|
||||||
|
tc := &TLSConfigOpts{
|
||||||
|
CertFile: "../test/configs/certs/server-cert.pem",
|
||||||
|
KeyFile: "../test/configs/certs/server-key.pem",
|
||||||
|
CaFile: "../test/configs/certs/ca.pem",
|
||||||
|
}
|
||||||
|
tlsConfig, err := GenTLSConfig(tc)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
opts.TLSConfig = tlsConfig
|
||||||
|
opts.TLSTimeout = 2
|
||||||
|
opts.TLSHandshakeFirst = true
|
||||||
|
opts.TLSHandshakeFirstFallback = 250 * time.Millisecond
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
rawConn, err := net.Dial("tcp", s.Addr().String())
|
||||||
|
require_NoError(t, err)
|
||||||
|
defer rawConn.Close()
|
||||||
|
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
|
||||||
|
// Direct TLS-first client: start the handshake right away (within the
|
||||||
|
// fallback delay), no PROXY header.
|
||||||
|
tlsConn := tls.Client(rawConn, proxyProtoTLSClientConfig(t))
|
||||||
|
require_NoError(t, tlsConn.Handshake())
|
||||||
|
|
||||||
|
cr := bufio.NewReader(tlsConn)
|
||||||
|
infoLine, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(infoLine, "INFO "))
|
||||||
|
|
||||||
|
_, err = tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
line, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
expectPong(t, cr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runProxyProtoAllowNonTLSServer starts a server with proxy_protocol and a
|
||||||
|
// TLS listener that also allows non-TLS clients (allow_non_tls).
|
||||||
|
func runProxyProtoAllowNonTLSServer(t *testing.T) *Server {
|
||||||
|
t.Helper()
|
||||||
|
tc := &TLSConfigOpts{
|
||||||
|
CertFile: "../test/configs/certs/server-cert.pem",
|
||||||
|
KeyFile: "../test/configs/certs/server-key.pem",
|
||||||
|
CaFile: "../test/configs/certs/ca.pem",
|
||||||
|
}
|
||||||
|
tlsConfig, err := GenTLSConfig(tc)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.Port = -1
|
||||||
|
opts.ProxyProtocol = true
|
||||||
|
opts.TLSConfig = tlsConfig
|
||||||
|
opts.TLSTimeout = 2
|
||||||
|
opts.AllowNonTLS = true
|
||||||
|
return RunServer(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testClientProxyProtoAllowNonTLS verifies that with proxy_protocol and
|
||||||
|
// allow_non_tls, the TLS-vs-plaintext detection is performed on the first
|
||||||
|
// byte of the client's actual traffic, not on the first byte of the PROXY
|
||||||
|
// header. Both a TLS client and a plaintext client behind the proxy must
|
||||||
|
// be able to connect on the same listener.
|
||||||
|
func testClientProxyProtoAllowNonTLS(t *testing.T, header []byte, clientIP string, clientPort uint16) {
|
||||||
|
t.Helper()
|
||||||
|
s := runProxyProtoAllowNonTLSServer(t)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
// TLS client behind the proxy: the sniff must detect the TLS
|
||||||
|
// ClientHello that follows the PROXY header.
|
||||||
|
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
|
||||||
|
defer tlsConn.Close()
|
||||||
|
|
||||||
|
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
line, err := cr.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
expectPong(t, cr)
|
||||||
|
|
||||||
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
||||||
|
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
|
||||||
|
return fmt.Errorf("no client with PROXY-supplied host/port yet")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Plaintext client behind the proxy must still be allowed.
|
||||||
|
rawConn, err := net.Dial("tcp", s.Addr().String())
|
||||||
|
require_NoError(t, err)
|
||||||
|
defer rawConn.Close()
|
||||||
|
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
|
||||||
|
_, err = rawConn.Write(header)
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
plainReader := bufio.NewReader(rawConn)
|
||||||
|
infoLine, err := plainReader.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(infoLine, "INFO "))
|
||||||
|
|
||||||
|
_, err = rawConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
|
||||||
|
require_NoError(t, err)
|
||||||
|
|
||||||
|
line, err = plainReader.ReadString('\n')
|
||||||
|
require_NoError(t, err)
|
||||||
|
require_True(t, strings.HasPrefix(line, "+OK"))
|
||||||
|
expectPong(t, plainReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientProxyProtoV1WithAllowNonTLS verifies that a proxied TLS client is
|
||||||
|
// detected as TLS (and a proxied plaintext client as non-TLS) when the
|
||||||
|
// listener has both tls and allow_non_tls configured, using a PROXY v1 header.
|
||||||
|
func TestClientProxyProtoV1WithAllowNonTLS(t *testing.T) {
|
||||||
|
clientIP, clientPort := "203.0.113.60", uint16(54331)
|
||||||
|
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
|
||||||
|
testClientProxyProtoAllowNonTLS(t, header, clientIP, clientPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientProxyProtoV2WithAllowNonTLS is the v2 counterpart to
|
||||||
|
// TestClientProxyProtoV1WithAllowNonTLS.
|
||||||
|
func TestClientProxyProtoV2WithAllowNonTLS(t *testing.T) {
|
||||||
|
clientIP, clientPort := "203.0.113.61", uint16(54332)
|
||||||
|
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
|
||||||
|
testClientProxyProtoAllowNonTLS(t, header, clientIP, clientPort)
|
||||||
|
}
|
||||||
270
server/closed_conns_test.go
Normal file
270
server/closed_conns_test.go
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
// Copyright 2018-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkClosedConns(t *testing.T, s *Server, num int, wait time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
checkFor(t, wait, 5*time.Millisecond, func() error {
|
||||||
|
if nc := s.closed.len(); nc != num {
|
||||||
|
return fmt.Errorf("Closed conns expected to be %v, got %v", num, nc)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTotalClosedConns(t *testing.T, s *Server, num uint64, wait time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
checkFor(t, wait, 5*time.Millisecond, func() error {
|
||||||
|
if nc := s.closed.totalConns(); nc != num {
|
||||||
|
return fmt.Errorf("Total closed conns expected to be %v, got %v", num, nc)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedConnsAccounting(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.MaxClosedClients = 10
|
||||||
|
opts.NoSystemAccount = true
|
||||||
|
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
wait := time.Second
|
||||||
|
|
||||||
|
nc, err := nats.Connect(fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error on connect: %v", err)
|
||||||
|
}
|
||||||
|
id, _ := nc.GetClientID()
|
||||||
|
nc.Close()
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, wait)
|
||||||
|
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
|
||||||
|
}
|
||||||
|
if conns[0].Cid != id {
|
||||||
|
t.Fatalf("Expected CID to be %d, got %d\n", id, conns[0].Cid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create 21 more
|
||||||
|
for i := 0; i < 21; i++ {
|
||||||
|
nc, err = nats.Connect(fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error on connect: %v", err)
|
||||||
|
}
|
||||||
|
nc.Close()
|
||||||
|
checkTotalClosedConns(t, s, uint64(i+2), wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkClosedConns(t, s, opts.MaxClosedClients, wait)
|
||||||
|
checkTotalClosedConns(t, s, 22, wait)
|
||||||
|
|
||||||
|
conns = s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != opts.MaxClosedClients {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n",
|
||||||
|
opts.MaxClosedClients, lc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set it to the start after overflow.
|
||||||
|
cid := uint64(22 - opts.MaxClosedClients)
|
||||||
|
for _, ci := range conns {
|
||||||
|
cid++
|
||||||
|
if ci.Cid != cid {
|
||||||
|
t.Fatalf("Expected cid of %d, got %d\n", cid, ci.Cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedConnsSubsAccounting(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
|
||||||
|
|
||||||
|
nc, err := nats.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error on subscribe: %v", err)
|
||||||
|
}
|
||||||
|
defer nc.Close()
|
||||||
|
|
||||||
|
// Now create some subscriptions
|
||||||
|
numSubs := 10
|
||||||
|
for i := 0; i < numSubs; i++ {
|
||||||
|
subj := fmt.Sprintf("foo.%d", i)
|
||||||
|
nc.Subscribe(subj, func(m *nats.Msg) {})
|
||||||
|
}
|
||||||
|
nc.Flush()
|
||||||
|
nc.Close()
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, 20*time.Millisecond)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be 1, got %d\n", lc)
|
||||||
|
}
|
||||||
|
ci := conns[0]
|
||||||
|
|
||||||
|
if len(ci.subs) != numSubs {
|
||||||
|
t.Fatalf("Expected number of Subs to be %d, got %d\n", numSubs, len(ci.subs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkReason(t *testing.T, reason string, expected ClosedState) {
|
||||||
|
if !strings.Contains(reason, expected.String()) {
|
||||||
|
t.Fatalf("Expected closed connection with `%s` state, got `%s`\n",
|
||||||
|
expected, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedAuthorizationTimeout(t *testing.T) {
|
||||||
|
serverOptions := DefaultOptions()
|
||||||
|
serverOptions.Authorization = "my_token"
|
||||||
|
serverOptions.AuthTimeout = 0.4
|
||||||
|
s := RunServer(serverOptions)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort(serverOptions.Host, fmt.Sprintf("%d", serverOptions.Port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error dialing server: %v\n", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, 2*time.Second)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
|
||||||
|
}
|
||||||
|
checkReason(t, conns[0].Reason, AuthenticationTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedAuthorizationViolation(t *testing.T) {
|
||||||
|
serverOptions := DefaultOptions()
|
||||||
|
serverOptions.Authorization = "my_token"
|
||||||
|
s := RunServer(serverOptions)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
opts := s.getOpts()
|
||||||
|
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
|
||||||
|
|
||||||
|
nc, err := nats.Connect(url)
|
||||||
|
if err == nil {
|
||||||
|
nc.Close()
|
||||||
|
t.Fatal("Expected failure for connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, 2*time.Second)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
|
||||||
|
}
|
||||||
|
checkReason(t, conns[0].Reason, AuthenticationViolation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedUPAuthorizationViolation(t *testing.T) {
|
||||||
|
serverOptions := DefaultOptions()
|
||||||
|
serverOptions.Username = "my_user"
|
||||||
|
serverOptions.Password = "my_secret"
|
||||||
|
s := RunServer(serverOptions)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
opts := s.getOpts()
|
||||||
|
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
|
||||||
|
|
||||||
|
nc, err := nats.Connect(url)
|
||||||
|
if err == nil {
|
||||||
|
nc.Close()
|
||||||
|
t.Fatal("Expected failure for connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
url2 := fmt.Sprintf("nats://my_user:wrong_pass@%s:%d", opts.Host, opts.Port)
|
||||||
|
nc, err = nats.Connect(url2)
|
||||||
|
if err == nil {
|
||||||
|
nc.Close()
|
||||||
|
t.Fatal("Expected failure for connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 2, 2*time.Second)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 2 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 2, lc)
|
||||||
|
}
|
||||||
|
checkReason(t, conns[0].Reason, AuthenticationViolation)
|
||||||
|
checkReason(t, conns[1].Reason, AuthenticationViolation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedMaxPayload(t *testing.T) {
|
||||||
|
serverOptions := DefaultOptions()
|
||||||
|
serverOptions.MaxPayload = 100
|
||||||
|
|
||||||
|
s := RunServer(serverOptions)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
opts := s.getOpts()
|
||||||
|
endpoint := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port))
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", endpoint, time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not make a raw connection to the server: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// This should trigger it.
|
||||||
|
pub := "PUB foo.bar 1024\r\n"
|
||||||
|
conn.Write([]byte(pub))
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, 2*time.Second)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
|
||||||
|
}
|
||||||
|
checkReason(t, conns[0].Reason, MaxPayloadExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClosedTLSHandshake(t *testing.T) {
|
||||||
|
opts, err := ProcessConfigFile("./configs/tls.conf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing config file: %v", err)
|
||||||
|
}
|
||||||
|
opts.TLSVerify = true
|
||||||
|
opts.NoLog = true
|
||||||
|
opts.NoSigs = true
|
||||||
|
s := RunServer(opts)
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
nc, err := nats.Connect(fmt.Sprintf("tls://%s:%d", opts.Host, opts.Port))
|
||||||
|
if err == nil {
|
||||||
|
nc.Close()
|
||||||
|
t.Fatal("Expected failure for connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkClosedConns(t, s, 1, 2*time.Second)
|
||||||
|
conns := s.closed.closedClients()
|
||||||
|
if lc := len(conns); lc != 1 {
|
||||||
|
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
|
||||||
|
}
|
||||||
|
checkReason(t, conns[0].Reason, TLSHandshakeError)
|
||||||
|
}
|
||||||
2636
server/config_check_test.go
Normal file
2636
server/config_check_test.go
Normal file
File diff suppressed because it is too large
Load Diff
22
server/configs/certs/cert.new.pem
Normal file
22
server/configs/certs/cert.new.pem
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDnjCCAoagAwIBAgIJAM/HacKKaH7zMA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD
|
||||||
|
VQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMSQwIgYD
|
||||||
|
VQQKDBtTeW5hZGlhIENvbW11bmljYXRpb25zIEluYy4xKzApBgNVBAMMIm5hdHMu
|
||||||
|
aW8vZW1haWxBZGRyZXNzPWRlcmVrQG5hdHMuaW8wHhcNMTkxMDE3MTIyODIxWhcN
|
||||||
|
MjkxMDE0MTIyODIxWjCBgzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYD
|
||||||
|
VQQHDAtMb3MgQW5nZWxlczEkMCIGA1UECgwbU3luYWRpYSBDb21tdW5pY2F0aW9u
|
||||||
|
cyBJbmMuMSswKQYDVQQDDCJuYXRzLmlvL2VtYWlsQWRkcmVzcz1kZXJla0BuYXRz
|
||||||
|
LmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBIAVUr4gCEBRVbA
|
||||||
|
HSweCLExOrMZmII4AdvgIqT+svjBkJd+vdkbd5b/SC1HQx1E14kiRJ/JrZIZoMqi
|
||||||
|
+7pK3kFM63Fkhkg8rWOxn0tQznSymKTpha5NdDWxnB0dXlXFCQG1e/cuDalR7UhF
|
||||||
|
LPHiuK42gAvhivBcymDPV0hTYt4rHb71SQ1DwfCYzcLkDvDFA/W7kronaEhRyWn6
|
||||||
|
uvZvHkdvScoubdzoW/kNBH4JYZw5svLzGz3z20rUGeLttF4ge5SCAz9unZk96HUO
|
||||||
|
EFmUDvFmxdnTXrYINjraNvgu7fPFmVzupWrWPA/7U+cxOvm3qBMdqxFxNr7bq+Md
|
||||||
|
UPKi3QIDAQABoxMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB
|
||||||
|
AQCCgFKP/bMi5cvasJOzXKLpwOneW+oItL2t/5NxPumIBMDo5NnShzsFLfGmujYw
|
||||||
|
fjcMharynkkbz/oeDAm8h1mySAJrnqtabiWgaW8zbNJfJOrAq4Jvs9COMAzjJclL
|
||||||
|
+h9GWtvVylDnsNtd18n1gA5OYv0A6YjuSrWINL8Sp5QvTF/5tT8jFrDOIjZl7m50
|
||||||
|
lX4R70N9GLt2jIlKro+qdsi6qUZccuJmoQxUpG1iQcRNFHtWfDPr5KFEXaO6IoYt
|
||||||
|
D1kYWmo/A3WQm7nbXeZw/zaSGSS0t6/hKZwm+gPCL6TEdDrjhjxpCZQTwaMd6jj2
|
||||||
|
bvT2OA0ZpzUWyxnaX6u+cDfM
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
server/configs/certs/key.new.pem
Normal file
28
server/configs/certs/key.new.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8EgBVSviAIQFF
|
||||||
|
VsAdLB4IsTE6sxmYgjgB2+AipP6y+MGQl3692Rt3lv9ILUdDHUTXiSJEn8mtkhmg
|
||||||
|
yqL7ukreQUzrcWSGSDytY7GfS1DOdLKYpOmFrk10NbGcHR1eVcUJAbV79y4NqVHt
|
||||||
|
SEUs8eK4rjaAC+GK8FzKYM9XSFNi3isdvvVJDUPB8JjNwuQO8MUD9buSuidoSFHJ
|
||||||
|
afq69m8eR29Jyi5t3Ohb+Q0EfglhnDmy8vMbPfPbStQZ4u20XiB7lIIDP26dmT3o
|
||||||
|
dQ4QWZQO8WbF2dNetgg2Oto2+C7t88WZXO6latY8D/tT5zE6+beoEx2rEXE2vtur
|
||||||
|
4x1Q8qLdAgMBAAECggEBAI0zJrTi2Le6D+4zN4Gvgc0c280qcmkiO1KJ9jmMjv1n
|
||||||
|
7kvUsf9vZUPgRkG2XO5ypyD7gJLtNMnwCvXBraQ5NcSwWkPampKG4ad8Vfs23LBk
|
||||||
|
xUH9bqZDOzuopHSFF2ugEZK1icBM0HLJUQ1JWUZpRMNLaPex8+AQnloDXSg20QRE
|
||||||
|
Z53CDYZVEVaLAYIkfgZ6QgBSbJ0ob/a+ZJkChWs3f6cpNHige4tR2RQ0aJFwvlwu
|
||||||
|
eC8G3RDNzUMhPf4TSf/XUlHXngyHKa7MS3feEtuKIQ4gb8hjESDcZznJUGG9qyMT
|
||||||
|
7445mapc60t8z46o9iVeVJQmQxmjFUDkLcZ9ckcWfzECgYEA4OiolNUiTnXbvP0H
|
||||||
|
3rxAnhx9GlyHiRaIZvgsAHH/6aMbJIXnH89cAR38PL79OaQpWZqK8Z1vrCHC8q7s
|
||||||
|
Wb1RehgnwPxtTl4RTZ0wj3Ri5BIq32nzgla0w/5RAmAw9HrYXjp4TR4Quc+bHLcr
|
||||||
|
ZM2gA8nZ4PG73rYvd/Mu3lCQiS8CgYEA1hGoaduGG7eHTMCct/kCupFfYXgEuUDh
|
||||||
|
V/nO1SYJbIkMuSONNaQy1oUc5o0BB/JTcslZWqtuGa9snKPVd3/rJz75YGt1QR6u
|
||||||
|
MukCYjbWGAWXfDh8G8aGSr3k1k3JmKNE7fI7L5DuvKCGoGjzzC+kclyoij8jwXk0
|
||||||
|
zghaYPvM+bMCgYEA3/fwgR3p5vZJF19mqfEP7CP0lP7V3bd5qAi1UNA1h4VsrydF
|
||||||
|
LRFCzr38hMWwx+jpYJicitU78s9AIon9RbRY4dwSIoV9mE/mrUK+q+y72eEZnpgU
|
||||||
|
7ZPIuXCVXWdK+PsoYlWZnTo2b8ME9UiWxvBZy8wD05UGgFcu2CVsY+kYtfMCgYAs
|
||||||
|
Vn/xXPyL5Rlq9kH/gN3l0pJU18zyqdOCq0UBtN0i08gE2K44vAejkvKHdhEOmkxa
|
||||||
|
bAXL19H4E/OFBhICrEYCXPK928PvdvFRrh1GRmFVnGLh4bki752FAYvSL05gBQET
|
||||||
|
36YOlhA8lWsM8m/8jKmc3kAyUh2PxxD+05AUolK0LQKBgHqPgMj5rfiR7MoVIaXr
|
||||||
|
K8h7g+HejppmCxzENc98hvIQIO2aWeC4Hi/EMe+XYSN/Gl6d5+aw80JYRgIv6dFV
|
||||||
|
hDEEt8lEI4sFhKD0oG1dQo+BdDNXOLbpHq+RQRPHa8J/pmSjI5Joqb2mCaAcm/KJ
|
||||||
|
nYE20ox06YMSb+ZrjP2p4qw3
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
28
server/configs/certs/key.pem
Normal file
28
server/configs/certs/key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDc8YaxliKNsa8M
|
||||||
|
idwQsDXS0MI4z+ZP1UMW9U/Vt2qTienYs3L6KqezRfwEH2AoE7XyvQdzePV3aN1g
|
||||||
|
A0L3mAKiS2FN3DhxR6Y8D/JNb5YA6Pl0flaMKnWktma/SMOf7o7eS7Zt6ZmIvtBg
|
||||||
|
p1ZtRSZSH64KSqC467eGByHnXm25Z7Uf/2rO8K1jsmRwgDEJoJL5Z4va4pypX5Kq
|
||||||
|
7OxWk8Za50Kuk6DtdRfIcnUjoDadsYzGHzF7kp1/31elQ2aUPp9e6X3aWu++gJv5
|
||||||
|
brBMsWeQ+ETmjCMPqxoXDM2AJqwP73irELU7POUR/q3gooqy16BvTp7TaLvAtKdP
|
||||||
|
SK7kl1QVAgMBAAECggEAKVT1gZs4c5Fd0daoWGbeasWqz730s9KNbmoNuUlKorcW
|
||||||
|
C/fl9m6sWJkrAApujpDBZNt/3VYvdVskrqVtjaveMkYvucUMugRRUMMa7OmHAjyT
|
||||||
|
DfvkbOw0fc+xgO83yV9zUpqPUhh1JGOtz6b+QArDlHFguYQkdPODrYsACKxdkmU/
|
||||||
|
A+ZJkC2Sdn0L7ARmROxvoelxV3HNbC+p8lqrIVarpni8oeDF2JxQfyJETl3Df099
|
||||||
|
/KA/IQvXytEQBJpguh0j7sK81cI40TKO3kNWsCe46S2nkCuEeEums7BqAspmdNMS
|
||||||
|
gBW0rpFA9Ep5AyT4+A2XhWFQAwf/PI26JUVDhbClAQKBgQD/XlONHf5tNiOEtW88
|
||||||
|
Whd9ha7oLgoBhr4gFOcPM4MUStRDftRXY1ty0IGseV0Rj1xQRUp31Pl6Kep/udy1
|
||||||
|
r/6ML/IQNaht1VJOf2+2v7oZFoLRGLvR3qSRFNVtfLWp1Dg5OPgyi00OpFnxz/6o
|
||||||
|
iuWlh1EWwSm9vPl91HcWPPcwBQKBgQDdfWfDfy8q9jExj8tVNh5++haeemprkZxk
|
||||||
|
QuduIkahlbQ3V+SLaSWQrwzcB4PWL+45fnIWwK3Vmm2POBv+OWfKGjJniSd4Zrrf
|
||||||
|
H+lSg4+hpivvoaYf1TsCTHy/af/5pYIkEUmbPM0EqwubyoW6oMAgGIAMyH6XBLaY
|
||||||
|
a4vWZo6g0QKBgFXDXhgUrLAM8JzPOk5wi1cSoI1FeQLON+gaXQdT63/TKbqJS9MV
|
||||||
|
gU7sC8Da+ZC+LuiefMYF9ss6bJD84Mz8EGcQayFag/hvHjdSwTgE6AEo+EI1Jk7z
|
||||||
|
kR1Qe+VLbs9cgI1nPqPq+LQkKjj1+7aq/zk6WtdjhBs+7iN+SYhkbTfpAoGAeCbv
|
||||||
|
gz4beFLVnO1EgJU7Nea1HoOJ95CmJj2lDjnJ1x4/BUbI1FfV6QcNEs+A1VBrCwVt
|
||||||
|
HqqnopiDlo35oY/CngBYF5JvtwEDnsbQ69IyuJ5Md1JZrCsgN78GbVAzbFo8nxRB
|
||||||
|
udTh4wZm5byXjwZlMwQXctfQ1FvaMhUlZsl71gECgYA82osmLHxZZOiQrYJ7z302
|
||||||
|
CmPohwcTtUASqeA0kQOX/OdFJ1IxWIFpzq2ffMipLTfzk7OeFS2hqvNo/NE8NHlR
|
||||||
|
/79IycYwlO99T3e2yQhYKW5ZMT3Q8YtyLLPtBzPPtO/Tc8yKhjX8wc9w9sxq9kqg
|
||||||
|
i5Z+6xfO+l/NBT/JHF4yEw==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
22
server/configs/certs/server.pem
Normal file
22
server/configs/certs/server.pem
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDnjCCAoagAwIBAgIJAOXpOWk41Pd7MA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD
|
||||||
|
VQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMSQwIgYD
|
||||||
|
VQQKDBtTeW5hZGlhIENvbW11bmljYXRpb25zIEluYy4xKzApBgNVBAMMIm5hdHMu
|
||||||
|
aW8vZW1haWxBZGRyZXNzPWRlcmVrQG5hdHMuaW8wHhcNMTkxMDE3MTIzMjQ5WhcN
|
||||||
|
MjkxMDE0MTIzMjQ5WjCBgzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYD
|
||||||
|
VQQHDAtMb3MgQW5nZWxlczEkMCIGA1UECgwbU3luYWRpYSBDb21tdW5pY2F0aW9u
|
||||||
|
cyBJbmMuMSswKQYDVQQDDCJuYXRzLmlvL2VtYWlsQWRkcmVzcz1kZXJla0BuYXRz
|
||||||
|
LmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3PGGsZYijbGvDInc
|
||||||
|
ELA10tDCOM/mT9VDFvVP1bdqk4np2LNy+iqns0X8BB9gKBO18r0Hc3j1d2jdYANC
|
||||||
|
95gCokthTdw4cUemPA/yTW+WAOj5dH5WjCp1pLZmv0jDn+6O3ku2bemZiL7QYKdW
|
||||||
|
bUUmUh+uCkqguOu3hgch515tuWe1H/9qzvCtY7JkcIAxCaCS+WeL2uKcqV+Squzs
|
||||||
|
VpPGWudCrpOg7XUXyHJ1I6A2nbGMxh8xe5Kdf99XpUNmlD6fXul92lrvvoCb+W6w
|
||||||
|
TLFnkPhE5owjD6saFwzNgCasD+94qxC1OzzlEf6t4KKKstegb06e02i7wLSnT0iu
|
||||||
|
5JdUFQIDAQABoxMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB
|
||||||
|
AQDIMNgMqgGdXwZTIDxz4iq76iHdSHxZK4YwJr/4xRSq1uEddxmUfAQzo+gBboA5
|
||||||
|
c0XxYxc0xViuueThnqdLhHmEyCs8uGFRLaQAI5Bq1PdMQP12m5fCWRAyKrCxdtli
|
||||||
|
zm8ByDq7mWpkfMTd/rJGR4wCR9qI9Y5Bp6p4FBKZ3pzEanFXMV9IHhkm1BGh9tbe
|
||||||
|
l6GQyBptEpfTiRwNCC/ympeiL3G8hfDCPkcLed5sQ+OhPe5iWMVPncZh/qehnUJK
|
||||||
|
B5CIXcagcROFutsDYPCurKcfQOsfqulu0q95h7FQUOsIeU7jIlcLxIii19qjlTZh
|
||||||
|
sFjsul5G7qqMEgIUsx3U985v
|
||||||
|
-----END CERTIFICATE-----
|
||||||
23
server/configs/certs/tls/benchmark-ca-cert.pem
Normal file
23
server/configs/certs/tls/benchmark-ca-cert.pem
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID2zCCAsOgAwIBAgIUZj0PngA93uUSShcRndTQju/J88YwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
|
||||||
|
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
|
||||||
|
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
|
||||||
|
WhgPMjEyMzA3MjExNTI3NTdaMHwxEzARBgNVBAMMCm5hdHMuaW8uQ0ExIjAgBgNV
|
||||||
|
BAsMGVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWEx
|
||||||
|
EDAOBgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNB
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2cCyJL+DExUyZto2eFLm
|
||||||
|
MBRSkQLxM9pOWB9O8TecHlPcc/SPGq/x9lpguJ/IiaUj+VffVWy236KW2JL5Xj83
|
||||||
|
PZwhXi1yZzxlIBsKAgAUeNfWuTAc0K0Qm9pR5Wjv5eNcT0mw6JX0SPgUQAl9BSwU
|
||||||
|
WvtMOTxOt0hBjHmZaEamp7nLmwogpvgPsrubD6U4O/vUQm3JTsbp2rFQxXPpkG19
|
||||||
|
69PGsT37r0/w9Zv0xNAcB/zCWdNBXCTA2ACV2IpJedWm8Jrjcn3Kp4Fv3TKTsCZl
|
||||||
|
eWtfxCdljndk88+NFK7cEw7b9Bs5R5Zhu20C+Ne8vmMWhYbVBFYws5/jGzPBkVTD
|
||||||
|
7wIDAQABo1MwUTAdBgNVHQ4EFgQUEqfeAemfeIp4MM4C7H1bJS+mra4wHwYDVR0j
|
||||||
|
BBgwFoAUEqfeAemfeIp4MM4C7H1bJS+mra4wDwYDVR0TAQH/BAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAQEAiamiPxOlZ0pwOJvv0ylDreHVk2kast67YlhAOcZoSMvi
|
||||||
|
e2jbKL98U3+ZznGj21AKqEaOkO7UmKoJ/3QlrjgElXzcMUrUrJ1WNowlbXPlAhyL
|
||||||
|
KhNthLKUr72Tv6wv5GZdAR6DaAwq3iYTbpnLq4oCnFHiXgDWgWyJDLsTGulWve/K
|
||||||
|
GGM2JMcnacNgNC18uki440Wcfp0vGj9HhO6I/u63oGewZnIK87GQMQCt3JLFyiUc
|
||||||
|
hrn9nWoixFWcJfCjBcMlwZXMIAlDdelU1/hWtSknKCs57GvZuACcicAYiYIkWCkd
|
||||||
|
p1pF4G0Ic6irAnLTqhdGwL4+5pjNd1Ih0Gezn9hJLg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
server/configs/certs/tls/benchmark-ca-key.pem
Normal file
28
server/configs/certs/tls/benchmark-ca-key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZwLIkv4MTFTJm
|
||||||
|
2jZ4UuYwFFKRAvEz2k5YH07xN5weU9xz9I8ar/H2WmC4n8iJpSP5V99VbLbfopbY
|
||||||
|
kvlePzc9nCFeLXJnPGUgGwoCABR419a5MBzQrRCb2lHlaO/l41xPSbDolfRI+BRA
|
||||||
|
CX0FLBRa+0w5PE63SEGMeZloRqanucubCiCm+A+yu5sPpTg7+9RCbclOxunasVDF
|
||||||
|
c+mQbX3r08axPfuvT/D1m/TE0BwH/MJZ00FcJMDYAJXYikl51abwmuNyfcqngW/d
|
||||||
|
MpOwJmV5a1/EJ2WOd2Tzz40UrtwTDtv0GzlHlmG7bQL417y+YxaFhtUEVjCzn+Mb
|
||||||
|
M8GRVMPvAgMBAAECggEASvFWfm5JMtqjP6HPiGbjoV2FMzJjiFiUiSCxXzSn7wom
|
||||||
|
v+PGEsXGTWa6jiAz+SeUc38KNtDVOa+wIfani4fPP82J8GtMyfoPxdZ4gcq8QQDr
|
||||||
|
/k1wRWOi6Tjg4cdVdXXkMcencs0VR73V3lpFpG+Qy+VcTQCUCOF96dZ59VkHh4a4
|
||||||
|
CHX6PegOWwr0TSaCUacwhua+rPmCar/btAYv7Wp7+c+Zf7Rn2WYTV7ol4sYXR4ZQ
|
||||||
|
Sy/ROijeFTMkYNpaW/KOO2/pn3OJA8ycYH8UWZpsenPfIajC0Eka7BoEQpw6M8HR
|
||||||
|
wRWrKwssBEs0psFiq9s8J+6resPgXfU/9pf+mTkTqQKBgQD8kktUqN+vYc6t22DE
|
||||||
|
tSkg8y8OsGh9VTYfp7x5hu9qEC9t4mAKjqA/rRLiTXze/wInntreRTjjMb/NlqMy
|
||||||
|
PvI0Z+dM1UuqcF1axgKrIYsgnLJWuunOhaj5K3LhiNcHznlCtN9601dbccwLlQhL
|
||||||
|
5jdjnOuJ0i+Nh9v5oiu37SfldwKBgQDctWdbF4hJrBPnS6CcojuQj6ha10wYYe46
|
||||||
|
ZVcxKe5hFBs1q975YCHnEntyCDvXGfOTRgbKeZbwNhMvAc7Pp6eGMR/9SpiRwTt4
|
||||||
|
567hUz56WXVmp4gSvxoNuYRlWiMI8rZkyKJ8KFipvHgRa8nuamh0QBB4ShEJiVk8
|
||||||
|
fhaUiZeTSQKBgF0hAD/OKPR9Jv06J9tARVMN+Cr9ZvnXwqY3biqNU5gTMbndv7YE
|
||||||
|
0xfHlG/3THTZKI09aMyOT6SOQn/m7HPpe9tQ+Jt/BnBpEDMZUgCR1MAIp0WNlAp/
|
||||||
|
hEej+q8oiskpG9M56DFc3hgsxKT8pdt+nqvPP5ZI9xnDn5vTbTVbb9uPAoGAFRvU
|
||||||
|
csXhZwpqLOjyx4hMohrbQzTsNjjHjBY9LJqSDf7aS1vQy5ECLRN7cwCOmJgGz8MW
|
||||||
|
yy6t3POPCiPmH74tK4xvPs5voSEWCw49j5dillkP/W1wejqEx2NC4l6okyaDg0gd
|
||||||
|
IjrJoBJCeYgRnBfZPaUS7i5HSt40BrEYf8RZFuECgYAjSnvY8nYFRudsylYOu3TL
|
||||||
|
AcGbAdpDfL2H4G9z7qEQC6t1LqqGNcfMan/qS4J/n5GCOedVWHcfCraROOMil52v
|
||||||
|
3ZDXyyjGEO08XgKnoa2ZL/z2a6s077+hAAnbcywyi2Qz8Yfi6GnwYfU1u8SN0APd
|
||||||
|
T71HPNsWkU4zkxmP4S0Olg==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
19
server/configs/certs/tls/benchmark-server-cert-ed25519.pem
Normal file
19
server/configs/certs/tls/benchmark-server-cert-ed25519.pem
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDHTCCAgWgAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsYwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
|
||||||
|
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
|
||||||
|
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
|
||||||
|
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
|
||||||
|
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
|
||||||
|
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMCow
|
||||||
|
BQYDK2VwAyEAyyc9y9iZgWWSsPRahbeGxF6XN3VOFPZBvD/HQps6jr6jgZEwgY4w
|
||||||
|
CwYDVR0PBAQDAgQwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCoGA1UdEQQjMCGCDnJl
|
||||||
|
dWJlbi5uYXRzLmlvgg9yZXViZW4ubmF0cy5jb20wHQYDVR0OBBYEFBwkwMU8xuQO
|
||||||
|
FN1Ck5o2qQ4Dz87ZMB8GA1UdIwQYMBaAFBKn3gHpn3iKeDDOAux9WyUvpq2uMA0G
|
||||||
|
CSqGSIb3DQEBCwUAA4IBAQALjCynuxEobk1MYQAFhkrfAD29H6yRpOcKigHCZjTJ
|
||||||
|
Dnpupip1xvaFPPvhi4nxtuWcXgKpWEfd1jOPaiNV6lrefahitZpzcflD7wNOxqvx
|
||||||
|
Hau2U3lFnjnGaC0ppp66x26cQznp6YcTdxrJ1QF4vkOejxqNvaTzmiwzSPIIYm7+
|
||||||
|
iKVWT+Z86WKof3vAdsX/f148YH1YSPk0ykiBzlbLScbyWebbaydrAIpU01IkSvMo
|
||||||
|
qDYu+Fba0tpONLe1BUklc608riwQjw9HiJJ2zJIAOBAUev5+48RP91/K111Ix1bl
|
||||||
|
fGPT8/1TJbyGG2jeJwyLoSIu72aDnnIBfqGkVunRTmeg
|
||||||
|
-----END CERTIFICATE-----
|
||||||
22
server/configs/certs/tls/benchmark-server-cert-rsa-1024.pem
Normal file
22
server/configs/certs/tls/benchmark-server-cert-rsa-1024.pem
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDkzCCAnugAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsMwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
|
||||||
|
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
|
||||||
|
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
|
||||||
|
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
|
||||||
|
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
|
||||||
|
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIGf
|
||||||
|
MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyHHaVHinB3jBsicR4hp7uopz0u3+O
|
||||||
|
kUicIUSQXDcWiPzdvE+7YZ/s4+Ud4aw4g9q0wHzkZSaMg8nil4tCKmTrUKolVTVj
|
||||||
|
CCCBmtqq3LwzNLapyoDJRyXsWqHt5TWYSxaf/UQT6sWOgqHOLrbd4J8F0sjxEniB
|
||||||
|
GDHR1ZXpJCBaIQIDAQABo4GRMIGOMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggr
|
||||||
|
BgEFBQcDATAqBgNVHREEIzAhgg5yZXViZW4ubmF0cy5pb4IPcmV1YmVuLm5hdHMu
|
||||||
|
Y29tMB0GA1UdDgQWBBQk5kWOUcUNn7FppddLANe3droUlzAfBgNVHSMEGDAWgBQS
|
||||||
|
p94B6Z94ingwzgLsfVslL6atrjANBgkqhkiG9w0BAQsFAAOCAQEA2Njy2f1PUZRf
|
||||||
|
G1/oZ0El7J8L6Ql1HmEC7tOTzbORg7U9uMHKqIFL/IXXAdAlE/EjFEA2riPO8cu/
|
||||||
|
bvL2A4CapYzt2kDD9PPYfVtniRr7mv0EVntPwEvfiySMAEeZuW/M2liPfgPpQkhL
|
||||||
|
fzwPeCOfqM8AjpyDab8NEGX5Bbf421oQorlENpm4PKQCXoUN5cWpBwuwWxj7yndj
|
||||||
|
256MevLDKKe/ALSLQEo/2Jgpnmp7Qol0GtomCzsLgZ+ASuVtCsGTFmaRrsqVPspJ
|
||||||
|
oOl6qby5gYwN9TR8zfRYL1m1sbYROz+5+ofEoiTnaOoOSjiBIoYoMeSC/jvJQTPT
|
||||||
|
VdD8QeQ6Og==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
24
server/configs/certs/tls/benchmark-server-cert-rsa-2048.pem
Normal file
24
server/configs/certs/tls/benchmark-server-cert-rsa-2048.pem
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEFzCCAv+gAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsQwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
|
||||||
|
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
|
||||||
|
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
|
||||||
|
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
|
||||||
|
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
|
||||||
|
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIIB
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyx3O+Z6u8Y1SiuHu3szWbLvL
|
||||||
|
WrZpSpEiZkll+wk5205S1FRcQLccfr4ubdtjOBdi+RzILCtkflUI01Dbqu6cV7/2
|
||||||
|
yfLthxBeNDiXMhjyOFkYLwwE4w7CdTwWWsmW31oUH1rYXIDPoeb7WPF7w3NwaUJu
|
||||||
|
ZXnqM98LRgWDTmh+nsqDDW/bz1fYIdxcO9az6iBOnJ2AGWI2ur5GzWc4+gNMOZiZ
|
||||||
|
Xj657g0MbyVM4Gzyc4Au22hShZ/YorLP8NAiwNJamlrCFzlnZN/ePjuQPcI6glnb
|
||||||
|
oO9IAGfPdAOJptfayuPAZgUngzewB38yY0Q/rKG1GJKSkQ8X6/lXiWaRPZJjYwID
|
||||||
|
AQABo4GRMIGOMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATAqBgNV
|
||||||
|
HREEIzAhgg5yZXViZW4ubmF0cy5pb4IPcmV1YmVuLm5hdHMuY29tMB0GA1UdDgQW
|
||||||
|
BBRtanJZScdSlsPsi58lBcpdj+bV/zAfBgNVHSMEGDAWgBQSp94B6Z94ingwzgLs
|
||||||
|
fVslL6atrjANBgkqhkiG9w0BAQsFAAOCAQEAV4TZ3b8cYO7ZeRyoCQtCBAab9gNe
|
||||||
|
kbQpWqICvkVQOk5Anq3opwAWk2FuIRs5KoT7ssckHpXwTwWLs+KuIVo+Fet19IH6
|
||||||
|
BQfck1jwhzM04MA6zLO/F2j548XlrJy3IzViPM/VxwMMTt5YSoogrz/3TzzJPIe0
|
||||||
|
eQomf5HbpVgrf08pMVkdaI7PCd7N/CxeWiD5zEWqBu9FqofO188Kb/umx0VwgBju
|
||||||
|
dX46MKO5TyUc91UrG3M35/r4Z7fd52SWWWFQiI7UBOl2L27samjHlJsKjyFoBF3Z
|
||||||
|
alvnoUVzo7zwAYmhEdPYDNVceF4KtAFpGipoQPRMg83G87LgYBA4Sa6uKw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
30
server/configs/certs/tls/benchmark-server-cert-rsa-4096.pem
Normal file
30
server/configs/certs/tls/benchmark-server-cert-rsa-4096.pem
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFFzCCA/+gAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsUwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
|
||||||
|
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
|
||||||
|
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
|
||||||
|
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
|
||||||
|
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
|
||||||
|
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIIC
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAj/rw0WpCnizUSs06NxSsjuYb
|
||||||
|
6sWtYQjLd6O6SCoQwrPSY2Zv0u28RywBJkSiIbv12kWxrhqv+kDqhmt7tfwEdHAE
|
||||||
|
gOl6E2P/CFDnyAJ4it5qgFRHcEItGp1Ap4ZrZ3OTl41DDe28giflHb7+VAzxUV6r
|
||||||
|
zkHmrqkaJd41YrJoKyZ3u+/FHs6CudO+jGXC2ubH1i71ARXHn6tkmOTwFf1N49wB
|
||||||
|
Hr144xBmPyvH/A/elgXBjR28/7x64MO/qBZsr/lbTaS4YwN1+rod/HRc2GIiJxpS
|
||||||
|
tB1bh/dXZCfa0QyhTzCNLG/j7IzetDrPBGzpw2WjufhkSuaxoMDWlDkkfqmQxtHJ
|
||||||
|
5L+PqIiPT69tzEbfuS9Ogz7DW10CcpSdXW13sWCdSNCGEvPqLFka36q9B82V0GHz
|
||||||
|
tmx8VqdfWSu4qMyVmTsxxzLTTxpQU4X1Q2RnT/1igbsOM620LuvrvnXlh5Rdg7tp
|
||||||
|
T++QJ/b4xCCg62tv4VwORe27xHYXMeDn4aoRdyoI45/+ZK6yqwNAOrKKpse/M8uz
|
||||||
|
mJK2i8pfEFmitIKoNYn3MR2dFrCqifZkFf9rX9A/1Ym+WKAPLmWdGp13fvTdzxQG
|
||||||
|
Y44f9tBL2RWsoGX++01XEwIiWz7kqObC0L8fz3EdIPaULX7MZiQrxzzhRCcJhyn/
|
||||||
|
aOrJfLYj0GAmIaHElHkCAwEAAaOBkTCBjjALBgNVHQ8EBAMCBDAwEwYDVR0lBAww
|
||||||
|
CgYIKwYBBQUHAwEwKgYDVR0RBCMwIYIOcmV1YmVuLm5hdHMuaW+CD3JldWJlbi5u
|
||||||
|
YXRzLmNvbTAdBgNVHQ4EFgQUbJSM8LNWmc9IgLe0X53yE3c3h1cwHwYDVR0jBBgw
|
||||||
|
FoAUEqfeAemfeIp4MM4C7H1bJS+mra4wDQYJKoZIhvcNAQELBQADggEBAEGmLvEE
|
||||||
|
+MTE1bHMbl/5QG+/xusmervIuxkfAfId0H+8TWB75y+yhVZpEdM7knfl+lexmtGQ
|
||||||
|
Gr4HNGTZhAZ3NYFaBw7nfeqO48He7gHUKfJA/zv7FREF3Fy+Qe/hydDJQzBzZfaU
|
||||||
|
64XqhY6jOurpZhTAoOXfjpYzZaLi7+rdpTAbfxHCCAC8SxZD3++Q97ZeoT6en47O
|
||||||
|
8SQQ7FIzWxs15k88oYalw51vZujxX7dz4l+LxsLXtlYW7ZM1163cgU7lF/jQqDcN
|
||||||
|
z8X8jk1AjQY7AuFPuOzQ1hLXcZySm8rUG5pPgHrZ1QKmkFFWRaeCiO2hU794wI5C
|
||||||
|
vIGR8lIhkNwEJnQ=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIJRCtUNxUuutNs9j8OtcwFw1xkbs+zxjHhpAqVuqDNo5
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user