mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-08 11:53:07 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
392e8965d0 | ||
|
|
5caa34d51a | ||
|
|
8f743fe9f3 | ||
|
|
08eb7321ee | ||
|
|
66ba9504a2 | ||
|
|
07859d9ac4 | ||
|
|
2aa6943eaf | ||
|
|
a7bb32bf64 | ||
|
|
df4bc7c1d8 | ||
|
|
2ef5d1289d | ||
|
|
8ec82331cc | ||
|
|
15fc59626b | ||
|
|
813f42f825 | ||
|
|
f0e9e10fa7 | ||
|
|
32549ae0e1 | ||
|
|
f70ce6d8a8 | ||
|
|
e9ce946fe0 | ||
|
|
c0b0910000 | ||
|
|
4af5add77d | ||
|
|
01686db6d9 | ||
|
|
3434f55c78 | ||
|
|
7374ae61a9 | ||
|
|
62674c71cf | ||
|
|
193431b875 | ||
|
|
5e889612d6 | ||
|
|
f4d1fb2cdf | ||
|
|
d7f87eeb0b | ||
|
|
6ba2cdf7b6 | ||
|
|
c8769532d4 | ||
|
|
9995160624 | ||
|
|
b55462b185 | ||
|
|
9c4ac18e47 | ||
|
|
8cf768bf80 | ||
|
|
336acffb16 | ||
|
|
09d95eb415 | ||
|
|
8f7ad7f328 | ||
|
|
228adbe177 | ||
|
|
de1120210e | ||
|
|
ffea35ebf5 | ||
|
|
f8bed839ce | ||
|
|
8a667ff1c7 | ||
|
|
2a2d925ee4 | ||
|
|
9cf02e5c7e | ||
|
|
44fc25dbf6 | ||
|
|
8d99cf3f8b | ||
|
|
a5f5009f9d | ||
|
|
6af2c44f7f | ||
|
|
f7a86d681a | ||
|
|
a9660e3e8c | ||
|
|
66d220ff44 | ||
|
|
2e36859816 | ||
|
|
06708d94a9 | ||
|
|
5aefcb9544 | ||
|
|
ca559bc3fb | ||
|
|
535a5d67e9 | ||
|
|
98b6c53270 | ||
|
|
fe84d0dec4 | ||
|
|
17606da390 | ||
|
|
239ce6a794 | ||
|
|
4f36445048 | ||
|
|
312eacd46a | ||
|
|
3c906e1dae | ||
|
|
fc5ee78dc8 | ||
|
|
913c51d8d5 | ||
|
|
052ee8910d | ||
|
|
ed285049dc | ||
|
|
867da079a8 | ||
|
|
389638878a | ||
|
|
949f14a278 | ||
|
|
27450c97bb |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
# The test runner source for UI tests
|
||||
WEB_COMMITID=50e3fff6a518361d59cba864a927470f313b6f91
|
||||
WEB_COMMITID=c247f854a95b7bcaf10f27577458d2f566b2f939
|
||||
WEB_BRANCH=stable-4.2
|
||||
|
||||
+518
-340
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,82 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.7](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.7) - 2026-05-18
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@ScharfViktor, @rhafer
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- [stable/4.0] Backports [[#2761](https://github.com/opencloud-eu/opencloud/pull/2761)]
|
||||
|
||||
## [4.0.6](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.6) - 2026-04-29
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@ScharfViktor, @dragonchaser, @kulmann, @rhafer
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- [stable-4.0] More graceful shutdown fixes [[#2692](https://github.com/opencloud-eu/opencloud/pull/2692)]
|
||||
|
||||
## [4.0.5](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.5) - 2026-04-02
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@ScharfViktor, @rhafer
|
||||
|
||||
### 📦️ Dependencies
|
||||
|
||||
- Bump go dependencies with known vulnerabilities [[#2566](https://github.com/opencloud-eu/opencloud/pull/2566)]
|
||||
|
||||
## [4.0.4](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.4) - 2026-03-30
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@ScharfViktor, @aduffeck, @dragonchaser, @rhafer, @saw-jan
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Backport bleve batch fix [[#2527](https://github.com/opencloud-eu/opencloud/pull/2527)]
|
||||
- Backport backchannel logout [[#2407](https://github.com/opencloud-eu/opencloud/pull/2407)]
|
||||
|
||||
### 📦️ Dependencies
|
||||
|
||||
- [stable-4.0] Bump some go dependencies [[#2544](https://github.com/opencloud-eu/opencloud/pull/2544)]
|
||||
|
||||
## [4.0.3](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.3) - 2026-02-05
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@aduffeck
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Bump reva 2.40.3 [[#2275](https://github.com/opencloud-eu/opencloud/pull/2275)]
|
||||
|
||||
## [4.0.2](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.2) - 2026-02-05
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
@ScharfViktor, @aduffeck, @fschade, @kulmann, @micbar, @prashant-gurung899, @saw-jan
|
||||
|
||||
### ✅ Tests
|
||||
|
||||
- [tests-only] port tests to stable #2087 #2039 [[#2185](https://github.com/opencloud-eu/opencloud/pull/2185)]
|
||||
- [full-ci][tests-only] port test fixes [[#2017](https://github.com/opencloud-eu/opencloud/pull/2017)]
|
||||
- [stable-4.0] Port #2011 [[#2018](https://github.com/opencloud-eu/opencloud/pull/2018)]
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- [stable-4.0] fix: build time edition channels #2001 [[#2010](https://github.com/opencloud-eu/opencloud/pull/2010)]
|
||||
- [stable-4.0] fix: enforce trailing slash for server url [[#2002](https://github.com/opencloud-eu/opencloud/pull/2002)]
|
||||
- [stable-4.0] fix: enhance resource creation with detailed process information (#1978) [[#2000](https://github.com/opencloud-eu/opencloud/pull/2000)]
|
||||
|
||||
### 📦️ Dependencies
|
||||
|
||||
- Bump reva [[#2274](https://github.com/opencloud-eu/opencloud/pull/2274)]
|
||||
|
||||
## [4.0.1](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.1) - 2025-12-15
|
||||
|
||||
### ❤️ Thanks to all contributors! ❤️
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ COPY --from=generate /opencloud /opencloud
|
||||
WORKDIR /opencloud/opencloud
|
||||
RUN make go-generate build ENABLE_VIPS=true
|
||||
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk add --no-cache attr ca-certificates curl mailcap tree vips && \
|
||||
echo 'hosts: files dns' >| /etc/nsswitch.conf
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "opencloud-eu/opencloud",
|
||||
"config" : {
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.2"
|
||||
"php": "8.4"
|
||||
},
|
||||
"vendor-dir": "./vendor-php",
|
||||
"allow-plugins": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/opencloud-eu/opencloud
|
||||
|
||||
go 1.24.6
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
@@ -54,8 +54,8 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/mna/pigeon v1.3.0
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/nats-io/nats-server/v2 v2.12.2
|
||||
github.com/nats-io/nats.go v1.47.0
|
||||
github.com/nats-io/nats-server/v2 v2.12.6
|
||||
github.com/nats-io/nats.go v1.49.0
|
||||
github.com/oklog/run v1.2.0
|
||||
github.com/olekukonko/tablewriter v1.1.1
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
@@ -64,7 +64,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.10.1
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
|
||||
github.com/opencloud-eu/reva/v2 v2.40.1
|
||||
github.com/opencloud-eu/reva/v2 v2.40.4
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.5.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -96,21 +96,21 @@ require (
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/contrib/zpages v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/image v0.33.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/text v0.31.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8
|
||||
google.golang.org/grpc v1.77.0
|
||||
golang.org/x/image v0.38.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/text v0.35.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -134,7 +134,7 @@ require (
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/alexedwards/argon2id v1.0.0 // indirect
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -240,7 +240,7 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v1.9.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/renameio/v2 v2.0.1 // indirect
|
||||
github.com/gookit/goutil v0.7.1 // indirect
|
||||
@@ -249,6 +249,7 @@ require (
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
@@ -259,7 +260,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juliangruber/go-intersect v1.1.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
@@ -305,8 +306,8 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
@@ -332,7 +333,7 @@ require (
|
||||
github.com/prometheus/statsd_exporter v0.22.8 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russellhaering/goxmldsig v1.5.0 // indirect
|
||||
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
@@ -378,18 +379,18 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
|
||||
@@ -119,8 +119,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
@@ -572,8 +572,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -648,6 +648,8 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
@@ -725,8 +727,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -908,14 +910,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
|
||||
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.12.2 h1:4TEQd0Y4zvcW0IsVxjlXnRso1hBkQl3TS0BI+SxgPhE=
|
||||
github.com/nats-io/nats-server/v2 v2.12.2/go.mod h1:j1AAttYeu7WnvD8HLJ+WWKNMSyxsqmZ160pNtCQRMyE=
|
||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8=
|
||||
github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo=
|
||||
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
@@ -963,8 +965,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.40.1 h1:QwMkbGMhwDSwfk2WxbnTpIig2BugPBaVFjWcy2DSU3U=
|
||||
github.com/opencloud-eu/reva/v2 v2.40.1/go.mod h1:DGH08n2mvtsQLkt8o15FV6m51FwSJJGhjR8Ty+iIJww=
|
||||
github.com/opencloud-eu/reva/v2 v2.40.4 h1:KTeFLHSwRp2bnK9J1dtaAGwPCreEiijYIXSyQ1A5tbo=
|
||||
github.com/opencloud-eu/reva/v2 v2.40.4/go.mod h1:NN88hQAbjCVq011D2zhr0IOZ9Xclz3HS+Wf961YHwbg=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -1084,8 +1086,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
|
||||
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
|
||||
github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks=
|
||||
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -1299,8 +1301,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/contrib/zpages v0.63.0 h1:TppOKuZGbqXMgsfjqq3i09N5Vbo1JLtLImUqiTPGnX4=
|
||||
go.opentelemetry.io/contrib/zpages v0.63.0/go.mod h1:5F8uugz75ay/MMhRRhxAXY33FuaI8dl7jTxefrIy5qk=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
@@ -1309,14 +1311,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -1359,8 +1361,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1376,8 +1378,8 @@ golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScy
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1402,8 +1404,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1457,8 +1459,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1466,8 +1468,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1485,8 +1487,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1570,8 +1572,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1583,8 +1585,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1599,16 +1601,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -1663,8 +1665,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1728,10 +1730,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -1747,8 +1749,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e h1:m7aQHHqd0q89mRwhwS9Bx2rjyl/hsFAeta+uGrHsQaU=
|
||||
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e/go.mod h1:gID3PKrg7pWKntu9Ss6zTLJ0ttC0X9IHgREOCZwbCVU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:alpine3.22 AS build
|
||||
FROM golang:alpine3.23 AS build
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION
|
||||
@@ -14,7 +14,7 @@ RUN --mount=type=bind,target=/build,rw \
|
||||
GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH:-amd64}" ; \
|
||||
make -C opencloud/opencloud release-linux-docker-${TARGETARCH} ENABLE_VIPS=true DIST=/dist
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
ARG TARGETOS
|
||||
|
||||
@@ -434,12 +434,9 @@ func Start(ctx context.Context, o ...Option) error {
|
||||
// prepare the set of services to run
|
||||
s.generateRunSet(s.cfg)
|
||||
|
||||
// There are reasons not to do this, but we have race conditions ourselves. Until we resolve them, mind the following disclaimer:
|
||||
// Calling ServeBackground will CORRECTLY start the supervisor running in a new goroutine. It is risky to directly run
|
||||
// go supervisor.Serve()
|
||||
// because that will briefly create a race condition as it starts up, if you try to .Add() services immediately afterward.
|
||||
// https://pkg.go.dev/github.com/thejerf/suture/v4@v4.0.0#Supervisor
|
||||
go s.Supervisor.ServeBackground(ctx)
|
||||
// We need to control the order in which services are started and shut down,
|
||||
// so we need a backgroud context that will outlive the service execution.
|
||||
go s.Supervisor.ServeBackground(context.Background())
|
||||
|
||||
for i, service := range s.Services {
|
||||
scheduleServiceTokens(s, service)
|
||||
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
// LatestTag is the latest released version plus the dev meta version.
|
||||
// Will be overwritten by the release pipeline
|
||||
// Needs a manual change for every tagged release
|
||||
LatestTag = "4.0.1+dev"
|
||||
LatestTag = "4.0.7+dev"
|
||||
|
||||
// Date indicates the build date.
|
||||
// This has been removed, it looks like you can only replace static strings with recent go versions
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"build": "node --openssl-legacy-provider scripts/build.js && rm -f build/service-worker.js",
|
||||
"licenses": "NODE_PATH=./node_modules node ../scripts/js-license-ranger.js",
|
||||
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;ODC-By-1.0;BlueOak-1.0.0;OFL-1.1' --excludePackages 'identifier;kpop;unicoderegexp' --clarificationsFile license-checker-clarifications.json",
|
||||
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;ODC-By-1.0;BlueOak-1.0.0;OFL-1.1' --excludePackages 'identifier;unicoderegexp' --clarificationsFile license-checker-clarifications.json",
|
||||
"licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ../../third-party-licenses/node/idp/third-party-licenses.csv",
|
||||
"licenses:save": "license-checker-rseidelsohn --relativeLicensePath --out /dev/null --files ../../third-party-licenses/node/idp/third-party-licenses",
|
||||
"lint": "eslint ./**/*.{tsx,ts,jsx,js}",
|
||||
@@ -89,7 +89,6 @@
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"kpop": "https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz",
|
||||
"query-string": "^9.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
@@ -154,10 +153,5 @@
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"kpop>cldr": ""
|
||||
}
|
||||
}
|
||||
"packageManager": "pnpm@9.15.4"
|
||||
}
|
||||
Generated
-151
@@ -4,9 +4,6 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
kpop>cldr: ''
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -65,9 +62,6 @@ importers:
|
||||
i18next-resources-to-backend:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
kpop:
|
||||
specifier: https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz
|
||||
version: https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz(@gluejs/glue@0.3.0)(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@material-ui/icons@4.11.3(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(notistack@0.8.9(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(oidc-client@1.11.5)(react-dom@17.0.2(react@17.0.2))(react-intl@2.9.0(prop-types@15.8.1)(react@17.0.2))(react@17.0.2)
|
||||
query-string:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
@@ -1516,9 +1510,6 @@ packages:
|
||||
'@fontsource/roboto@5.2.5':
|
||||
resolution: {integrity: sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==}
|
||||
|
||||
'@gluejs/glue@0.3.0':
|
||||
resolution: {integrity: sha512-byvFoZCbZW+A3Pg8JUU+8FjoPuF5l1v7mDeLJQP/YSeEcEDiD/YdUKLBUapPrcuyxclrtS8+peX4cxkh6awwTw==}
|
||||
|
||||
'@gulpjs/to-absolute-glob@4.0.0':
|
||||
resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -2724,9 +2715,6 @@ packages:
|
||||
core-js@3.40.0:
|
||||
resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==}
|
||||
|
||||
core-js@3.43.0:
|
||||
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@@ -2738,11 +2726,6 @@ packages:
|
||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
crc32@0.2.2:
|
||||
resolution: {integrity: sha512-PFZEGbDUeoNbL2GHIEpJRQGheXReDody/9axKTxhXtQqIL443wnNigtVZO9iuCIMPApKZRv7k2xr8euXHqNxQQ==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
cross-fetch@4.0.0:
|
||||
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||
|
||||
@@ -2754,9 +2737,6 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3718,9 +3698,6 @@ packages:
|
||||
resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
hsv-rgb@1.0.0:
|
||||
resolution: {integrity: sha512-Azd6IP11LZm0cEczEnJw5B6zIgWdGlE4TSoM2eh+RPRbXSQCy/0JS2POEq0wOtbAZtxTJhEMGm3GUYGbnTIJGw==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
@@ -3850,23 +3827,6 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
intl-format-cache@2.2.9:
|
||||
resolution: {integrity: sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==}
|
||||
|
||||
intl-messageformat-parser@1.4.0:
|
||||
resolution: {integrity: sha512-/XkqFHKezO6UcF4Av2/Lzfrez18R0jyw7kRFhSeB/YRakdrgSc9QfFZUwNJI9swMwMoNPygK1ArC5wdFSjPw+A==}
|
||||
deprecated: We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser
|
||||
|
||||
intl-messageformat@2.2.0:
|
||||
resolution: {integrity: sha512-I+tSvHnXqJYjDfNmY95tpFMj30yoakC6OXAo+wu/wTMy6tA/4Fd4mvV7Uzs4cqK/Ap29sHhwjcY+78a8eifcXw==}
|
||||
|
||||
intl-relativeformat@2.2.0:
|
||||
resolution: {integrity: sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw==}
|
||||
deprecated: This package has been deprecated, please see migration guide at 'https://github.com/formatjs/formatjs/tree/master/packages/intl-relativeformat#migration-guide'
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
|
||||
is-arguments@1.2.0:
|
||||
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4045,10 +4005,6 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
iso-639-1@2.1.15:
|
||||
resolution: {integrity: sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4346,20 +4302,6 @@ packages:
|
||||
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
kpop@https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz:
|
||||
resolution: {tarball: https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz}
|
||||
version: 2.7.1
|
||||
engines: {node: '>=6.11.0'}
|
||||
peerDependencies:
|
||||
'@gluejs/glue': ^0.3.0
|
||||
'@material-ui/core': ^4.11.0
|
||||
'@material-ui/icons': ^4.9.1
|
||||
notistack: ^0.8.8
|
||||
oidc-client: ^1.11.0
|
||||
react: ^16.8.0 || ^17.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0
|
||||
react-intl: ^2.6.0
|
||||
|
||||
language-subtag-registry@0.3.23:
|
||||
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
||||
|
||||
@@ -4636,13 +4578,6 @@ packages:
|
||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
notistack@0.8.9:
|
||||
resolution: {integrity: sha512-nRHQVWUfgHnvnKrjRbRX9f+YAnbyh96yRyO5bEP/FCLVLuTZcJOwUr0GZ7Xr/8wK3+hXa9JYpXUkUhSxj1K8NQ==}
|
||||
peerDependencies:
|
||||
'@material-ui/core': ^3.2.0 || ^4.0.0
|
||||
react: ^16.8.0
|
||||
react-dom: ^16.8.0
|
||||
|
||||
now-and-later@3.0.0:
|
||||
resolution: {integrity: sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -4698,9 +4633,6 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
oidc-client@1.11.5:
|
||||
resolution: {integrity: sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
@@ -5364,12 +5296,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-intl@2.9.0:
|
||||
resolution: {integrity: sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==}
|
||||
peerDependencies:
|
||||
prop-types: ^15.5.4
|
||||
react: ^0.14.9 || ^15.0.0 || ^16.0.0
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -5698,9 +5624,6 @@ packages:
|
||||
seq@0.3.5:
|
||||
resolution: {integrity: sha512-sisY2Ln1fj43KBkRtXkesnRHYNdswIkIibvNe/0UKm2GZxjMbqmccpiatoKr/k2qX5VKiLU8xm+tz/74LAho4g==}
|
||||
|
||||
serialize-javascript@4.0.0:
|
||||
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
@@ -7918,8 +7841,6 @@ snapshots:
|
||||
|
||||
'@fontsource/roboto@5.2.5': {}
|
||||
|
||||
'@gluejs/glue@0.3.0': {}
|
||||
|
||||
'@gulpjs/to-absolute-glob@4.0.0':
|
||||
dependencies:
|
||||
is-negated-glob: 1.0.0
|
||||
@@ -9403,8 +9324,6 @@ snapshots:
|
||||
|
||||
core-js@3.40.0: {}
|
||||
|
||||
core-js@3.43.0: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cosmiconfig@6.0.0:
|
||||
@@ -9423,8 +9342,6 @@ snapshots:
|
||||
path-type: 4.0.0
|
||||
yaml: 1.10.2
|
||||
|
||||
crc32@0.2.2: {}
|
||||
|
||||
cross-fetch@4.0.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
@@ -9443,8 +9360,6 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
css-blank-pseudo@7.0.1(postcss@8.5.4):
|
||||
@@ -10683,8 +10598,6 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 7.18.3
|
||||
|
||||
hsv-rgb@1.0.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-minifier-terser@6.1.0:
|
||||
@@ -10833,22 +10746,6 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
intl-format-cache@2.2.9: {}
|
||||
|
||||
intl-messageformat-parser@1.4.0: {}
|
||||
|
||||
intl-messageformat@2.2.0:
|
||||
dependencies:
|
||||
intl-messageformat-parser: 1.4.0
|
||||
|
||||
intl-relativeformat@2.2.0:
|
||||
dependencies:
|
||||
intl-messageformat: 2.2.0
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
is-arguments@1.2.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -11007,8 +10904,6 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
iso-639-1@2.1.15: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-instrument@6.0.3:
|
||||
@@ -11539,20 +11434,6 @@ snapshots:
|
||||
|
||||
klona@2.0.6: {}
|
||||
|
||||
kpop@https://download.kopano.io/community/kapp:/kpop-2.7.2.tgz(@gluejs/glue@0.3.0)(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@material-ui/icons@4.11.3(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(notistack@0.8.9(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(oidc-client@1.11.5)(react-dom@17.0.2(react@17.0.2))(react-intl@2.9.0(prop-types@15.8.1)(react@17.0.2))(react@17.0.2):
|
||||
dependencies:
|
||||
'@gluejs/glue': 0.3.0
|
||||
'@material-ui/core': 4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
'@material-ui/icons': 4.11.3(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
crc32: 0.2.2
|
||||
hsv-rgb: 1.0.0
|
||||
iso-639-1: 2.1.15
|
||||
notistack: 0.8.9(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
oidc-client: 1.11.5
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
react-intl: 2.9.0(prop-types@15.8.1)(react@17.0.2)
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
|
||||
language-tags@1.0.9:
|
||||
@@ -11787,16 +11668,6 @@ snapshots:
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
notistack@0.8.9(@material-ui/core@4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react-dom@17.0.2(react@17.0.2))(react@17.0.2):
|
||||
dependencies:
|
||||
'@material-ui/core': 4.12.4(@types/react@17.0.80)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
classnames: 2.5.1
|
||||
hoist-non-react-statics: 3.3.2
|
||||
prop-types: 15.8.1
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
react-is: 16.13.1
|
||||
|
||||
now-and-later@3.0.0:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
@@ -11860,14 +11731,6 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
oidc-client@1.11.5:
|
||||
dependencies:
|
||||
acorn: 7.4.1
|
||||
base64-js: 1.5.1
|
||||
core-js: 3.43.0
|
||||
crypto-js: 4.2.0
|
||||
serialize-javascript: 4.0.0
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
@@ -12599,16 +12462,6 @@ snapshots:
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
typescript: 5.8.3
|
||||
|
||||
react-intl@2.9.0(prop-types@15.8.1)(react@17.0.2):
|
||||
dependencies:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
intl-format-cache: 2.2.9
|
||||
intl-messageformat: 2.2.0
|
||||
intl-relativeformat: 2.2.0
|
||||
invariant: 2.2.4
|
||||
prop-types: 15.8.1
|
||||
react: 17.0.2
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
@@ -12975,10 +12828,6 @@ snapshots:
|
||||
chainsaw: 0.0.9
|
||||
hashish: 0.0.4
|
||||
|
||||
serialize-javascript@4.0.0:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
@@ -2,10 +2,7 @@ import React, {ReactElement, Suspense, lazy, useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MuiThemeProvider} from '@material-ui/core/styles';
|
||||
import {defaultTheme} from 'kpop/es/theme';
|
||||
|
||||
import 'kpop/static/css/base.css';
|
||||
import 'kpop/static/css/scrollbar.css';
|
||||
import muiTheme from './theme';
|
||||
|
||||
import Spinner from './components/Spinner';
|
||||
import * as version from './version';
|
||||
@@ -52,7 +49,7 @@ const App = ({ bgImg }): ReactElement => {
|
||||
className={`oc-login-bg ${bgImg ? 'oc-login-bg-image' : ''}`}
|
||||
style={{backgroundImage: bgImg ? `url(${bgImg})` : undefined}}
|
||||
>
|
||||
<MuiThemeProvider theme={defaultTheme}>
|
||||
<MuiThemeProvider theme={muiTheme}>
|
||||
<Suspense fallback={<Spinner/>}>
|
||||
<LazyMain/>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* additional css on top of kpop */
|
||||
@font-face {
|
||||
font-family: OpenCloud;
|
||||
src: url('./fonts/OpenCloud500-Regular.woff2') format('woff2');
|
||||
@@ -17,16 +16,25 @@
|
||||
html {
|
||||
font-feature-settings: "cv11";
|
||||
color: #20434f !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: OpenCloud, sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.oc-font-weight-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
gr.Add(runner.New(cfg.Service.Name+".svc", func() error {
|
||||
return natsServer.ListenAndServe()
|
||||
}, func() {
|
||||
logger.Info().Msg("Gracefully shutting down the NATS server...")
|
||||
natsServer.Shutdown()
|
||||
logger.Info().Msg("NATS server shutdown")
|
||||
}))
|
||||
|
||||
grResults := gr.Run(ctx)
|
||||
|
||||
@@ -88,7 +88,7 @@ func NewPostprocessingService(ctx context.Context, logger log.Logger, sto store.
|
||||
|
||||
m := metrics.New()
|
||||
m.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
monitorMetrics(raw, "postprocessing-pull", m, logger)
|
||||
monitorMetrics(ctx, raw, "postprocessing-pull", m, logger)
|
||||
|
||||
return &PostprocessingService{
|
||||
ctx: ctx,
|
||||
@@ -425,25 +425,30 @@ func (pps *PostprocessingService) findUploadsByStep(step events.Postprocessingst
|
||||
return ids
|
||||
}
|
||||
|
||||
func monitorMetrics(stream raw.Stream, name string, m *metrics.Metrics, logger log.Logger) {
|
||||
ctx := context.Background()
|
||||
func monitorMetrics(ctx context.Context, stream raw.Stream, name string, m *metrics.Metrics, logger log.Logger) {
|
||||
consumer, err := stream.JetStream().Consumer(ctx, name)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
}
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
info, err := consumer.Info(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
continue
|
||||
}
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
info, err := consumer.Info(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
continue
|
||||
}
|
||||
|
||||
m.EventsOutstandingAcks.Set(float64(info.NumAckPending))
|
||||
m.EventsUnprocessed.Set(float64(info.NumPending))
|
||||
m.EventsRedelivered.Set(float64(info.NumRedelivered))
|
||||
logger.Trace().Msg("updated postprocessing event metrics")
|
||||
m.EventsOutstandingAcks.Set(float64(info.NumAckPending))
|
||||
m.EventsUnprocessed.Set(float64(info.NumPending))
|
||||
m.EventsRedelivered.Set(float64(info.NumRedelivered))
|
||||
logger.Trace().Msg("updated postprocessing event metrics")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -12,3 +12,8 @@ packages:
|
||||
github.com/opencloud-eu/opencloud/services/proxy/pkg/userroles:
|
||||
interfaces:
|
||||
UserRoleAssigner: {}
|
||||
go-micro.dev/v4/store:
|
||||
config:
|
||||
dir: pkg/staticroutes/internal/backchannellogout/mocks
|
||||
interfaces:
|
||||
Store: {}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/justinas/alice"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
"github.com/opencloud-eu/opencloud/pkg/generators"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -73,6 +74,7 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...),
|
||||
microstore.Database("proxy"),
|
||||
microstore.Table("signing-keys"),
|
||||
store.DisablePersistence(cfg.PreSignedURL.SigningKeys.DisablePersistence),
|
||||
store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
store "go-micro.dev/v4/store"
|
||||
"go-micro.dev/v4/store"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/oauth2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -114,16 +116,25 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
m.Logger.Error().Err(err).Msg("failed to write to userinfo cache")
|
||||
}
|
||||
|
||||
if sid := aClaims.SessionID; sid != "" {
|
||||
// reuse user cache for session id lookup
|
||||
err = m.userInfoCache.Write(&store.Record{
|
||||
Key: sid,
|
||||
Value: []byte(encodedHash),
|
||||
Expiry: time.Until(expiration),
|
||||
})
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
|
||||
}
|
||||
// fail if creating the storage key fails,
|
||||
// it means there is no subject and no session.
|
||||
//
|
||||
// ok: {key: ".sessionId"}
|
||||
// ok: {key: "subject."}
|
||||
// ok: {key: "subject.sessionId"}
|
||||
// fail: {key: "."}
|
||||
subjectSessionKey, err := staticroutes.NewRecordKey(aClaims.Subject, aClaims.SessionID)
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to build subject.session")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.userInfoCache.Write(&store.Record{
|
||||
Key: subjectSessionKey,
|
||||
Value: []byte(encodedHash),
|
||||
Expiry: time.Until(expiration),
|
||||
}); err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -6,17 +6,40 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/opencloud-eu/opencloud/pkg/oidc"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
bcl "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes/internal/backchannellogout"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
|
||||
// NewRecordKey converts the subject and session to a base64 encoded key
|
||||
var NewRecordKey = bcl.NewKey
|
||||
|
||||
// backchannelLogout handles backchannel logout requests from the identity provider and invalidates the related sessions in the cache
|
||||
// spec: https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
|
||||
//
|
||||
// known side effects of backchannel logout in keycloak:
|
||||
//
|
||||
// - keyCloak "Sign out all active sessions" does not send a backchannel logout request,
|
||||
// as the devs mention, this may lead to thousands of backchannel logout requests,
|
||||
// therefore, they recommend a short token lifetime.
|
||||
// https://github.com/keycloak/keycloak/issues/27342#issuecomment-2408461913
|
||||
//
|
||||
// - keyCloak user self-service portal, "Sign out all devices" may not send a backchannel
|
||||
// logout request for each session, it's not mentionex explicitly,
|
||||
// but maybe the reason for that is the same as for "Sign out all active sessions"
|
||||
// to prevent a flood of backchannel logout requests.
|
||||
//
|
||||
// - if the keycloak setting "Backchannel logout session required" is disabled (or the token has no session id),
|
||||
// we resolve the session by the subject which can lead to multiple session records (subject.*),
|
||||
// we then send a logout event (sse) to each connected client and delete our stored cache record (subject.session & claim).
|
||||
// all sessions besides the one that triggered the backchannel logout continue to exist in the identity provider,
|
||||
// so the user will not be fully logged out until all sessions are logged out or expired.
|
||||
// this leads to the situation that web renders the logout view even if the instance is not fully logged out yet.
|
||||
func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) {
|
||||
// parse the application/x-www-form-urlencoded POST request
|
||||
logger := s.Logger.SubloggerWithRequestID(r.Context())
|
||||
if err := r.ParseForm(); err != nil {
|
||||
logger.Warn().Err(err).Msg("ParseForm failed")
|
||||
@@ -27,45 +50,86 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
|
||||
|
||||
logoutToken, err := s.OidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token"))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("VerifyLogoutToken failed")
|
||||
msg := "failed to verify logout token"
|
||||
logger.Warn().Err(err).Msg(msg)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg})
|
||||
return
|
||||
}
|
||||
|
||||
records, err := s.UserInfoCache.Read(logoutToken.SessionId)
|
||||
if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 {
|
||||
lookupKey, err := bcl.NewKey(logoutToken.Subject, logoutToken.SessionId)
|
||||
if err != nil {
|
||||
msg := "failed to build key from logout token"
|
||||
logger.Warn().Err(err).Msg(msg)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg})
|
||||
return
|
||||
}
|
||||
|
||||
requestSubjectAndSession, err := bcl.NewSuSe(lookupKey)
|
||||
if err != nil {
|
||||
msg := "failed to build subjec.session from lookupKey"
|
||||
logger.Error().Err(err).Msg(msg)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg})
|
||||
return
|
||||
}
|
||||
|
||||
lookupRecords, err := bcl.GetLogoutRecords(requestSubjectAndSession, s.UserInfoCache)
|
||||
if errors.Is(err, microstore.ErrNotFound) || len(lookupRecords) == 0 {
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error reading userinfo cache")
|
||||
msg := "failed to read userinfo cache"
|
||||
logger.Error().Err(err).Msg(msg)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg})
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
err := s.publishBackchannelLogoutEvent(r.Context(), record, logoutToken)
|
||||
for _, record := range lookupRecords {
|
||||
// the record key is in the format "subject.session" or ".session"
|
||||
// the record value is the key of the record that contains the claim in its value
|
||||
key, value := record.Key, string(record.Value)
|
||||
|
||||
subjectSession, err := bcl.NewSuSe(key)
|
||||
if err != nil {
|
||||
s.Logger.Warn().Err(err).Msg("could not publish backchannel logout event")
|
||||
// never leak any key-related information
|
||||
logger.Warn().Err(err).Msgf("failed to parse key: %s", key)
|
||||
continue
|
||||
}
|
||||
err = s.UserInfoCache.Delete(string(record.Value))
|
||||
|
||||
session, err := subjectSession.Session()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("failed to read session for: %s", key)
|
||||
continue
|
||||
}
|
||||
|
||||
if requestSubjectAndSession.Mode() == bcl.LogoutModeSession {
|
||||
if err := s.publishBackchannelLogoutEvent(r.Context(), session, value); err != nil {
|
||||
s.Logger.Warn().Err(err).Msgf("failed to publish backchannel logout event for: %s", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = s.UserInfoCache.Delete(value)
|
||||
if err != nil && !errors.Is(err, microstore.ErrNotFound) {
|
||||
// Spec requires us to return a 400 BadRequest when the session could not be destroyed
|
||||
logger.Err(err).Msg("could not delete user info from cache")
|
||||
// we have to return a 400 BadRequest when we fail to delete the session
|
||||
// https://openid.net/specs/openid-connect-backchannel-1_0.html#rfc.section.2.8
|
||||
msg := "failed to delete record"
|
||||
s.Logger.Warn().Err(err).Msgf("%s for: %s", msg, key)
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: msg})
|
||||
return
|
||||
}
|
||||
logger.Debug().Msg("Deleted userinfo from cache")
|
||||
}
|
||||
|
||||
// we can ignore errors when cleaning up the lookup table
|
||||
err = s.UserInfoCache.Delete(logoutToken.SessionId)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("Failed to cleanup sessionid lookup entry")
|
||||
// we can ignore errors when deleting the lookup record
|
||||
err = s.UserInfoCache.Delete(key)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msgf("failed to delete record for: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
@@ -73,41 +137,42 @@ func (s *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
// publishBackchannelLogoutEvent publishes a backchannel logout event when the callback revived from the identity provider
|
||||
func (s StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, record *microstore.Record, logoutToken *oidc.LogoutToken) error {
|
||||
func (s *StaticRouteHandler) publishBackchannelLogoutEvent(ctx context.Context, sessionId, claimKey string) error {
|
||||
if s.EventsPublisher == nil {
|
||||
return fmt.Errorf("the events publisher is not set")
|
||||
return errors.New("events publisher not set")
|
||||
}
|
||||
urecords, err := s.UserInfoCache.Read(string(record.Value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading userinfo cache: %w", err)
|
||||
}
|
||||
if len(urecords) == 0 {
|
||||
return fmt.Errorf("userinfo not found")
|
||||
|
||||
claimRecords, err := s.UserInfoCache.Read(claimKey)
|
||||
switch {
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to read userinfo cache: %w", err)
|
||||
case len(claimRecords) == 0:
|
||||
return fmt.Errorf("no claim found for key: %s", claimKey)
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err = msgpack.Unmarshal(urecords[0].Value, &claims); err != nil {
|
||||
return fmt.Errorf("could not unmarshal userinfo: %w", err)
|
||||
if err = msgpack.Unmarshal(claimRecords[0].Value, &claims); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal claims: %w", err)
|
||||
}
|
||||
|
||||
oidcClaim, ok := claims[s.Config.UserOIDCClaim].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not get claim %w", err)
|
||||
return fmt.Errorf("failed to get claim %w", err)
|
||||
}
|
||||
|
||||
user, _, err := s.UserProvider.GetUserByClaims(ctx, s.Config.UserCS3Claim, oidcClaim)
|
||||
if err != nil || user.GetId() == nil {
|
||||
return fmt.Errorf("could not get user by claims: %w", err)
|
||||
return fmt.Errorf("failed to get user by claims: %w", err)
|
||||
}
|
||||
|
||||
e := events.BackchannelLogout{
|
||||
Executant: user.GetId(),
|
||||
SessionId: logoutToken.SessionId,
|
||||
SessionId: sessionId,
|
||||
Timestamp: utils.TSNow(),
|
||||
}
|
||||
|
||||
if err := events.Publish(ctx, s.EventsPublisher, e); err != nil {
|
||||
return fmt.Errorf("could not publish user created event %w", err)
|
||||
return fmt.Errorf("failed to publish user logout event %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// package backchannellogout provides functions to classify and lookup
|
||||
// backchannel logout records from the cache store.
|
||||
|
||||
package backchannellogout
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// keyEncoding is the base64 encoding used for session and subject keys
|
||||
var keyEncoding = base64.URLEncoding
|
||||
|
||||
// ErrInvalidKey indicates that the provided key does not conform to the expected format.
|
||||
var ErrInvalidKey = errors.New("invalid key format")
|
||||
|
||||
// NewKey converts the subject and session to a base64 encoded key
|
||||
func NewKey(subject, session string) (string, error) {
|
||||
subjectSession := strings.Join([]string{
|
||||
keyEncoding.EncodeToString([]byte(subject)),
|
||||
keyEncoding.EncodeToString([]byte(session)),
|
||||
}, ".")
|
||||
|
||||
if subjectSession == "." {
|
||||
return "", ErrInvalidKey
|
||||
}
|
||||
|
||||
return subjectSession, nil
|
||||
}
|
||||
|
||||
// LogoutMode defines the mode of backchannel logout, either by session or by subject
|
||||
type LogoutMode int
|
||||
|
||||
const (
|
||||
// LogoutModeUndefined is used when the logout mode cannot be determined
|
||||
LogoutModeUndefined LogoutMode = iota
|
||||
// LogoutModeSubject is used when the logout mode is determined by the subject
|
||||
LogoutModeSubject
|
||||
// LogoutModeSession is used when the logout mode is determined by the session id
|
||||
LogoutModeSession
|
||||
)
|
||||
|
||||
// ErrDecoding is returned when decoding fails
|
||||
var ErrDecoding = errors.New("failed to decode")
|
||||
|
||||
// SuSe 🦎 ;) is a struct that groups the subject and session together
|
||||
// to prevent mix-ups for ('session, subject' || 'subject, session')
|
||||
// return values.
|
||||
type SuSe struct {
|
||||
encodedSubject string
|
||||
encodedSession string
|
||||
}
|
||||
|
||||
// Subject decodes and returns the subject or an error
|
||||
func (suse SuSe) Subject() (string, error) {
|
||||
subject, err := keyEncoding.DecodeString(suse.encodedSubject)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to decode subject"), ErrDecoding, err)
|
||||
}
|
||||
|
||||
return string(subject), nil
|
||||
}
|
||||
|
||||
// Session decodes and returns the session or an error
|
||||
func (suse SuSe) Session() (string, error) {
|
||||
subject, err := keyEncoding.DecodeString(suse.encodedSession)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to decode session"), ErrDecoding, err)
|
||||
}
|
||||
|
||||
return string(subject), nil
|
||||
}
|
||||
|
||||
// Mode determines the backchannel logout mode based on the presence of subject and session
|
||||
func (suse SuSe) Mode() LogoutMode {
|
||||
switch {
|
||||
case suse.encodedSession == "" && suse.encodedSubject != "":
|
||||
return LogoutModeSubject
|
||||
case suse.encodedSession != "":
|
||||
return LogoutModeSession
|
||||
default:
|
||||
return LogoutModeUndefined
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInvalidSubjectOrSession is returned when the provided key does not match the expected key format
|
||||
var ErrInvalidSubjectOrSession = errors.New("invalid subject or session")
|
||||
|
||||
// NewSuSe parses the subject and session id from the given key and returns a SuSe struct
|
||||
func NewSuSe(key string) (SuSe, error) {
|
||||
suse := SuSe{}
|
||||
keys := strings.Split(key, ".")
|
||||
switch len(keys) {
|
||||
case 1:
|
||||
suse.encodedSession = keys[0]
|
||||
case 2:
|
||||
suse.encodedSubject = keys[0]
|
||||
suse.encodedSession = keys[1]
|
||||
default:
|
||||
return suse, ErrInvalidSubjectOrSession
|
||||
}
|
||||
|
||||
if suse.encodedSubject == "" && suse.encodedSession == "" {
|
||||
return suse, ErrInvalidSubjectOrSession
|
||||
}
|
||||
|
||||
if _, err := suse.Subject(); err != nil {
|
||||
return suse, errors.Join(ErrInvalidSubjectOrSession, err)
|
||||
}
|
||||
|
||||
if _, err := suse.Session(); err != nil {
|
||||
return suse, errors.Join(ErrInvalidSubjectOrSession, err)
|
||||
}
|
||||
|
||||
if mode := suse.Mode(); mode == LogoutModeUndefined {
|
||||
return suse, ErrInvalidSubjectOrSession
|
||||
}
|
||||
|
||||
return suse, nil
|
||||
}
|
||||
|
||||
// ErrSuspiciousCacheResult is returned when the cache result is suspicious
|
||||
var ErrSuspiciousCacheResult = errors.New("suspicious cache result")
|
||||
|
||||
// GetLogoutRecords retrieves the records from the user info cache based on the backchannel
|
||||
// logout mode and the provided SuSe struct.
|
||||
// it uses a seperator to prevent sufix and prefix exploration in the cache and checks
|
||||
// if the retrieved records match the requested subject and or session id as well, to prevent false positives.
|
||||
func GetLogoutRecords(suse SuSe, store microstore.Store) ([]*microstore.Record, error) {
|
||||
var key string
|
||||
var opts []microstore.ReadOption
|
||||
switch {
|
||||
case suse.Mode() == LogoutModeSubject && suse.encodedSubject != "":
|
||||
// the dot at the end prevents prefix exploration in the cache,
|
||||
// so only keys that start with 'subject.*' will be returned, but not 'sub*'.
|
||||
key = suse.encodedSubject + "."
|
||||
opts = append(opts, microstore.ReadPrefix())
|
||||
case suse.Mode() == LogoutModeSession && suse.encodedSession != "":
|
||||
// the dot at the beginning prevents sufix exploration in the cache,
|
||||
// so only keys that end with '*.session' will be returned, but not '*sion'.
|
||||
key = "." + suse.encodedSession
|
||||
opts = append(opts, microstore.ReadSuffix())
|
||||
default:
|
||||
return nil, errors.Join(errors.New("cannot determine logout mode"), ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
// the go micro memory store requires a limit to work, why???
|
||||
records, err := store.Read(key, append(opts, microstore.ReadLimit(1000))...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, microstore.ErrNotFound
|
||||
}
|
||||
|
||||
if suse.Mode() == LogoutModeSession && len(records) > 1 {
|
||||
return nil, errors.Join(errors.New("multiple session records found"), ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
// double-check if the found records match the requested subject and or session id as well,
|
||||
// to prevent false positives.
|
||||
for _, record := range records {
|
||||
recordSuSe, err := NewSuSe(record.Key)
|
||||
if err != nil {
|
||||
// never leak any key-related information
|
||||
return nil, errors.Join(errors.New("failed to parse key"), ErrSuspiciousCacheResult, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
// in subject mode, the subject must match, but the session id can be different
|
||||
case suse.Mode() == LogoutModeSubject && suse.encodedSubject == recordSuSe.encodedSubject:
|
||||
continue
|
||||
// in session mode, the session id must match, but the subject can be different
|
||||
case suse.Mode() == LogoutModeSession && suse.encodedSession == recordSuSe.encodedSession:
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, errors.Join(errors.New("key does not match the requested subject or session"), ErrSuspiciousCacheResult)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package backchannellogout
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go-micro.dev/v4/store"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes/internal/backchannellogout/mocks"
|
||||
)
|
||||
|
||||
func mustNewKey(t *testing.T, subject, session string) string {
|
||||
key, err := NewKey(subject, session)
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
}
|
||||
|
||||
func mustNewSuSe(t *testing.T, subject, session string) SuSe {
|
||||
suse, err := NewSuSe(mustNewKey(t, subject, session))
|
||||
require.NoError(t, err)
|
||||
return suse
|
||||
}
|
||||
|
||||
func TestNewKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subject string
|
||||
session string
|
||||
wantKey string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "key variation: 'subject.session'",
|
||||
subject: "subject",
|
||||
session: "session",
|
||||
wantKey: "c3ViamVjdA==.c2Vzc2lvbg==",
|
||||
},
|
||||
{
|
||||
name: "key variation: 'subject.'",
|
||||
subject: "subject",
|
||||
wantKey: "c3ViamVjdA==.",
|
||||
},
|
||||
{
|
||||
name: "key variation: '.session'",
|
||||
session: "session",
|
||||
wantKey: ".c2Vzc2lvbg==",
|
||||
},
|
||||
{
|
||||
name: "key variation: '.'",
|
||||
wantErr: ErrInvalidKey,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key, err := NewKey(tt.subject, tt.session)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
require.Equal(t, tt.wantKey, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSuSe(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
wantSubject string
|
||||
wantSession string
|
||||
wantMode LogoutMode
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "key variation: '.session'",
|
||||
key: mustNewKey(t, "", "session"),
|
||||
wantSession: "session",
|
||||
wantMode: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: 'session'",
|
||||
key: mustNewKey(t, "", "session"),
|
||||
wantSession: "session",
|
||||
wantMode: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: 'subject.'",
|
||||
key: mustNewKey(t, "subject", ""),
|
||||
wantSubject: "subject",
|
||||
wantMode: LogoutModeSubject,
|
||||
},
|
||||
{
|
||||
name: "key variation: 'subject.session'",
|
||||
key: mustNewKey(t, "subject", "session"),
|
||||
wantSubject: "subject",
|
||||
wantSession: "session",
|
||||
wantMode: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: 'dot'",
|
||||
key: ".",
|
||||
wantErr: ErrInvalidSubjectOrSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: 'empty'",
|
||||
key: "",
|
||||
wantErr: ErrInvalidSubjectOrSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: string('subject.session')",
|
||||
key: "subject.session",
|
||||
wantErr: ErrInvalidSubjectOrSession,
|
||||
wantMode: LogoutModeSession,
|
||||
},
|
||||
{
|
||||
name: "key variation: string('subject.')",
|
||||
key: "subject.",
|
||||
wantErr: ErrInvalidSubjectOrSession,
|
||||
wantMode: LogoutModeSubject,
|
||||
},
|
||||
{
|
||||
name: "key variation: string('.session')",
|
||||
key: ".session",
|
||||
wantErr: ErrInvalidSubjectOrSession,
|
||||
wantMode: LogoutModeSession,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
suSe, err := NewSuSe(tt.key)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
|
||||
mode := suSe.Mode()
|
||||
require.Equal(t, tt.wantMode, mode)
|
||||
|
||||
subject, _ := suSe.Subject()
|
||||
require.Equal(t, tt.wantSubject, subject)
|
||||
|
||||
session, _ := suSe.Session()
|
||||
require.Equal(t, tt.wantSession, session)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogoutRecords(t *testing.T) {
|
||||
sessionStore := store.NewMemoryStore()
|
||||
|
||||
recordClaimA := &store.Record{Key: "claim-a", Value: []byte("claim-a-data")}
|
||||
recordClaimB := &store.Record{Key: "claim-b", Value: []byte("claim-b-data")}
|
||||
recordClaimC := &store.Record{Key: "claim-c", Value: []byte("claim-c-data")}
|
||||
recordClaimD := &store.Record{Key: "claim-d", Value: []byte("claim-d-data")}
|
||||
recordSessionA := &store.Record{Key: mustNewKey(t, "", "session-a"), Value: []byte(recordClaimA.Key)}
|
||||
recordSessionB := &store.Record{Key: mustNewKey(t, "", "session-b"), Value: []byte(recordClaimB.Key)}
|
||||
recordSubjectASessionC := &store.Record{Key: mustNewKey(t, "subject-a", "session-c"), Value: []byte(recordSessionA.Key)}
|
||||
recordSubjectASessionD := &store.Record{Key: mustNewKey(t, "subject-a", "session-d"), Value: []byte(recordSessionA.Key)}
|
||||
|
||||
for _, r := range []*store.Record{
|
||||
recordClaimA,
|
||||
recordClaimB,
|
||||
recordClaimC,
|
||||
recordClaimD,
|
||||
recordSessionA,
|
||||
recordSessionB,
|
||||
recordSubjectASessionC,
|
||||
recordSubjectASessionD,
|
||||
} {
|
||||
require.NoError(t, sessionStore.Write(r))
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
suSe SuSe
|
||||
store func(t *testing.T) store.Store
|
||||
wantRecords []*store.Record
|
||||
wantErrs []error
|
||||
}{
|
||||
{
|
||||
name: "fails if multiple session records are found",
|
||||
suSe: mustNewSuSe(t, "", "session-a"),
|
||||
store: func(t *testing.T) store.Store {
|
||||
s := mocks.NewStore(t)
|
||||
s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{
|
||||
recordSessionA,
|
||||
recordSessionB,
|
||||
}, nil)
|
||||
return s
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{ErrSuspiciousCacheResult}},
|
||||
{
|
||||
name: "fails if the record key is not ok",
|
||||
suSe: mustNewSuSe(t, "", "session-a"),
|
||||
store: func(t *testing.T) store.Store {
|
||||
s := mocks.NewStore(t)
|
||||
s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{
|
||||
{Key: "invalid.record.key"},
|
||||
}, nil)
|
||||
return s
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{ErrInvalidSubjectOrSession, ErrSuspiciousCacheResult},
|
||||
},
|
||||
{
|
||||
name: "fails if the session does not match the retrieved record",
|
||||
suSe: mustNewSuSe(t, "", "session-a"),
|
||||
store: func(t *testing.T) store.Store {
|
||||
s := mocks.NewStore(t)
|
||||
s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{
|
||||
recordSessionB,
|
||||
}, nil)
|
||||
return s
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{ErrSuspiciousCacheResult}},
|
||||
{
|
||||
name: "fails if the subject does not match the retrieved record",
|
||||
suSe: mustNewSuSe(t, "subject-a", ""),
|
||||
store: func(t *testing.T) store.Store {
|
||||
s := mocks.NewStore(t)
|
||||
s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{
|
||||
recordSessionB,
|
||||
}, nil)
|
||||
return s
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{ErrSuspiciousCacheResult}},
|
||||
// key variation tests
|
||||
{
|
||||
name: "key variation: 'session-a'",
|
||||
suSe: mustNewSuSe(t, "", "session-a"),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{recordSessionA},
|
||||
},
|
||||
{
|
||||
name: "key variation: 'session-b'",
|
||||
suSe: mustNewSuSe(t, "", "session-b"),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{recordSessionB},
|
||||
},
|
||||
{
|
||||
name: "key variation: 'session-c'",
|
||||
suSe: mustNewSuSe(t, "", "session-c"),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{recordSubjectASessionC},
|
||||
},
|
||||
{
|
||||
name: "key variation: 'ession-c'",
|
||||
suSe: mustNewSuSe(t, "", "ession-c"),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{store.ErrNotFound},
|
||||
},
|
||||
{
|
||||
name: "key variation: 'subject-a'",
|
||||
suSe: mustNewSuSe(t, "subject-a", ""),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{recordSubjectASessionC, recordSubjectASessionD},
|
||||
},
|
||||
{
|
||||
name: "key variation: 'subject-'",
|
||||
suSe: mustNewSuSe(t, "subject-", ""),
|
||||
store: func(*testing.T) store.Store {
|
||||
return sessionStore
|
||||
},
|
||||
wantRecords: []*store.Record{},
|
||||
wantErrs: []error{store.ErrNotFound},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
records, err := GetLogoutRecords(tt.suSe, tt.store(t))
|
||||
for _, wantErr := range tt.wantErrs {
|
||||
require.ErrorIs(t, err, wantErr)
|
||||
}
|
||||
require.Len(t, records, len(tt.wantRecords))
|
||||
|
||||
sortRecords := func(r []*store.Record) []*store.Record {
|
||||
slices.SortFunc(r, func(a, b *store.Record) int {
|
||||
return strings.Compare(a.Key, b.Key)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
records = sortRecords(records)
|
||||
for i, wantRecords := range sortRecords(tt.wantRecords) {
|
||||
require.True(t, len(records) >= i+1)
|
||||
require.Equal(t, wantRecords.Key, records[i].Key)
|
||||
require.Equal(t, wantRecords.Value, records[i].Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewStore(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Store {
|
||||
mock := &Store{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Store is an autogenerated mock type for the Store type
|
||||
type Store struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Store_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Store) EXPECT() *Store_Expecter {
|
||||
return &Store_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function for the type Store
|
||||
func (_mock *Store) Close() error {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type Store_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *Store_Expecter) Close() *Store_Close_Call {
|
||||
return &Store_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *Store_Close_Call) Run(run func()) *Store_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Close_Call) Return(err error) *Store_Close_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Close_Call) RunAndReturn(run func() error) *Store_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function for the type Store
|
||||
func (_mock *Store) Delete(key string, opts ...store.DeleteOption) error {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(key, opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called(key)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string, ...store.DeleteOption) error); ok {
|
||||
r0 = returnFunc(key, opts...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type Store_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - key string
|
||||
// - opts ...store.DeleteOption
|
||||
func (_e *Store_Expecter) Delete(key interface{}, opts ...interface{}) *Store_Delete_Call {
|
||||
return &Store_Delete_Call{Call: _e.mock.On("Delete",
|
||||
append([]interface{}{key}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *Store_Delete_Call) Run(run func(key string, opts ...store.DeleteOption)) *Store_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(string)
|
||||
}
|
||||
var arg1 []store.DeleteOption
|
||||
var variadicArgs []store.DeleteOption
|
||||
if len(args) > 1 {
|
||||
variadicArgs = args[1].([]store.DeleteOption)
|
||||
}
|
||||
arg1 = variadicArgs
|
||||
run(
|
||||
arg0,
|
||||
arg1...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Delete_Call) Return(err error) *Store_Delete_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Delete_Call) RunAndReturn(run func(key string, opts ...store.DeleteOption) error) *Store_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Init provides a mock function for the type Store
|
||||
func (_mock *Store) Init(options ...store.Option) error {
|
||||
var tmpRet mock.Arguments
|
||||
if len(options) > 0 {
|
||||
tmpRet = _mock.Called(options)
|
||||
} else {
|
||||
tmpRet = _mock.Called()
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Init")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(...store.Option) error); ok {
|
||||
r0 = returnFunc(options...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
|
||||
type Store_Init_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Init is a helper method to define mock.On call
|
||||
// - options ...store.Option
|
||||
func (_e *Store_Expecter) Init(options ...interface{}) *Store_Init_Call {
|
||||
return &Store_Init_Call{Call: _e.mock.On("Init",
|
||||
append([]interface{}{}, options...)...)}
|
||||
}
|
||||
|
||||
func (_c *Store_Init_Call) Run(run func(options ...store.Option)) *Store_Init_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 []store.Option
|
||||
var variadicArgs []store.Option
|
||||
if len(args) > 0 {
|
||||
variadicArgs = args[0].([]store.Option)
|
||||
}
|
||||
arg0 = variadicArgs
|
||||
run(
|
||||
arg0...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Init_Call) Return(err error) *Store_Init_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Init_Call) RunAndReturn(run func(options ...store.Option) error) *Store_Init_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function for the type Store
|
||||
func (_mock *Store) List(opts ...store.ListOption) ([]string, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called()
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(...store.ListOption) ([]string, error)); ok {
|
||||
return returnFunc(opts...)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(...store.ListOption) []string); ok {
|
||||
r0 = returnFunc(opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(...store.ListOption) error); ok {
|
||||
r1 = returnFunc(opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
|
||||
type Store_List_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// List is a helper method to define mock.On call
|
||||
// - opts ...store.ListOption
|
||||
func (_e *Store_Expecter) List(opts ...interface{}) *Store_List_Call {
|
||||
return &Store_List_Call{Call: _e.mock.On("List",
|
||||
append([]interface{}{}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *Store_List_Call) Run(run func(opts ...store.ListOption)) *Store_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 []store.ListOption
|
||||
var variadicArgs []store.ListOption
|
||||
if len(args) > 0 {
|
||||
variadicArgs = args[0].([]store.ListOption)
|
||||
}
|
||||
arg0 = variadicArgs
|
||||
run(
|
||||
arg0...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_List_Call) Return(strings []string, err error) *Store_List_Call {
|
||||
_c.Call.Return(strings, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_List_Call) RunAndReturn(run func(opts ...store.ListOption) ([]string, error)) *Store_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Options provides a mock function for the type Store
|
||||
func (_mock *Store) Options() store.Options {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Options")
|
||||
}
|
||||
|
||||
var r0 store.Options
|
||||
if returnFunc, ok := ret.Get(0).(func() store.Options); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Get(0).(store.Options)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_Options_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Options'
|
||||
type Store_Options_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Options is a helper method to define mock.On call
|
||||
func (_e *Store_Expecter) Options() *Store_Options_Call {
|
||||
return &Store_Options_Call{Call: _e.mock.On("Options")}
|
||||
}
|
||||
|
||||
func (_c *Store_Options_Call) Run(run func()) *Store_Options_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Options_Call) Return(options store.Options) *Store_Options_Call {
|
||||
_c.Call.Return(options)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Options_Call) RunAndReturn(run func() store.Options) *Store_Options_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Read provides a mock function for the type Store
|
||||
func (_mock *Store) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(key, opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called(key)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Read")
|
||||
}
|
||||
|
||||
var r0 []*store.Record
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string, ...store.ReadOption) ([]*store.Record, error)); ok {
|
||||
return returnFunc(key, opts...)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(string, ...store.ReadOption) []*store.Record); ok {
|
||||
r0 = returnFunc(key, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*store.Record)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(string, ...store.ReadOption) error); ok {
|
||||
r1 = returnFunc(key, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
|
||||
type Store_Read_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Read is a helper method to define mock.On call
|
||||
// - key string
|
||||
// - opts ...store.ReadOption
|
||||
func (_e *Store_Expecter) Read(key interface{}, opts ...interface{}) *Store_Read_Call {
|
||||
return &Store_Read_Call{Call: _e.mock.On("Read",
|
||||
append([]interface{}{key}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *Store_Read_Call) Run(run func(key string, opts ...store.ReadOption)) *Store_Read_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(string)
|
||||
}
|
||||
var arg1 []store.ReadOption
|
||||
var variadicArgs []store.ReadOption
|
||||
if len(args) > 1 {
|
||||
variadicArgs = args[1].([]store.ReadOption)
|
||||
}
|
||||
arg1 = variadicArgs
|
||||
run(
|
||||
arg0,
|
||||
arg1...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Read_Call) Return(records []*store.Record, err error) *Store_Read_Call {
|
||||
_c.Call.Return(records, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Read_Call) RunAndReturn(run func(key string, opts ...store.ReadOption) ([]*store.Record, error)) *Store_Read_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// String provides a mock function for the type Store
|
||||
func (_mock *Store) String() string {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for String")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if returnFunc, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_String_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'String'
|
||||
type Store_String_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// String is a helper method to define mock.On call
|
||||
func (_e *Store_Expecter) String() *Store_String_Call {
|
||||
return &Store_String_Call{Call: _e.mock.On("String")}
|
||||
}
|
||||
|
||||
func (_c *Store_String_Call) Run(run func()) *Store_String_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_String_Call) Return(s string) *Store_String_Call {
|
||||
_c.Call.Return(s)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_String_Call) RunAndReturn(run func() string) *Store_String_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Write provides a mock function for the type Store
|
||||
func (_mock *Store) Write(r *store.Record, opts ...store.WriteOption) error {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(r, opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called(r)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Write")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*store.Record, ...store.WriteOption) error); ok {
|
||||
r0 = returnFunc(r, opts...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
|
||||
type Store_Write_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Write is a helper method to define mock.On call
|
||||
// - r *store.Record
|
||||
// - opts ...store.WriteOption
|
||||
func (_e *Store_Expecter) Write(r interface{}, opts ...interface{}) *Store_Write_Call {
|
||||
return &Store_Write_Call{Call: _e.mock.On("Write",
|
||||
append([]interface{}{r}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *Store_Write_Call) Run(run func(r *store.Record, opts ...store.WriteOption)) *Store_Write_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *store.Record
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(*store.Record)
|
||||
}
|
||||
var arg1 []store.WriteOption
|
||||
var variadicArgs []store.WriteOption
|
||||
if len(args) > 1 {
|
||||
variadicArgs = args[1].([]store.WriteOption)
|
||||
}
|
||||
arg1 = variadicArgs
|
||||
run(
|
||||
arg0,
|
||||
arg1...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Write_Call) Return(err error) *Store_Write_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_Write_Call) RunAndReturn(run func(r *store.Record, opts ...store.WriteOption) error) *Store_Write_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -71,6 +71,11 @@ func (b *Batch) Move(id, parentID, location string) error {
|
||||
if err := b.batch.Index(resource.ID, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
if b.batch.Size() >= b.size {
|
||||
if err := b.Push(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -88,6 +93,11 @@ func (b *Batch) Delete(id string) error {
|
||||
if err := b.batch.Index(resource.ID, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
if b.batch.Size() >= b.size {
|
||||
if err := b.Push(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -105,6 +115,11 @@ func (b *Batch) Restore(id string) error {
|
||||
if err := b.batch.Index(resource.ID, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
if b.batch.Size() >= b.size {
|
||||
if err := b.Push(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -142,6 +157,11 @@ func (b *Batch) Purge(id string, onlyDeleted bool) error {
|
||||
|
||||
for _, resource := range affectResources {
|
||||
b.batch.Delete(resource.ID)
|
||||
if b.batch.Size() >= b.size {
|
||||
if err := b.Push(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -64,7 +64,7 @@ func DefaultConfig() *config.Config {
|
||||
AckWait: 1 * time.Minute,
|
||||
},
|
||||
ContentExtractionSizeLimit: 20 * 1024 * 1024, // Limit content extraction to <20MB files by default
|
||||
BatchSize: 500,
|
||||
BatchSize: 50,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s Service) Run() error {
|
||||
}
|
||||
|
||||
if s.m != nil {
|
||||
monitorMetrics(s.stream, "search-pull", s.m, s.log)
|
||||
monitorMetrics(s.ctx, s.stream, "search-pull", s.m, s.log)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -202,25 +202,30 @@ func (s Service) processEvent(e raw.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func monitorMetrics(stream raw.Stream, name string, m *metrics.Metrics, logger log.Logger) {
|
||||
ctx := context.Background()
|
||||
func monitorMetrics(ctx context.Context, stream raw.Stream, name string, m *metrics.Metrics, logger log.Logger) {
|
||||
consumer, err := stream.JetStream().Consumer(ctx, name)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
}
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
info, err := consumer.Info(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
continue
|
||||
}
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
info, err := consumer.Info(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to get consumer")
|
||||
continue
|
||||
}
|
||||
|
||||
m.EventsOutstandingAcks.Set(float64(info.NumAckPending))
|
||||
m.EventsUnprocessed.Set(float64(info.NumPending))
|
||||
m.EventsRedelivered.Set(float64(info.NumRedelivered))
|
||||
logger.Trace().Msg("updated search event metrics")
|
||||
m.EventsOutstandingAcks.Set(float64(info.NumAckPending))
|
||||
m.EventsUnprocessed.Set(float64(info.NumPending))
|
||||
m.EventsRedelivered.Set(float64(info.NumRedelivered))
|
||||
logger.Trace().Msg("updated search event metrics")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
package thumbnail
|
||||
|
||||
import "github.com/davidbyttow/govips/v2/vips"
|
||||
|
||||
func init() {
|
||||
// temporary remove TIFF and JP2K from go-vips' list of supported
|
||||
// imagetypes
|
||||
delete(vips.ImageTypes, vips.ImageTypeTIFF)
|
||||
delete(vips.ImageTypes, vips.ImageTypeJP2K)
|
||||
}
|
||||
|
||||
var (
|
||||
// SupportedMimeTypes contains an all mimetypes which are supported by the thumbnailer.
|
||||
SupportedMimeTypes = map[string]struct{}{
|
||||
@@ -11,7 +20,6 @@ var (
|
||||
"image/gif": {},
|
||||
"image/bmp": {},
|
||||
"image/x-ms-bmp": {},
|
||||
"image/tiff": {},
|
||||
"text/plain": {},
|
||||
"audio/flac": {},
|
||||
"audio/mpeg": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
SHELL := bash
|
||||
NAME := web
|
||||
WEB_ASSETS_VERSION = v4.2.1
|
||||
WEB_ASSETS_VERSION = v4.2.2
|
||||
WEB_ASSETS_BRANCH = main
|
||||
|
||||
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
|
||||
@@ -33,6 +33,7 @@ use SimpleXMLElement;
|
||||
use Sabre\Xml\LibXMLException;
|
||||
use Sabre\Xml\Reader;
|
||||
use GuzzleHttp\Pool;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Helper for HTTP requests
|
||||
@@ -74,7 +75,6 @@ class HttpRequestHelper {
|
||||
* than download it all up-front.
|
||||
* @param int|null $timeout
|
||||
* @param Client|null $client
|
||||
* @param string|null $bearerToken
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws GuzzleException
|
||||
@@ -92,8 +92,42 @@ class HttpRequestHelper {
|
||||
bool $stream = false,
|
||||
?int $timeout = 0,
|
||||
?Client $client = null,
|
||||
?string $bearerToken = null
|
||||
): ResponseInterface {
|
||||
$bearerToken = null;
|
||||
if (TokenHelper::useBearerToken() && $user && $user !== 'public') {
|
||||
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
|
||||
// check token is still valid
|
||||
$parsedUrl = parse_url($url);
|
||||
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
|
||||
$baseUrl .= isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
|
||||
$testUrl = $baseUrl . "/graph/v1.0/use/$user";
|
||||
if (OcHelper::isTestingOnReva()) {
|
||||
$url = $baseUrl . "/ocs/v2.php/cloud/users/$user";
|
||||
}
|
||||
// check token validity with a GET request
|
||||
$c = self::createClient(
|
||||
$user,
|
||||
$password,
|
||||
$config,
|
||||
$cookies,
|
||||
$stream,
|
||||
$timeout,
|
||||
$bearerToken
|
||||
);
|
||||
$testReq = self::createRequest($testUrl, $xRequestId, 'GET');
|
||||
try {
|
||||
$testRes = $c->send($testReq);
|
||||
} catch (RequestException $ex) {
|
||||
$testRes = $ex->getResponse();
|
||||
if ($testRes && $testRes->getStatusCode() === Response::HTTP_UNAUTHORIZED) {
|
||||
// token is invalid or expired, get a new one
|
||||
echo "[INFO] Bearer token expired or invalid, getting a new one...\n";
|
||||
TokenHelper::clearAllTokens();
|
||||
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($client === null) {
|
||||
$client = self::createClient(
|
||||
$user,
|
||||
@@ -160,6 +194,24 @@ class HttpRequestHelper {
|
||||
}
|
||||
|
||||
HttpLogger::logResponse($response);
|
||||
|
||||
// wait for post-processing to finish if applicable
|
||||
if (WebdavHelper::isDAVRequest($url)
|
||||
&& \str_starts_with($url, OcHelper::getServerUrl())
|
||||
&& \in_array($method, ["PUT", "MOVE", "COPY"])
|
||||
&& \in_array($response->getStatusCode(), [Response::HTTP_CREATED, Response::HTTP_NO_CONTENT])
|
||||
&& OcConfigHelper::getPostProcessingDelay() === 0
|
||||
) {
|
||||
if (\in_array($method, ["MOVE", "COPY"])) {
|
||||
$url = $headers['Destination'];
|
||||
}
|
||||
WebDavHelper::waitForPostProcessingToFinish(
|
||||
$url,
|
||||
$user,
|
||||
$password,
|
||||
$headers,
|
||||
);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -203,13 +255,6 @@ class HttpRequestHelper {
|
||||
} else {
|
||||
$debugResponses = false;
|
||||
}
|
||||
// use basic auth for 'public' user or no user
|
||||
if ($user === 'public' || $user === null || $user === '') {
|
||||
$bearerToken = null;
|
||||
} else {
|
||||
$useBearerToken = TokenHelper::useBearerToken();
|
||||
$bearerToken = $useBearerToken ? TokenHelper::getTokens($user, $password, $url)['access_token'] : null;
|
||||
}
|
||||
|
||||
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
|
||||
$sendCount = 0;
|
||||
@@ -228,7 +273,6 @@ class HttpRequestHelper {
|
||||
$stream,
|
||||
$timeout,
|
||||
$client,
|
||||
$bearerToken,
|
||||
);
|
||||
|
||||
if ($response->getStatusCode() >= 400
|
||||
@@ -256,7 +300,8 @@ class HttpRequestHelper {
|
||||
// we need to repeat the send request, because we got HTTP_TOO_EARLY or HTTP_CONFLICT
|
||||
// wait 1 second before sending again, to give the server some time
|
||||
// to finish whatever post-processing it might be doing.
|
||||
self::debugResponse($response);
|
||||
echo "[INFO] Received '" . $response->getStatusCode() .
|
||||
"' status code, retrying request ($sendCount)...\n";
|
||||
\sleep(1);
|
||||
}
|
||||
} while ($loopAgain);
|
||||
|
||||
@@ -30,6 +30,26 @@ use Psr\Http\Message\ResponseInterface;
|
||||
* A helper class for configuring OpenCloud server
|
||||
*/
|
||||
class OcConfigHelper {
|
||||
public static $postProcessingDelay = 0;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public static function getPostProcessingDelay(): int {
|
||||
return self::$postProcessingDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $postProcessingDelay
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setPostProcessingDelay(string $postProcessingDelay): void {
|
||||
// extract number from string
|
||||
$delay = (int) filter_var($postProcessingDelay, FILTER_SANITIZE_NUMBER_INT);
|
||||
self::$postProcessingDelay = $delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
|
||||
@@ -84,7 +84,9 @@ class TokenHelper {
|
||||
$tokenData = [
|
||||
'access_token' => $refreshedToken['access_token'],
|
||||
'refresh_token' => $refreshedToken['refresh_token'],
|
||||
'expires_at' => time() + 300 // 5 minutes
|
||||
// set expiry to 240 (4 minutes) seconds to allow for some buffer
|
||||
// token actually expires in 300 seconds (5 minutes)
|
||||
'expires_at' => time() + 240
|
||||
];
|
||||
self::$tokenCache[$cacheKey] = $tokenData;
|
||||
return $tokenData;
|
||||
@@ -100,7 +102,9 @@ class TokenHelper {
|
||||
$tokenData = [
|
||||
'access_token' => $tokens['access_token'],
|
||||
'refresh_token' => $tokens['refresh_token'],
|
||||
'expires_at' => time() + 290 // set expiry to 290 seconds to allow for some buffer
|
||||
// set expiry to 240 (4 minutes) seconds to allow for some buffer
|
||||
// token actually expires in 300 seconds (5 minutes)
|
||||
'expires_at' => time() + 240
|
||||
];
|
||||
|
||||
// Save to cache
|
||||
|
||||
@@ -923,4 +923,45 @@ class WebDavHelper {
|
||||
$mtime = new DateTime($xmlPart[0]->__toString());
|
||||
return $mtime->format('U');
|
||||
}
|
||||
|
||||
/**
|
||||
* wait until the reqeust doesn't return 425 anymore
|
||||
*
|
||||
* @param string $url
|
||||
* @param ?string $user
|
||||
* @param ?string $password
|
||||
* @param ?array $headers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function waitForPostProcessingToFinish(
|
||||
string $url,
|
||||
?string $user = null,
|
||||
?string $password = null,
|
||||
?array $headers = [],
|
||||
): void {
|
||||
$retried = 0;
|
||||
do {
|
||||
$response = HttpRequestHelper::sendRequest(
|
||||
$url,
|
||||
'check-425-status',
|
||||
'GET',
|
||||
$user,
|
||||
$password,
|
||||
$headers,
|
||||
);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($statusCode !== 425) {
|
||||
return;
|
||||
}
|
||||
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
|
||||
if ($tryAgain) {
|
||||
$retried += 1;
|
||||
echo "[INFO] Waiting for post processing to finish, attempt ($retried)...\n";
|
||||
// wait 1s and try again
|
||||
\sleep(1);
|
||||
}
|
||||
} while ($tryAgain);
|
||||
echo "[ERROR] 10 seconds timeout! Post processing did not finish in time.\n";
|
||||
}
|
||||
}
|
||||
@@ -2026,8 +2026,12 @@ class FeatureContext extends BehatVariablesContext {
|
||||
if ($response === null) {
|
||||
$response = $this->getResponse();
|
||||
}
|
||||
$body = (string)$response->getBody();
|
||||
if (!$body) {
|
||||
return [];
|
||||
}
|
||||
return \json_decode(
|
||||
(string)$response->getBody(),
|
||||
$body,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ class NotificationContext implements Context {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^user "([^"]*)" should (?:get|have) a notification with subject "([^"]*)" and message:$/
|
||||
* @Then /^user "([^"]*)" should get a notification with subject "([^"]*)" and message:$/
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $subject
|
||||
@@ -411,10 +411,10 @@ class NotificationContext implements Context {
|
||||
throw new \Exception("Notification was not found even after retrying for 5 seconds.");
|
||||
}
|
||||
$expectedMessage = $table->getColumnsHash()[0]['message'];
|
||||
Assert::assertSame(
|
||||
Assert::assertStringStartsWith(
|
||||
$expectedMessage,
|
||||
$actualMessage,
|
||||
__METHOD__ . "expected message to be '$expectedMessage' but found'$actualMessage'"
|
||||
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,10 +441,10 @@ class NotificationContext implements Context {
|
||||
if (\count($notification) === 1) {
|
||||
$actualMessage = str_replace(["\r", "\r"], " ", $notification[0]->message);
|
||||
$expectedMessage = $table->getColumnsHash()[0]['message'];
|
||||
Assert::assertSame(
|
||||
Assert::assertStringStartsWith(
|
||||
$expectedMessage,
|
||||
$actualMessage,
|
||||
__METHOD__ . "expected message to be '$expectedMessage' but found'$actualMessage'"
|
||||
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
|
||||
);
|
||||
$response = $this->userDeletesNotification($user);
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
|
||||
@@ -462,7 +462,7 @@ class NotificationContext implements Context {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then user :user should not have a notification related to resource :resource with subject :subject
|
||||
* @Then user :user should not get a notification related to resource :resource with subject :subject
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $resource
|
||||
|
||||
@@ -68,6 +68,7 @@ class OcConfigContext implements Context {
|
||||
$response->getStatusCode(),
|
||||
"Failed to set async upload with delayed post processing"
|
||||
);
|
||||
OcConfigHelper::setPostProcessingDelay($delayTime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +91,9 @@ class OcConfigContext implements Context {
|
||||
$response->getStatusCode(),
|
||||
"Failed to set config $configVariable=$configValue"
|
||||
);
|
||||
if ($configVariable === "POSTPROCESSING_DELAY") {
|
||||
OcConfigHelper::setPostProcessingDelay($configValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,6 +188,9 @@ class OcConfigContext implements Context {
|
||||
$envs = [];
|
||||
foreach ($table->getHash() as $row) {
|
||||
$envs[$row['config']] = $row['value'];
|
||||
if ($row['config'] === "POSTPROCESSING_DELAY") {
|
||||
OcConfigHelper::setPostProcessingDelay($row['value']);
|
||||
}
|
||||
}
|
||||
|
||||
$response = OcConfigHelper::reConfigureOc($envs);
|
||||
@@ -200,6 +207,7 @@ class OcConfigContext implements Context {
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackOc(): void {
|
||||
OcConfigHelper::setPostProcessingDelay('0');
|
||||
$response = OcConfigHelper::rollbackOc();
|
||||
Assert::assertEquals(
|
||||
200,
|
||||
|
||||
@@ -607,7 +607,7 @@ trait Provisioning {
|
||||
Assert::assertEquals(
|
||||
201,
|
||||
$response->getStatusCode(),
|
||||
__METHOD__ . " cannot create user '$userName' using Graph API.\nResponse:" .
|
||||
__METHOD__ . " cannot create user '$userName'.\nResponse:" .
|
||||
json_encode($this->getJsonDecodedResponse($response))
|
||||
);
|
||||
|
||||
@@ -1083,7 +1083,7 @@ trait Provisioning {
|
||||
Assert::assertEquals(
|
||||
201,
|
||||
$response->getStatusCode(),
|
||||
__METHOD__ . " cannot create user '$user' using Graph API.\nResponse:" .
|
||||
__METHOD__ . " cannot create user '$user'.\nResponse:" .
|
||||
json_encode($this->getJsonDecodedResponse($response))
|
||||
);
|
||||
$userId = $this->getJsonDecodedResponse($response)['id'];
|
||||
|
||||
@@ -750,6 +750,9 @@ class SpacesContext implements Context {
|
||||
} else {
|
||||
$rawBody = $this->featureContext->getResponse()->getBody()->getContents();
|
||||
}
|
||||
if (!$rawBody) {
|
||||
throw new Exception(__METHOD__ . " - Response body is empty");
|
||||
}
|
||||
$drives = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (isset($drives["value"])) {
|
||||
$drives = $drives["value"];
|
||||
|
||||
@@ -216,6 +216,44 @@ class TUSContext implements Context {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with retry on offset mismatch using the WebDAV API
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $offset
|
||||
* @param string $data
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws GuzzleException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function userSendsAChunkToTUSLocationWithOffsetAndDataWithRetryOnOffsetMismatch(
|
||||
string $user,
|
||||
string $offset,
|
||||
string $data,
|
||||
): void {
|
||||
$resourceLocation = $this->getLastTusResourceLocation();
|
||||
|
||||
$retried = 0;
|
||||
do {
|
||||
$tryAgain = false;
|
||||
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
|
||||
// retry on 409 Conflict (Offset mismatch during TUS upload)
|
||||
if ($response->getStatusCode() === 409) {
|
||||
$tryAgain = true;
|
||||
}
|
||||
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
|
||||
if ($tryAgain) {
|
||||
$retried += 1;
|
||||
echo "Offset mismatch during TUS upload, retrying ($retried)...\n";
|
||||
// wait 1s and try again
|
||||
\sleep(1);
|
||||
}
|
||||
} while ($tryAgain);
|
||||
$this->featureContext->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data using the WebDAV API
|
||||
*
|
||||
|
||||
@@ -25,6 +25,7 @@ use GuzzleHttp\Exception\GuzzleException;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Stream\StreamInterface;
|
||||
use TestHelpers\OcConfigHelper;
|
||||
use TestHelpers\OcHelper;
|
||||
use TestHelpers\UploadHelper;
|
||||
use TestHelpers\WebDavHelper;
|
||||
@@ -743,6 +744,7 @@ trait WebDav {
|
||||
|
||||
/**
|
||||
* @When the user waits for :time seconds for postprocessing to finish
|
||||
* @When the user waits for :time seconds
|
||||
*
|
||||
* @param int $time
|
||||
*
|
||||
@@ -973,6 +975,61 @@ trait WebDav {
|
||||
$this->checkDownloadedContentMatches($content, '', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* check file content with retry
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $fileName
|
||||
* @param string $content
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function checkFileContentWithRetry(string $user, string $fileName, string $content): void {
|
||||
$retried = 0;
|
||||
do {
|
||||
$tryAgain = false;
|
||||
$response = $this->downloadFileAsUserUsingPassword($this->getActualUsername($user), $fileName);
|
||||
$status = $response->getStatusCode();
|
||||
$downloadedContent = $response->getBody()->getContents();
|
||||
if ($status !== 200) {
|
||||
$tryAgain = true;
|
||||
$message = "Expected '200' but got '$status'";
|
||||
} elseif ($downloadedContent !== $content) {
|
||||
$tryAgain = true;
|
||||
$message = "Expected content '$content' but got '$downloadedContent'";
|
||||
}
|
||||
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
|
||||
if ($tryAgain) {
|
||||
$retried += 1;
|
||||
echo "[INFO] File content mismatch. $message, checking content again ($retried)...\n";
|
||||
|
||||
// break the loop if status is 425 as the request will already be retried
|
||||
if ($status === HttpRequestHelper::HTTP_TOO_EARLY) {
|
||||
break;
|
||||
}
|
||||
|
||||
// wait 1s and try again
|
||||
\sleep(1);
|
||||
}
|
||||
} while ($tryAgain);
|
||||
$this->theHTTPStatusCodeShouldBe(200, '', $response);
|
||||
$this->checkDownloadedContentMatches($content, '', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then as :user the final content of file :fileName should be :content
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $fileName
|
||||
* @param string $content
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function asUserFinalContentOfFileShouldBe(string $user, string $fileName, string $content): void {
|
||||
$this->checkFileContentWithRetry($user, $fileName, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the content of the following files for user "([^"]*)" should be "([^"]*)"$/
|
||||
*
|
||||
@@ -1264,20 +1321,18 @@ trait WebDav {
|
||||
$type
|
||||
);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($statusCode < 400 || $statusCode > 499) {
|
||||
try {
|
||||
$responseXmlObject = HttpRequestHelper::getResponseXml(
|
||||
$response,
|
||||
__METHOD__
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
Assert::fail(
|
||||
"$entry '$path' should not exist. But API returned $statusCode without XML in the body"
|
||||
);
|
||||
}
|
||||
// when checking path with '..' it may return 405 Method Not Allowed
|
||||
if ($statusCode === 404 || $statusCode === 405) {
|
||||
return;
|
||||
}
|
||||
if ($statusCode === 207) {
|
||||
$responseXmlObject = HttpRequestHelper::getResponseXml(
|
||||
$response,
|
||||
__METHOD__
|
||||
);
|
||||
Assert::assertTrue(
|
||||
$this->isEtagValid($this->getEtagFromResponseXmlObject($responseXmlObject)),
|
||||
"$entry '$path' should not exist. But API returned $statusCode without an etag in the body"
|
||||
"$entry '$path' should not exist but found with invalid etag."
|
||||
);
|
||||
$isCollection = $responseXmlObject->xpath("//d:prop/d:resourcetype/d:collection");
|
||||
if (\count($isCollection) === 0) {
|
||||
@@ -1291,7 +1346,11 @@ trait WebDav {
|
||||
"$entry '$path' should not exist. But it does."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Assert::fail(
|
||||
"$entry '$path' should not exist. But API returned $statusCode without XML in the body"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2270,6 +2329,11 @@ trait WebDav {
|
||||
"HTTP status code was not 201 or 204 while trying to upload file '$destination' for user '$user'",
|
||||
$response
|
||||
);
|
||||
|
||||
// check uploaded content only if post-processing delay is not configured
|
||||
if (OcConfigHelper::getPostProcessingDelay() === 0) {
|
||||
$this->checkFileContentWithRetry($user, $destination, $content);
|
||||
}
|
||||
return $response->getHeader('oc-fileid');
|
||||
}
|
||||
|
||||
|
||||
@@ -36,13 +36,13 @@ Feature: antivirus
|
||||
| <message> |
|
||||
And as "Alice" file "<new-file-name>" should not exist
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus and a file without virus
|
||||
@@ -64,13 +64,13 @@ Feature: antivirus
|
||||
Cheers.
|
||||
"""
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus in chunks
|
||||
@@ -83,8 +83,8 @@ Feature: antivirus
|
||||
# antivirus service can scan files during post-processing. on demand scanning is currently not available
|
||||
Then the HTTP status code should be "201"
|
||||
And user "Alice" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in myChunkedFile.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in myChunkedFile.txt. Upload not possible. Virus: |
|
||||
And as "Alice" file "/myChunkedFile.txt" should not exist
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -110,13 +110,13 @@ Feature: antivirus
|
||||
| <message> |
|
||||
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
@issue-10331
|
||||
Scenario Outline: public uploads a file with the virus to a password-protected public share
|
||||
@@ -136,13 +136,13 @@ Feature: antivirus
|
||||
| <message> |
|
||||
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus to a user share
|
||||
@@ -164,13 +164,13 @@ Feature: antivirus
|
||||
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
|
||||
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus to a group share
|
||||
@@ -194,13 +194,13 @@ Feature: antivirus
|
||||
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
|
||||
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
|
||||
Examples:
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| dav-path-version | file-name | new-file-name | message |
|
||||
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus to a project space
|
||||
@@ -223,9 +223,9 @@ Feature: antivirus
|
||||
And for user "Alice" the space "new-space" should not contain these entries:
|
||||
| /<new-file-name> |
|
||||
Examples:
|
||||
| file-name | new-file-name | message |
|
||||
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| file-name | new-file-name | message |
|
||||
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
|
||||
Scenario Outline: upload a file with virus to a shared project space
|
||||
@@ -248,9 +248,9 @@ Feature: antivirus
|
||||
And for user "Alice" the space "new-space" should not contain these entries:
|
||||
| /<new-file-name> |
|
||||
Examples:
|
||||
| file-name | new-file-name | message |
|
||||
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| file-name | new-file-name | message |
|
||||
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
|
||||
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
|
||||
|
||||
@env-config @issue-6494
|
||||
Scenario Outline: upload a file with virus by setting antivirus infected file handling config to continue
|
||||
@@ -272,8 +272,8 @@ Feature: antivirus
|
||||
When user "Alice" uploads file "filesForUpload/filesWithVirus/eicar.com" to "/aFileWithVirus.txt" using the WebDAV API
|
||||
Then the HTTP status code should be "201"
|
||||
And user "Alice" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in aFileWithVirus.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in aFileWithVirus.txt. Upload not possible. Virus: |
|
||||
And as "Alice" file "/aFileWithVirus.txt" should not exist
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -306,12 +306,12 @@ Feature: antivirus
|
||||
And as "Alice" file "/aFileWithVirus.txt" should not exist
|
||||
Examples:
|
||||
| dav-path-version | language | subject | message |
|
||||
| old | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| new | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| old | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: Win.Test.EICAR_HDB-1 |
|
||||
| new | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: Win.Test.EICAR_HDB-1 |
|
||||
| spaces | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: Win.Test.EICAR_HDB-1 |
|
||||
| old | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
|
||||
| new | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
|
||||
| spaces | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
|
||||
| old | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
|
||||
| new | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
|
||||
| spaces | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
|
||||
|
||||
@issue-enterprise-5709
|
||||
Scenario Outline: try to create a version of file by uploading virus content
|
||||
@@ -321,8 +321,8 @@ Feature: antivirus
|
||||
When user "Alice" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "test.txt" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Alice" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: |
|
||||
And as "Alice" file "/test.txt" should exist
|
||||
And the version folder of file "/test.txt" for user "Alice" should contain "1" element
|
||||
And the content of file "/test.txt" for user "Alice" should be "hello nepal"
|
||||
@@ -348,8 +348,8 @@ Feature: antivirus
|
||||
When the public overwrites file "test.txt" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" using the public WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Alice" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: |
|
||||
And the content of file "/test.txt" for user "Alice" should be "hello"
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -375,8 +375,8 @@ Feature: antivirus
|
||||
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/test.txt" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Brian" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: |
|
||||
And the content of file "/test.txt" for user "Alice" should be "hello"
|
||||
And the content of file "Shares/test.txt" for user "Brian" should be "hello"
|
||||
Examples:
|
||||
@@ -409,15 +409,15 @@ Feature: antivirus
|
||||
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/uploadFolder/test.txt" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Brian" should get a notification for resource "test.txt" with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: |
|
||||
And the content of file "Shares/uploadFolder/test.txt" for user "Brian" should be "this is a test file."
|
||||
And the content of file "uploadFolder/test.txt" for user "Alice" should be "this is a test file."
|
||||
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/test.txt" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Brian" should get a notification for resource "test.txt" with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in test.txt. Upload not possible. Virus: |
|
||||
And the content of file "Shares/test.txt" for user "Brian" should be "this is a test file."
|
||||
And the content of file "/test.txt" for user "Alice" should be "this is a test file."
|
||||
Examples:
|
||||
@@ -436,8 +436,8 @@ Feature: antivirus
|
||||
When user "Alice" uploads a file inside space "new-space" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to ".space/readme.md" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Alice" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in readme.md. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in readme.md. Upload not possible. Virus: |
|
||||
And for user "Alice" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
|
||||
|
||||
|
||||
@@ -457,8 +457,8 @@ Feature: antivirus
|
||||
When user "Brian" uploads a file inside space "new-space" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to ".space/readme.md" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Brian" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in readme.md. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in readme.md. Upload not possible. Virus: |
|
||||
And for user "Brian" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
|
||||
And for user "Alice" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
|
||||
|
||||
@@ -477,7 +477,7 @@ Feature: antivirus
|
||||
When user "Brian" uploads a file "filesForUpload/filesWithVirus/eicar.com" to "text.txt" in space "new-space" using the WebDAV API
|
||||
Then the HTTP status code should be "204"
|
||||
And user "Brian" should get a notification with subject "Virus found" and message:
|
||||
| message |
|
||||
| Virus found in text.txt. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
|
||||
| message |
|
||||
| Virus found in text.txt. Upload not possible. Virus: |
|
||||
And for user "Brian" the content of the file "/text.txt" of the space "new-space" should be "hello world"
|
||||
And for user "Alice" the content of the file "/text.txt" of the space "new-space" should be "hello world"
|
||||
@@ -27,10 +27,10 @@ Feature: Delete notification
|
||||
Scenario: delete a notification
|
||||
When user "Brian" deletes a notification related to resource "my_data" with subject "Resource shared"
|
||||
Then the HTTP status code should be "200"
|
||||
And user "Brian" should have a notification with subject "Resource shared" and message:
|
||||
And user "Brian" should get a notification with subject "Resource shared" and message:
|
||||
| message |
|
||||
| Alice Hansen shared textfile1.txt with you |
|
||||
But user "Brian" should not have a notification related to resource "my_data" with subject "Resource shared"
|
||||
But user "Brian" should not get a notification related to resource "my_data" with subject "Resource shared"
|
||||
|
||||
|
||||
Scenario: delete all notifications
|
||||
|
||||
@@ -29,7 +29,7 @@ Feature: create a resources using collaborative posixfs
|
||||
Scenario: create file
|
||||
When the administrator creates the file "test.txt" with content "content" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/test.txt" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "test.txt" should be "content"
|
||||
|
||||
|
||||
Scenario: create large file
|
||||
@@ -41,21 +41,22 @@ Feature: create a resources using collaborative posixfs
|
||||
Scenario: creates files sequentially in a folder
|
||||
When the administrator creates 50 files sequentially in the directory "firstFolder" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/firstFolder/file_1.txt" for user "Alice" should be "file 1 content"
|
||||
And the content of file "/firstFolder/file_50.txt" for user "Alice" should be "file 50 content"
|
||||
And as "Alice" the final content of file "/firstFolder/file_1.txt" should be "file 1 content"
|
||||
And as "Alice" the final content of file "/firstFolder/file_50.txt" should be "file 50 content"
|
||||
|
||||
|
||||
Scenario: creates files in parallel in a folder
|
||||
When the administrator creates 100 files in parallel in the directory "firstFolder" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/firstFolder/parallel_1.txt" for user "Alice" should be "parallel file 1 content"
|
||||
And the content of file "/firstFolder/parallel_100.txt" for user "Alice" should be "parallel file 100 content"
|
||||
And as "Alice" the final content of file "/firstFolder/parallel_1.txt" should be "parallel file 1 content"
|
||||
And as "Alice" the final content of file "/firstFolder/parallel_100.txt" should be "parallel file 100 content"
|
||||
|
||||
|
||||
Scenario: edit file
|
||||
Given user "Alice" has uploaded file with content "content" to "test.txt"
|
||||
When the administrator puts the content "new" into the file "test.txt" in the POSIX storage folder of user "Alice"
|
||||
Then the content of file "/test.txt" for user "Alice" should be "contentnew"
|
||||
Then the command should be successful
|
||||
And as "Alice" the final content of file "test.txt" should be "contentnew"
|
||||
|
||||
|
||||
Scenario: read file content
|
||||
@@ -68,28 +69,28 @@ Feature: create a resources using collaborative posixfs
|
||||
Given user "Alice" has uploaded file with content "content" to "test.txt"
|
||||
When the administrator copies the file "test.txt" to the folder "firstFolder" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/firstFolder/test.txt" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "/firstFolder/test.txt" should be "content"
|
||||
|
||||
|
||||
Scenario: rename file
|
||||
Given user "Alice" has uploaded file with content "content" to "test.txt"
|
||||
When the administrator renames the file "test.txt" to "new-name.txt" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/new-name.txt" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "/new-name.txt" should be "content"
|
||||
|
||||
|
||||
Scenario: rename a created file
|
||||
Given the administrator has created the file "test.txt" with content "content" for user "Alice" on the POSIX filesystem
|
||||
When the administrator renames the file "test.txt" to "test.md" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/test.md" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "/test.md" should be "content"
|
||||
|
||||
|
||||
Scenario: move file to folder
|
||||
Given user "Alice" has uploaded file with content "content" to "test.txt"
|
||||
When the administrator moves the file "test.txt" to the folder "firstFolder" for user "Alice" on the POSIX filesystem
|
||||
Then the command should be successful
|
||||
And the content of file "/firstFolder/test.txt" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "/firstFolder/test.txt" should be "content"
|
||||
And as "Alice" file "/test.txt" should not exist
|
||||
|
||||
|
||||
@@ -187,4 +188,4 @@ Feature: create a resources using collaborative posixfs
|
||||
And the administrator renames the file "test.txt" to "renamed.txt" for user "Alice" on the POSIX filesystem
|
||||
And the administrator checks the attribute "user.oc.name" of file "renamed.txt" for user "Alice" on the POSIX filesystem
|
||||
Then the command output should contain "renamed.txt"
|
||||
And the content of file "/renamed.txt" for user "Alice" should be "content"
|
||||
And as "Alice" the final content of file "/renamed.txt" should be "content"
|
||||
@@ -202,7 +202,7 @@ Feature: capabilities
|
||||
"properties": {
|
||||
"edition": {
|
||||
"type": "string",
|
||||
"enum": ["dev"]
|
||||
"enum": ["%edition%"]
|
||||
},
|
||||
"product": {
|
||||
"type": "string",
|
||||
@@ -240,7 +240,7 @@ Feature: capabilities
|
||||
},
|
||||
"edition": {
|
||||
"type": "string",
|
||||
"enum": ["dev"]
|
||||
"enum": ["%edition%"]
|
||||
},
|
||||
"product": {
|
||||
"type": "string",
|
||||
|
||||
@@ -58,7 +58,7 @@ Feature: default capabilities for normal user
|
||||
"const": "%versionstring%"
|
||||
},
|
||||
"edition": {
|
||||
"const": "dev"
|
||||
"const": "%edition%"
|
||||
},
|
||||
"productname": {
|
||||
"const": "%productname%"
|
||||
|
||||
@@ -50,8 +50,7 @@ Feature: low level tests for upload of chunks
|
||||
| Upload-Metadata | filename ZmlsZS50eHQ= |
|
||||
When user "Alice" sends a chunk to the last created TUS Location with offset "0" and data "123" using the WebDAV API
|
||||
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "4567890" using the WebDAV API
|
||||
And the user waits for "2" seconds for postprocessing to finish
|
||||
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "0000000" using the WebDAV API
|
||||
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "0000000" with retry on offset mismatch using the WebDAV API
|
||||
Then the HTTP status code should be "404"
|
||||
And the content of file "/file.txt" for user "Alice" should be "1234567890"
|
||||
Examples:
|
||||
@@ -61,6 +60,22 @@ Feature: low level tests for upload of chunks
|
||||
| spaces |
|
||||
|
||||
|
||||
Scenario Outline: send last chunk with mismatch offset
|
||||
Given using <dav-path-version> DAV path
|
||||
And user "Alice" has created a new TUS resource on the WebDAV API with these headers:
|
||||
| Upload-Length | 10 |
|
||||
# ZmlsZS50eHQ= is the base64 encode of file.txt
|
||||
| Upload-Metadata | filename ZmlsZS50eHQ= |
|
||||
When user "Alice" sends a chunk to the last created TUS Location with offset "0" and data "123" using the WebDAV API
|
||||
And user "Alice" sends a chunk to the last created TUS Location with offset "2" and data "34567890" using the WebDAV API
|
||||
Then the HTTP status code should be "409"
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
| old |
|
||||
| new |
|
||||
| spaces |
|
||||
|
||||
|
||||
Scenario Outline: start with uploading not at the beginning of the file
|
||||
Given using <dav-path-version> DAV path
|
||||
And user "Alice" has created a new TUS resource on the WebDAV API with these headers:
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
const fs = require("fs");
|
||||
|
||||
const CI_REPO_NAME = process.env.CI_REPO_NAME;
|
||||
const CI_COMMIT_SHA = process.env.CI_COMMIT_SHA;
|
||||
const CI_WORKFLOW_NAME = process.env.CI_WORKFLOW_NAME;
|
||||
const CI_PIPELINE_EVENT = process.env.CI_PIPELINE_EVENT;
|
||||
|
||||
const opencloudBuildWorkflow = "build-opencloud-for-testing";
|
||||
const webCacheWorkflows = ["cache-web", "cache-web-pnpm", "cache-browsers"];
|
||||
|
||||
const INFO_URL = `https://s3.ci.opencloud.eu/public/${CI_REPO_NAME}/pipelines/${CI_COMMIT_SHA}-${CI_PIPELINE_EVENT}/pipeline_info.json`;
|
||||
|
||||
function getWorkflowNames(workflows) {
|
||||
const allWorkflows = [];
|
||||
for (const workflow of workflows) {
|
||||
allWorkflows.push(workflow.name);
|
||||
}
|
||||
return allWorkflows;
|
||||
}
|
||||
|
||||
function getFailedWorkflows(workflows) {
|
||||
const failedWorkflows = [];
|
||||
for (const workflow of workflows) {
|
||||
if (workflow.state !== "success") {
|
||||
failedWorkflows.push(workflow.name);
|
||||
}
|
||||
}
|
||||
return failedWorkflows;
|
||||
}
|
||||
|
||||
function hasFailingTestWorkflow(failedWorkflows) {
|
||||
for (const workflowName of failedWorkflows) {
|
||||
if (workflowName.startsWith("test-")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasFailingE2eTestWorkflow(failedWorkflows) {
|
||||
for (const workflowName of failedWorkflows) {
|
||||
if (workflowName.startsWith("test-e2e-")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const infoResponse = await fetch(INFO_URL);
|
||||
if (infoResponse.status === 404) {
|
||||
console.log("[INFO] No matching previous pipeline found. Continue...");
|
||||
process.exit(0);
|
||||
} else if (!infoResponse.ok) {
|
||||
console.error(
|
||||
"[ERROR] Failed to fetch previous pipeline info:" +
|
||||
`\n URL: ${INFO_URL}\n Status: ${infoResponse.status}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const info = await infoResponse.json();
|
||||
console.log(info);
|
||||
|
||||
if (info.status === "success") {
|
||||
console.log(
|
||||
"[INFO] All workflows passed in previous pipeline. Full restart. Continue..."
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const allWorkflows = getWorkflowNames(info.workflows);
|
||||
const failedWorkflows = getFailedWorkflows(info.workflows);
|
||||
|
||||
// NOTE: implement for test pipelines only for now
|
||||
// // run the build workflow if any test workflow has failed
|
||||
// if (
|
||||
// CI_WORKFLOW_NAME === opencloudBuildWorkflow &&
|
||||
// hasFailingTestWorkflow(failedWorkflows)
|
||||
// ) {
|
||||
// process.exit(0);
|
||||
// }
|
||||
|
||||
// // run the web cache workflows if any e2e test workflow has failed
|
||||
// if (
|
||||
// webCacheWorkflows.includes(CI_WORKFLOW_NAME) &&
|
||||
// hasFailingE2eTestWorkflow(failedWorkflows)
|
||||
// ) {
|
||||
// process.exit(0);
|
||||
// }
|
||||
|
||||
if (!allWorkflows.includes(CI_WORKFLOW_NAME)) {
|
||||
process.exit(0);
|
||||
}
|
||||
if (!failedWorkflows.includes(CI_WORKFLOW_NAME)) {
|
||||
console.log("[INFO] Workflow passed in previous pipeline. Skip...");
|
||||
fs.appendFileSync(".woodpecker.env", "SKIP_WORKFLOW=true\n");
|
||||
process.exit(0);
|
||||
}
|
||||
console.log("[INFO] Restarting previously failed workflow. Continue...");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
CACHE_KEY="$PUBLIC_BUCKET/$CI_REPO_NAME/pipelines/$CI_COMMIT_SHA-$CI_PIPELINE_EVENT"
|
||||
|
||||
mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
|
||||
|
||||
# check previous pipeline
|
||||
URL="https://s3.ci.opencloud.eu/$CACHE_KEY/prev_pipeline"
|
||||
status=$(curl -s -o prev_pipeline "$URL" -w '%{http_code}')
|
||||
|
||||
if [ "$status" == "200" ];
|
||||
then
|
||||
source prev_pipeline
|
||||
REPO_ID=$(printf '%s' "$CI_PIPELINE_URL" | sed 's|.*/repos/\([0-9]*\)/.*|\1|')
|
||||
p_status=$(curl -s -o pipeline_info.json "$CI_SYSTEM_URL/api/repos/$REPO_ID/pipelines/$PREV_PIPELINE_NUMBER" -w "%{http_code}")
|
||||
if [ "$p_status" != "200" ];
|
||||
then
|
||||
echo -e "[ERROR] Failed to fetch previous pipeline info.\n URL: $CI_SYSTEM_URL/api/repos/$REPO_ID/pipelines/$PREV_PIPELINE_NUMBER\n Status: $p_status"
|
||||
exit 1
|
||||
fi
|
||||
# update previous pipeline info
|
||||
mc cp -a pipeline_info.json "s3/$CACHE_KEY/"
|
||||
fi
|
||||
|
||||
# upload current pipeline number for the next pipeline
|
||||
echo "PREV_PIPELINE_NUMBER=$CI_PIPELINE_NUMBER" > prev_pipeline
|
||||
mc cp -a prev_pipeline "s3/$CACHE_KEY/"
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.2"
|
||||
"php": "8.4"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true
|
||||
@@ -20,7 +20,7 @@
|
||||
"guzzlehttp/guzzle": "^7.7",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"laminas/laminas-ldap": "^2.15",
|
||||
"ankitpokhrel/tus-php": "^2.3",
|
||||
"ankitpokhrel/tus-php": "^2.4",
|
||||
"swaggest/json-schema": "^0.12.42",
|
||||
"splitbrain/php-archive": "^1.3"
|
||||
}
|
||||
|
||||
+65
-2
@@ -17,13 +17,18 @@
|
||||
// [test properties]: https://antithesis.com/docs/using_antithesis/properties/
|
||||
// [workload]: https://antithesis.com/docs/getting_started/first_test/
|
||||
// [antithesis-go-generator]: https://antithesis.com/docs/using_antithesis/sdk/go/instrumentor/
|
||||
// [triage report]: https://antithesis.com/docs/reports/triage/
|
||||
// [triage report]: https://antithesis.com/docs/reports/
|
||||
// [here]: https://antithesis.com/docs/using_antithesis/sdk/fallback/
|
||||
// [Sometimes assertions]: https://antithesis.com/docs/best_practices/sometimes_assertions/
|
||||
//
|
||||
// [details]: https://antithesis.com/docs/reports/triage/#details
|
||||
// [details]: https://antithesis.com/docs/reports/
|
||||
package assert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type assertInfo struct {
|
||||
Location *locationInfo `json:"location"`
|
||||
Details map[string]any `json:"details"`
|
||||
@@ -36,6 +41,64 @@ type assertInfo struct {
|
||||
Condition bool `json:"condition"`
|
||||
}
|
||||
|
||||
// Create a custom json marshaler for assertInfo so that we can force Errors to be marshaled with their error details.
|
||||
// Without this, custom errors are marshaled as an empty object because the default json marshaler doesn't include the error
|
||||
// (because it's a method - not an exported struct field).
|
||||
func (f assertInfo) MarshalJSON() ([]byte, error) {
|
||||
type alias assertInfo // prevent infinite recursion
|
||||
a := alias(f)
|
||||
if a.Details != nil {
|
||||
a.Details = normalizeMap(a.Details)
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type jsonError struct {
|
||||
innerError error
|
||||
}
|
||||
|
||||
func (e jsonError) MarshalJSON() ([]byte, error) {
|
||||
// Marshal this as the debug output string instead of e.Error(). These should be equivalent, but Sprintf correctly
|
||||
// handles nil values for us (which otherwise are annoying to defend against due to this - https://go.dev/doc/faq#nil_error)
|
||||
return json.Marshal(fmt.Sprintf("%+v", e.innerError))
|
||||
}
|
||||
|
||||
// Recursively replace any `error` with jsonError while doing a deep copy.
|
||||
// Most of the logic is in the normalize method below. This method exists to localize the type assertions
|
||||
// and provide a function that takes in/out a map instead of any.
|
||||
func normalizeMap(v map[string]any) map[string]any {
|
||||
return normalize(v).(map[string]any)
|
||||
}
|
||||
|
||||
func normalize(input any) any {
|
||||
// This switch will miss some cases (pointers, structs, non-any types), but should catch a very large proportion of real error interfaces
|
||||
// in real details objects. We can augment this if we find other cases common enough to support.
|
||||
switch inputTyped := input.(type) {
|
||||
case error:
|
||||
// Check if the underlying error implements json.Marshaler, so that if the error
|
||||
// already knows who to marshal itself, we don't override that.
|
||||
if _, ok := inputTyped.(json.Marshaler); ok {
|
||||
return inputTyped
|
||||
} else {
|
||||
return jsonError{inputTyped}
|
||||
}
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(inputTyped))
|
||||
for k, v := range inputTyped {
|
||||
out[k] = normalize(v)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(inputTyped))
|
||||
for i := range inputTyped {
|
||||
out[i] = normalize(inputTyped[i])
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
type wrappedAssertInfo struct {
|
||||
A *assertInfo `json:"antithesis_assert"`
|
||||
}
|
||||
|
||||
+4
-146
@@ -4,67 +4,11 @@ package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// To build and run an executable with this package
|
||||
//
|
||||
// CC=clang CGO_ENABLED=1 go run ./main.go
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
// \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
|
||||
//
|
||||
// The commented lines below, and the `import "C"` line which must directly follow
|
||||
// the commented lines are used by CGO. They are load-bearing, and should not be
|
||||
// changed without first understanding how CGO uses them.
|
||||
//
|
||||
// \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
|
||||
|
||||
// #cgo LDFLAGS: -ldl
|
||||
//
|
||||
// #include <dlfcn.h>
|
||||
// #include <stdbool.h>
|
||||
// #include <stdint.h>
|
||||
// #include <stdlib.h>
|
||||
//
|
||||
// typedef void (*go_fuzz_json_data_fn)(const char *data, size_t size);
|
||||
// void
|
||||
// go_fuzz_json_data(void *f, const char *data, size_t size) {
|
||||
// ((go_fuzz_json_data_fn)f)(data, size);
|
||||
// }
|
||||
//
|
||||
// typedef void (*go_fuzz_flush_fn)(void);
|
||||
// void
|
||||
// go_fuzz_flush(void *f) {
|
||||
// ((go_fuzz_flush_fn)f)();
|
||||
// }
|
||||
//
|
||||
// typedef uint64_t (*go_fuzz_get_random_fn)(void);
|
||||
// uint64_t
|
||||
// go_fuzz_get_random(void *f) {
|
||||
// return ((go_fuzz_get_random_fn)f)();
|
||||
// }
|
||||
//
|
||||
// typedef bool (*go_notify_coverage_fn)(size_t);
|
||||
// int
|
||||
// go_notify_coverage(void *f, size_t edges) {
|
||||
// bool b = ((go_notify_coverage_fn)f)(edges);
|
||||
// return b ? 1 : 0;
|
||||
// }
|
||||
//
|
||||
// typedef uint64_t (*go_init_coverage_fn)(size_t num_edges, const char *symbols);
|
||||
// uint64_t
|
||||
// go_init_coverage(void *f, size_t num_edges, const char *symbols) {
|
||||
// return ((go_init_coverage_fn)f)(num_edges, symbols);
|
||||
// }
|
||||
//
|
||||
import "C"
|
||||
|
||||
func Json_data(v any) error {
|
||||
if data, err := json.Marshal(v); err != nil {
|
||||
return err
|
||||
@@ -95,45 +39,10 @@ type libHandler interface {
|
||||
|
||||
const (
|
||||
errorLogLinePrefix = "[* antithesis-sdk-go *]"
|
||||
defaultNativeLibraryPath = "/usr/lib/libvoidstar.so"
|
||||
)
|
||||
|
||||
var handler libHandler
|
||||
|
||||
type voidstarHandler struct {
|
||||
fuzzJsonData unsafe.Pointer
|
||||
fuzzFlush unsafe.Pointer
|
||||
fuzzGetRandom unsafe.Pointer
|
||||
initCoverage unsafe.Pointer
|
||||
notifyCoverage unsafe.Pointer
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) output(message string) {
|
||||
msg_len := len(message)
|
||||
if msg_len == 0 {
|
||||
return
|
||||
}
|
||||
cstrMessage := C.CString(message)
|
||||
defer C.free(unsafe.Pointer(cstrMessage))
|
||||
C.go_fuzz_json_data(h.fuzzJsonData, cstrMessage, C.ulong(msg_len))
|
||||
C.go_fuzz_flush(h.fuzzFlush)
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) random() uint64 {
|
||||
return uint64(C.go_fuzz_get_random(h.fuzzGetRandom))
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) init_coverage(num_edge uint64, symbols string) uint64 {
|
||||
cstrSymbols := C.CString(symbols)
|
||||
defer C.free(unsafe.Pointer(cstrSymbols))
|
||||
return uint64(C.go_init_coverage(h.initCoverage, C.ulong(num_edge), cstrSymbols))
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) notify(edge uint64) bool {
|
||||
ival := int(C.go_notify_coverage(h.notifyCoverage, C.ulong(edge)))
|
||||
return ival == 1
|
||||
}
|
||||
|
||||
type localHandler struct {
|
||||
outputFile *os.File // can be nil
|
||||
}
|
||||
@@ -160,63 +69,12 @@ func (h *localHandler) init_coverage(num_edges uint64, symbols string) uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If we have a file at `defaultNativeLibraryPath`, we load the shared library
|
||||
// (and panic on any error encountered during load).
|
||||
// Otherwise fallback to the local handler.
|
||||
func init() {
|
||||
if _, err := os.Stat(defaultNativeLibraryPath); err == nil {
|
||||
if handler, err = openSharedLib(defaultNativeLibraryPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
handler = init_in_antithesis()
|
||||
if handler == nil {
|
||||
// Otherwise fallback to the local handler.
|
||||
handler = openLocalHandler()
|
||||
}
|
||||
handler = openLocalHandler()
|
||||
}
|
||||
|
||||
// Attempt to load libvoidstar and some symbols from `path`
|
||||
func openSharedLib(path string) (*voidstarHandler, error) {
|
||||
cstrPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cstrPath))
|
||||
|
||||
dlError := func(message string) error {
|
||||
return fmt.Errorf("%s: (%s)", message, C.GoString(C.dlerror()))
|
||||
}
|
||||
|
||||
sharedLib := C.dlopen(cstrPath, C.int(C.RTLD_NOW))
|
||||
if sharedLib == nil {
|
||||
return nil, dlError("Can not load the Antithesis native library")
|
||||
}
|
||||
|
||||
loadFunc := func(name string) (symbol unsafe.Pointer, err error) {
|
||||
cstrName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cstrName))
|
||||
if symbol = C.dlsym(sharedLib, cstrName); symbol == nil {
|
||||
err = dlError(fmt.Sprintf("Can not access symbol %s", name))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fuzzJsonData, err := loadFunc("fuzz_json_data")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fuzzFlush, err := loadFunc("fuzz_flush")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fuzzGetRandom, err := loadFunc("fuzz_get_random")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notifyCoverage, err := loadFunc("notify_coverage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initCoverage, err := loadFunc("init_coverage_module")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &voidstarHandler{fuzzJsonData, fuzzFlush, fuzzGetRandom, initCoverage, notifyCoverage}, nil
|
||||
}
|
||||
|
||||
// If `localOutputEnvVar` is set to a non-empty path, attempt to open that path and truncate the file
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package internal
|
||||
// --------------------------------------------------------------------------------
|
||||
// Versions
|
||||
// --------------------------------------------------------------------------------
|
||||
const SDK_Version = "0.4.3"
|
||||
const SDK_Version = "0.6.0"
|
||||
const Protocol_Version = "1.1.0"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
//go:build enable_antithesis_sdk && linux && amd64 && cgo
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
"os"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// To build and run an executable with this package
|
||||
//
|
||||
// CC=clang CGO_ENABLED=1 go run ./main.go
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
// \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
|
||||
//
|
||||
// The commented lines below, and the `import "C"` line which must directly follow
|
||||
// the commented lines are used by CGO. They are load-bearing, and should not be
|
||||
// changed without first understanding how CGO uses them.
|
||||
//
|
||||
// \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
|
||||
|
||||
// #cgo LDFLAGS: -ldl
|
||||
//
|
||||
// #include <dlfcn.h>
|
||||
// #include <stdbool.h>
|
||||
// #include <stdint.h>
|
||||
// #include <stdlib.h>
|
||||
//
|
||||
// typedef void (*go_fuzz_json_data_fn)(const char *data, size_t size);
|
||||
// void
|
||||
// go_fuzz_json_data(void *f, const char *data, size_t size) {
|
||||
// ((go_fuzz_json_data_fn)f)(data, size);
|
||||
// }
|
||||
//
|
||||
// typedef void (*go_fuzz_flush_fn)(void);
|
||||
// void
|
||||
// go_fuzz_flush(void *f) {
|
||||
// ((go_fuzz_flush_fn)f)();
|
||||
// }
|
||||
//
|
||||
// typedef uint64_t (*go_fuzz_get_random_fn)(void);
|
||||
// uint64_t
|
||||
// go_fuzz_get_random(void *f) {
|
||||
// return ((go_fuzz_get_random_fn)f)();
|
||||
// }
|
||||
//
|
||||
// typedef bool (*go_notify_coverage_fn)(size_t);
|
||||
// int
|
||||
// go_notify_coverage(void *f, size_t edges) {
|
||||
// bool b = ((go_notify_coverage_fn)f)(edges);
|
||||
// return b ? 1 : 0;
|
||||
// }
|
||||
//
|
||||
// typedef uint64_t (*go_init_coverage_fn)(size_t num_edges, const char *symbols);
|
||||
// uint64_t
|
||||
// go_init_coverage(void *f, size_t num_edges, const char *symbols) {
|
||||
// return ((go_init_coverage_fn)f)(num_edges, symbols);
|
||||
// }
|
||||
//
|
||||
import "C"
|
||||
|
||||
const (
|
||||
defaultNativeLibraryPath = "/usr/lib/libvoidstar.so"
|
||||
)
|
||||
|
||||
type voidstarHandler struct {
|
||||
fuzzJsonData unsafe.Pointer
|
||||
fuzzFlush unsafe.Pointer
|
||||
fuzzGetRandom unsafe.Pointer
|
||||
initCoverage unsafe.Pointer
|
||||
notifyCoverage unsafe.Pointer
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) output(message string) {
|
||||
msg_len := len(message)
|
||||
if msg_len == 0 {
|
||||
return
|
||||
}
|
||||
cstrMessage := C.CString(message)
|
||||
defer C.free(unsafe.Pointer(cstrMessage))
|
||||
C.go_fuzz_json_data(h.fuzzJsonData, cstrMessage, C.ulong(msg_len))
|
||||
C.go_fuzz_flush(h.fuzzFlush)
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) random() uint64 {
|
||||
return uint64(C.go_fuzz_get_random(h.fuzzGetRandom))
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) init_coverage(num_edge uint64, symbols string) uint64 {
|
||||
cstrSymbols := C.CString(symbols)
|
||||
defer C.free(unsafe.Pointer(cstrSymbols))
|
||||
return uint64(C.go_init_coverage(h.initCoverage, C.ulong(num_edge), cstrSymbols))
|
||||
}
|
||||
|
||||
func (h *voidstarHandler) notify(edge uint64) bool {
|
||||
ival := int(C.go_notify_coverage(h.notifyCoverage, C.ulong(edge)))
|
||||
return ival == 1
|
||||
}
|
||||
|
||||
// Attempt to load libvoidstar and some symbols from `path`
|
||||
func openSharedLib(path string) (*voidstarHandler, error) {
|
||||
cstrPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cstrPath))
|
||||
|
||||
dlError := func(message string) error {
|
||||
return fmt.Errorf("%s: (%s)", message, C.GoString(C.dlerror()))
|
||||
}
|
||||
|
||||
sharedLib := C.dlopen(cstrPath, C.int(C.RTLD_NOW))
|
||||
if sharedLib == nil {
|
||||
return nil, dlError("Can not load the Antithesis native library")
|
||||
}
|
||||
|
||||
loadFunc := func(name string) (symbol unsafe.Pointer, err error) {
|
||||
cstrName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cstrName))
|
||||
if symbol = C.dlsym(sharedLib, cstrName); symbol == nil {
|
||||
err = dlError(fmt.Sprintf("Can not access symbol %s", name))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fuzzJsonData, err := loadFunc("fuzz_json_data")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fuzzFlush, err := loadFunc("fuzz_flush")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fuzzGetRandom, err := loadFunc("fuzz_get_random")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notifyCoverage, err := loadFunc("notify_coverage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initCoverage, err := loadFunc("init_coverage_module")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &voidstarHandler{fuzzJsonData, fuzzFlush, fuzzGetRandom, initCoverage, notifyCoverage}, nil
|
||||
}
|
||||
|
||||
// If we have a file at `defaultNativeLibraryPath`, we load the shared library
|
||||
// (and panic on any error encountered during load).
|
||||
func init_in_antithesis() libHandler {
|
||||
if _, err := os.Stat(defaultNativeLibraryPath); err == nil {
|
||||
handler, err := openSharedLib(defaultNativeLibraryPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
//go:build enable_antithesis_sdk && (!linux || !amd64 || !cgo)
|
||||
|
||||
package internal
|
||||
|
||||
func init_in_antithesis() libHandler {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
linters:
|
||||
fast: false
|
||||
disable-all: true
|
||||
enable:
|
||||
- revive
|
||||
- megacheck
|
||||
- govet
|
||||
- unconvert
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
# - stylecheck
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
|
||||
# golangci-lint configuration file
|
||||
linters-settings:
|
||||
revive:
|
||||
ignore-generated-header: true
|
||||
severity: warning
|
||||
rules:
|
||||
- name: package-comments
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: exported
|
||||
severity: warning
|
||||
disabled: false
|
||||
arguments: ["checkPrivateReceivers", "disableStutteringCheck"]
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default2QRecentRatio is the ratio of the 2Q cache dedicated
|
||||
// to recently added entries that have only been accessed once.
|
||||
Default2QRecentRatio = 0.25
|
||||
|
||||
// Default2QGhostEntries is the default ratio of ghost
|
||||
// entries kept to track entries recently evicted
|
||||
Default2QGhostEntries = 0.50
|
||||
)
|
||||
|
||||
// TwoQueueCache is a thread-safe fixed size 2Q cache.
|
||||
// 2Q is an enhancement over the standard LRU cache
|
||||
// in that it tracks both frequently and recently used
|
||||
// entries separately. This avoids a burst in access to new
|
||||
// entries from evicting frequently used entries. It adds some
|
||||
// additional tracking overhead to the standard LRU cache, and is
|
||||
// computationally about 2x the cost, and adds some metadata over
|
||||
// head. The ARCCache is similar, but does not require setting any
|
||||
// parameters.
|
||||
type TwoQueueCache[K comparable, V any] struct {
|
||||
size int
|
||||
recentSize int
|
||||
recentRatio float64
|
||||
ghostRatio float64
|
||||
|
||||
recent simplelru.LRUCache[K, V]
|
||||
frequent simplelru.LRUCache[K, V]
|
||||
recentEvict simplelru.LRUCache[K, struct{}]
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New2Q creates a new TwoQueueCache using the default
|
||||
// values for the parameters.
|
||||
func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
|
||||
return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
|
||||
}
|
||||
|
||||
// New2QParams creates a new TwoQueueCache using the provided
|
||||
// parameter values.
|
||||
func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("invalid size")
|
||||
}
|
||||
if recentRatio < 0.0 || recentRatio > 1.0 {
|
||||
return nil, errors.New("invalid recent ratio")
|
||||
}
|
||||
if ghostRatio < 0.0 || ghostRatio > 1.0 {
|
||||
return nil, errors.New("invalid ghost ratio")
|
||||
}
|
||||
|
||||
// Determine the sub-sizes
|
||||
recentSize := int(float64(size) * recentRatio)
|
||||
evictSize := int(float64(size) * ghostRatio)
|
||||
|
||||
// Allocate the LRUs
|
||||
recent, err := simplelru.NewLRU[K, V](size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frequent, err := simplelru.NewLRU[K, V](size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the cache
|
||||
c := &TwoQueueCache[K, V]{
|
||||
size: size,
|
||||
recentSize: recentSize,
|
||||
recentRatio: recentRatio,
|
||||
ghostRatio: ghostRatio,
|
||||
recent: recent,
|
||||
frequent: frequent,
|
||||
recentEvict: recentEvict,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Check if this is a frequent value
|
||||
if val, ok := c.frequent.Get(key); ok {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// If the value is contained in recent, then we
|
||||
// promote it to frequent
|
||||
if val, ok := c.recent.Peek(key); ok {
|
||||
c.recent.Remove(key)
|
||||
c.frequent.Add(key, val)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// No hit
|
||||
return
|
||||
}
|
||||
|
||||
// Add adds a value to the cache.
|
||||
func (c *TwoQueueCache[K, V]) Add(key K, value V) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Check if the value is frequently used already,
|
||||
// and just update the value
|
||||
if c.frequent.Contains(key) {
|
||||
c.frequent.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the value is recently used, and promote
|
||||
// the value into the frequent list
|
||||
if c.recent.Contains(key) {
|
||||
c.recent.Remove(key)
|
||||
c.frequent.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// If the value was recently evicted, add it to the
|
||||
// frequently used list
|
||||
if c.recentEvict.Contains(key) {
|
||||
c.ensureSpace(true)
|
||||
c.recentEvict.Remove(key)
|
||||
c.frequent.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Add to the recently seen list
|
||||
c.ensureSpace(false)
|
||||
c.recent.Add(key, value)
|
||||
}
|
||||
|
||||
// ensureSpace is used to ensure we have space in the cache
|
||||
func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
|
||||
// If we have space, nothing to do
|
||||
recentLen := c.recent.Len()
|
||||
freqLen := c.frequent.Len()
|
||||
if recentLen+freqLen < c.size {
|
||||
return
|
||||
}
|
||||
|
||||
// If the recent buffer is larger than
|
||||
// the target, evict from there
|
||||
if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
|
||||
k, _, _ := c.recent.RemoveOldest()
|
||||
c.recentEvict.Add(k, struct{}{})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove from the frequent list otherwise
|
||||
c.frequent.RemoveOldest()
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *TwoQueueCache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.recent.Len() + c.frequent.Len()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Recalculate the sub-sizes
|
||||
recentSize := int(float64(size) * c.recentRatio)
|
||||
evictSize := int(float64(size) * c.ghostRatio)
|
||||
c.size = size
|
||||
c.recentSize = recentSize
|
||||
|
||||
// ensureSpace
|
||||
diff := c.recent.Len() + c.frequent.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.ensureSpace(true)
|
||||
}
|
||||
|
||||
// Reallocate the LRUs
|
||||
c.recent.Resize(size)
|
||||
c.frequent.Resize(size)
|
||||
c.recentEvict.Resize(evictSize)
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache.
|
||||
// The frequently used keys are first in the returned slice.
|
||||
func (c *TwoQueueCache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
k1 := c.frequent.Keys()
|
||||
k2 := c.recent.Keys()
|
||||
return append(k1, k2...)
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache.
|
||||
// The frequently used values are first in the returned slice.
|
||||
func (c *TwoQueueCache[K, V]) Values() []V {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
v1 := c.frequent.Values()
|
||||
v2 := c.recent.Values()
|
||||
return append(v1, v2...)
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *TwoQueueCache[K, V]) Remove(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.frequent.Remove(key) {
|
||||
return
|
||||
}
|
||||
if c.recent.Remove(key) {
|
||||
return
|
||||
}
|
||||
if c.recentEvict.Remove(key) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *TwoQueueCache[K, V]) Purge() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.recent.Purge()
|
||||
c.frequent.Purge()
|
||||
c.recentEvict.Purge()
|
||||
}
|
||||
|
||||
// Contains is used to check if the cache contains a key
|
||||
// without updating recency or frequency.
|
||||
func (c *TwoQueueCache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.frequent.Contains(key) || c.recent.Contains(key)
|
||||
}
|
||||
|
||||
// Peek is used to inspect the cache value of a key
|
||||
// without updating recency or frequency.
|
||||
func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
if val, ok := c.frequent.Peek(key); ok {
|
||||
return val, ok
|
||||
}
|
||||
return c.recent.Peek(key)
|
||||
}
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
Copyright (c) 2014 HashiCorp, Inc.
|
||||
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
golang-lru
|
||||
==========
|
||||
|
||||
This provides the `lru` package which implements a fixed-size
|
||||
thread safe LRU cache. It is based on the cache in Groupcache.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2)
|
||||
|
||||
LRU cache example
|
||||
=================
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
l, _ := lru.New[int, any](128)
|
||||
for i := 0; i < 256; i++ {
|
||||
l.Add(i, nil)
|
||||
}
|
||||
if l.Len() != 128 {
|
||||
panic(fmt.Sprintf("bad len: %v", l.Len()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expirable LRU cache example
|
||||
===========================
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// make cache with 10ms TTL and 5 max keys
|
||||
cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10)
|
||||
|
||||
|
||||
// set value under key1.
|
||||
cache.Add("key1", "val1")
|
||||
|
||||
// get value under key1
|
||||
r, ok := cache.Get("key1")
|
||||
|
||||
// check for OK value
|
||||
if ok {
|
||||
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
|
||||
}
|
||||
|
||||
// wait for cache to expire
|
||||
time.Sleep(time.Millisecond * 12)
|
||||
|
||||
// get value under key1 after key expiration
|
||||
r, ok = cache.Get("key1")
|
||||
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)
|
||||
|
||||
// set value under key2, would evict old entry because it is already expired.
|
||||
cache.Add("key2", "val2")
|
||||
|
||||
fmt.Printf("Cache len: %d\n", cache.Len())
|
||||
// Output:
|
||||
// value before expiration is found: true, value: "val1"
|
||||
// value after expiration is found: false, value: ""
|
||||
// Cache len: 1
|
||||
}
|
||||
```
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package lru provides three different LRU caches of varying sophistication.
|
||||
//
|
||||
// Cache is a simple LRU cache. It is based on the LRU implementation in
|
||||
// groupcache: https://github.com/golang/groupcache/tree/master/lru
|
||||
//
|
||||
// TwoQueueCache tracks frequently used and recently used entries separately.
|
||||
// This avoids a burst of accesses from taking out frequently used entries, at
|
||||
// the cost of about 2x computational overhead and some extra bookkeeping.
|
||||
//
|
||||
// ARCCache is an adaptive replacement cache. It tracks recent evictions as well
|
||||
// as recent usage in both the frequent and recent caches. Its computational
|
||||
// overhead is comparable to TwoQueueCache, but the memory overhead is linear
|
||||
// with the size of the cache.
|
||||
//
|
||||
// ARC has been patented by IBM, so do not use it if that is problematic for
|
||||
// your program. For this reason, it is in a separate go module contained within
|
||||
// this repository.
|
||||
//
|
||||
// All caches in this package take locks while operating, and are therefore
|
||||
// thread-safe for consumers.
|
||||
package lru
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE_list file.
|
||||
|
||||
package internal
|
||||
|
||||
import "time"
|
||||
|
||||
// Entry is an LRU Entry
|
||||
type Entry[K comparable, V any] struct {
|
||||
// Next and previous pointers in the doubly-linked list of elements.
|
||||
// To simplify the implementation, internally a list l is implemented
|
||||
// as a ring, such that &l.root is both the next element of the last
|
||||
// list element (l.Back()) and the previous element of the first list
|
||||
// element (l.Front()).
|
||||
next, prev *Entry[K, V]
|
||||
|
||||
// The list to which this element belongs.
|
||||
list *LruList[K, V]
|
||||
|
||||
// The LRU Key of this element.
|
||||
Key K
|
||||
|
||||
// The Value stored with this element.
|
||||
Value V
|
||||
|
||||
// The time this element would be cleaned up, optional
|
||||
ExpiresAt time.Time
|
||||
|
||||
// The expiry bucket item was put in, optional
|
||||
ExpireBucket uint8
|
||||
}
|
||||
|
||||
// PrevEntry returns the previous list element or nil.
|
||||
func (e *Entry[K, V]) PrevEntry() *Entry[K, V] {
|
||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LruList represents a doubly linked list.
|
||||
// The zero Value for LruList is an empty list ready to use.
|
||||
type LruList[K comparable, V any] struct {
|
||||
root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used
|
||||
len int // current list Length excluding (this) sentinel element
|
||||
}
|
||||
|
||||
// Init initializes or clears list l.
|
||||
func (l *LruList[K, V]) Init() *LruList[K, V] {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
return l
|
||||
}
|
||||
|
||||
// NewList returns an initialized list.
|
||||
func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() }
|
||||
|
||||
// Length returns the number of elements of list l.
|
||||
// The complexity is O(1).
|
||||
func (l *LruList[K, V]) Length() int { return l.len }
|
||||
|
||||
// Back returns the last element of list l or nil if the list is empty.
|
||||
func (l *LruList[K, V]) Back() *Entry[K, V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// lazyInit lazily initializes a zero List Value.
|
||||
func (l *LruList[K, V]) lazyInit() {
|
||||
if l.root.next == nil {
|
||||
l.Init()
|
||||
}
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
e.list = l
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).
|
||||
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
|
||||
return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
|
||||
}
|
||||
|
||||
// Remove removes e from its list, decrements l.len
|
||||
func (l *LruList[K, V]) Remove(e *Entry[K, V]) V {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
e.list = nil
|
||||
l.len--
|
||||
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// move moves e to next to at.
|
||||
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
|
||||
if e == at {
|
||||
return
|
||||
}
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(k, v, time.Time{}, &l.root)
|
||||
}
|
||||
|
||||
// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.
|
||||
func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(k, v, expiresAt, &l.root)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
|
||||
if e.list != l || l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, &l.root)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
|
||||
DefaultEvictedBufferSize = 16
|
||||
)
|
||||
|
||||
// Cache is a thread-safe fixed size LRU cache.
|
||||
type Cache[K comparable, V any] struct {
|
||||
lru *simplelru.LRU[K, V]
|
||||
evictedKeys []K
|
||||
evictedVals []V
|
||||
onEvictedCB func(k K, v V)
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates an LRU of the given size.
|
||||
func New[K comparable, V any](size int) (*Cache[K, V], error) {
|
||||
return NewWithEvict[K, V](size, nil)
|
||||
}
|
||||
|
||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
||||
// callback.
|
||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) {
|
||||
// create a cache with default settings
|
||||
c = &Cache[K, V]{
|
||||
onEvictedCB: onEvicted,
|
||||
}
|
||||
if onEvicted != nil {
|
||||
c.initEvictBuffers()
|
||||
onEvicted = c.onEvicted
|
||||
}
|
||||
c.lru, err = simplelru.NewLRU(size, onEvicted)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) initEvictBuffers() {
|
||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
||||
}
|
||||
|
||||
// onEvicted save evicted key/val and sent in externally registered callback
|
||||
// outside of critical section
|
||||
func (c *Cache[K, V]) onEvicted(k K, v V) {
|
||||
c.evictedKeys = append(c.evictedKeys, k)
|
||||
c.evictedVals = append(c.evictedVals, v)
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *Cache[K, V]) Purge() {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
c.lru.Purge()
|
||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
// invoke callback outside of critical section
|
||||
if c.onEvictedCB != nil {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *Cache[K, V]) Add(key K, value V) (evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Add(key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.lock.Lock()
|
||||
value, ok = c.lru.Get(key)
|
||||
c.lock.Unlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the
|
||||
// recent-ness or deleting it for being stale.
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
containKey := c.lru.Contains(key)
|
||||
c.lock.RUnlock()
|
||||
return containKey
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *Cache[K, V]) Peek(key K) (value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
value, ok = c.lru.Peek(key)
|
||||
c.lock.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
if c.lru.Contains(key) {
|
||||
c.lock.Unlock()
|
||||
return true, false
|
||||
}
|
||||
evicted = c.lru.Add(key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return false, evicted
|
||||
}
|
||||
|
||||
// PeekOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
previous, ok = c.lru.Peek(key)
|
||||
if ok {
|
||||
c.lock.Unlock()
|
||||
return previous, true, false
|
||||
}
|
||||
evicted = c.lru.Add(key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *Cache[K, V]) Remove(key K) (present bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
present = c.lru.Remove(key)
|
||||
if c.onEvictedCB != nil && present {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && present {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *Cache[K, V]) Resize(size int) (evicted int) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Resize(size)
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
return evicted
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
key, value, ok = c.lru.RemoveOldest()
|
||||
if c.onEvictedCB != nil && ok {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && ok {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
key, value, ok = c.lru.GetOldest()
|
||||
c.lock.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
keys := c.lru.Keys()
|
||||
c.lock.RUnlock()
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
func (c *Cache[K, V]) Values() []V {
|
||||
c.lock.RLock()
|
||||
values := c.lru.Values()
|
||||
c.lock.RUnlock()
|
||||
return values
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
length := c.lru.Len()
|
||||
c.lock.RUnlock()
|
||||
return length
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
This license applies to simplelru/list.go
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package simplelru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/internal"
|
||||
)
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
||||
|
||||
// LRU implements a non-thread safe fixed size LRU cache
|
||||
type LRU[K comparable, V any] struct {
|
||||
size int
|
||||
evictList *internal.LruList[K, V]
|
||||
items map[K]*internal.Entry[K, V]
|
||||
onEvict EvictCallback[K, V]
|
||||
}
|
||||
|
||||
// NewLRU constructs an LRU of the given size
|
||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("must provide a positive size")
|
||||
}
|
||||
|
||||
c := &LRU[K, V]{
|
||||
size: size,
|
||||
evictList: internal.NewList[K, V](),
|
||||
items: make(map[K]*internal.Entry[K, V]),
|
||||
onEvict: onEvict,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *LRU[K, V]) Purge() {
|
||||
for k, v := range c.items {
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(k, v.Value)
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
c.evictList.Init()
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
ent.Value = value
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new item
|
||||
ent := c.evictList.PushFront(key, value)
|
||||
c.items[key] = ent
|
||||
|
||||
evict := c.evictList.Length() > c.size
|
||||
// Verify size not exceeded
|
||||
if evict {
|
||||
c.removeOldest()
|
||||
}
|
||||
return evict
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
return ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
||||
// or deleting it for being stale.
|
||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
||||
_, ok = c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
|
||||
var ent *internal.Entry[K, V]
|
||||
if ent, ok = c.items[key]; ok {
|
||||
return ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache, returning if the
|
||||
// key was contained.
|
||||
func (c *LRU[K, V]) Remove(key K) (present bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.removeElement(ent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Keys() []K {
|
||||
keys := make([]K, c.evictList.Length())
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
keys[i] = ent.Key
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Values() []V {
|
||||
values := make([]V, len(c.items))
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
values[i] = ent.Value
|
||||
i++
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *LRU[K, V]) Len() int {
|
||||
return c.evictList.Length()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *LRU[K, V]) Resize(size int) (evicted int) {
|
||||
diff := c.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.removeOldest()
|
||||
}
|
||||
c.size = size
|
||||
return diff
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) removeOldest() {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache
|
||||
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
|
||||
c.evictList.Remove(e)
|
||||
delete(c.items, e.Key)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(e.Key, e.Value)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package simplelru provides simple LRU implementation based on build-in container/list.
|
||||
package simplelru
|
||||
|
||||
// LRUCache is the interface for simple LRU cache.
|
||||
type LRUCache[K comparable, V any] interface {
|
||||
// Adds a value to the cache, returns true if an eviction occurred and
|
||||
// updates the "recently used"-ness of the key.
|
||||
Add(key K, value V) bool
|
||||
|
||||
// Returns key's value from the cache and
|
||||
// updates the "recently used"-ness of the key. #value, isFound
|
||||
Get(key K) (value V, ok bool)
|
||||
|
||||
// Checks if a key exists in cache without updating the recent-ness.
|
||||
Contains(key K) (ok bool)
|
||||
|
||||
// Returns key's value without updating the "recently used"-ness of the key.
|
||||
Peek(key K) (value V, ok bool)
|
||||
|
||||
// Removes a key from the cache.
|
||||
Remove(key K) bool
|
||||
|
||||
// Removes the oldest entry from cache.
|
||||
RemoveOldest() (K, V, bool)
|
||||
|
||||
// Returns the oldest entry from the cache. #key, value, isFound
|
||||
GetOldest() (K, V, bool)
|
||||
|
||||
// Returns a slice of the keys in the cache, from oldest to newest.
|
||||
Keys() []K
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
Values() []V
|
||||
|
||||
// Returns the number of items in the cache.
|
||||
Len() int
|
||||
|
||||
// Clears all cache entries.
|
||||
Purge()
|
||||
|
||||
// Resizes cache, returning number evicted
|
||||
Resize(int) int
|
||||
}
|
||||
+30
-8
@@ -7,7 +7,7 @@ This package provides various compression algorithms.
|
||||
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
|
||||
* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
|
||||
* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
|
||||
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped requests efficiently.
|
||||
* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
|
||||
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
|
||||
|
||||
[](https://pkg.go.dev/github.com/klauspost/compress?tab=subdirectories)
|
||||
@@ -26,6 +26,22 @@ This package will support the current Go version and 2 versions back.
|
||||
Use the links above for more information on each.
|
||||
|
||||
# changelog
|
||||
* Jan 16th, 2026 [1.18.3](https://github.com/klauspost/compress/releases/tag/v1.18.3)
|
||||
* Downstream CVE-2025-61728. See [golang/go#77102](https://github.com/golang/go/issues/77102).
|
||||
|
||||
* Dec 1st, 2025 - [1.18.2](https://github.com/klauspost/compress/releases/tag/v1.18.2)
|
||||
* flate: Fix invalid encoding on level 9 with single value input in https://github.com/klauspost/compress/pull/1115
|
||||
* flate: reduce stateless allocations by @RXamzin in https://github.com/klauspost/compress/pull/1106
|
||||
|
||||
* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1) - RETRACTED
|
||||
* zstd: Add simple zstd EncodeTo/DecodeTo functions https://github.com/klauspost/compress/pull/1079
|
||||
* zstd: Fix incorrect buffer size in dictionary encodes https://github.com/klauspost/compress/pull/1059
|
||||
* s2: check for cap, not len of buffer in EncodeBetter/Best by @vdarulis in https://github.com/klauspost/compress/pull/1080
|
||||
* zlib: Avoiding extra allocation in zlib.reader.Reset by @travelpolicy in https://github.com/klauspost/compress/pull/1086
|
||||
* gzhttp: remove redundant err check in zstdReader by @ryanfowler in https://github.com/klauspost/compress/pull/1090
|
||||
* flate: Faster load+store https://github.com/klauspost/compress/pull/1104
|
||||
* flate: Simplify matchlen https://github.com/klauspost/compress/pull/1101
|
||||
* flate: Use exact sizes for huffman tables https://github.com/klauspost/compress/pull/1103
|
||||
|
||||
* Feb 19th, 2025 - [1.18.0](https://github.com/klauspost/compress/releases/tag/v1.18.0)
|
||||
* Add unsafe little endian loaders https://github.com/klauspost/compress/pull/1036
|
||||
@@ -36,6 +52,9 @@ Use the links above for more information on each.
|
||||
* flate: Fix matchlen L5+L6 https://github.com/klauspost/compress/pull/1049
|
||||
* flate: Cleanup & reduce casts https://github.com/klauspost/compress/pull/1050
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.17.x</summary>
|
||||
|
||||
* Oct 11th, 2024 - [1.17.11](https://github.com/klauspost/compress/releases/tag/v1.17.11)
|
||||
* zstd: Fix extra CRC written with multiple Close calls https://github.com/klauspost/compress/pull/1017
|
||||
* s2: Don't use stack for index tables https://github.com/klauspost/compress/pull/1014
|
||||
@@ -102,7 +121,8 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
|
||||
* s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
|
||||
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
|
||||
* gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
|
||||
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>See changes to v1.16.x</summary>
|
||||
|
||||
@@ -589,7 +609,7 @@ While the release has been extensively tested, it is recommended to testing when
|
||||
|
||||
# deflate usage
|
||||
|
||||
The packages are drop-in replacements for standard libraries. Simply replace the import path to use them:
|
||||
The packages are drop-in replacements for standard library [deflate](https://godoc.org/github.com/klauspost/compress/flate), [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip), and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). Simply replace the import path to use them:
|
||||
|
||||
Typical speed is about 2x of the standard library packages.
|
||||
|
||||
@@ -600,17 +620,15 @@ Typical speed is about 2x of the standard library packages.
|
||||
| `archive/zip` | `github.com/klauspost/compress/zip` | [zip](https://pkg.go.dev/github.com/klauspost/compress/zip?tab=doc) |
|
||||
| `compress/flate` | `github.com/klauspost/compress/flate` | [flate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc) |
|
||||
|
||||
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
|
||||
You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop-in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
|
||||
|
||||
You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
|
||||
|
||||
The packages contains the same as the standard library, so you can use the godoc for that: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
|
||||
The packages implement the same API as the standard library, so you can use the original godoc documentation: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
|
||||
|
||||
Currently there is only minor speedup on decompression (mostly CRC32 calculation).
|
||||
|
||||
Memory usage is typically 1MB for a Writer. stdlib is in the same range.
|
||||
If you expect to have a lot of concurrently allocated Writers consider using
|
||||
the stateless compress described below.
|
||||
the stateless compression described below.
|
||||
|
||||
For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
|
||||
|
||||
@@ -669,3 +687,7 @@ Here are other packages of good quality and pure Go (no cgo wrappers or autoconv
|
||||
# license
|
||||
|
||||
This code is licensed under the same conditions as the original Go code. See LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
+3
-1
@@ -421,7 +421,9 @@ func (d *compressor) deflateLazy() {
|
||||
d.h = newHuffmanEncoder(maxFlateBlockTokens)
|
||||
}
|
||||
var tmp [256]uint16
|
||||
for _, v := range d.window[s.index:d.windowEnd] {
|
||||
toIndex := d.window[s.index:d.windowEnd]
|
||||
toIndex = toIndex[:min(len(toIndex), maxFlateBlockTokens)]
|
||||
for _, v := range toIndex {
|
||||
tmp[v]++
|
||||
}
|
||||
d.h.generate(tmp[:], 15)
|
||||
|
||||
+5
-4
@@ -646,7 +646,7 @@ func (w *huffmanBitWriter) writeBlockDynamic(tokens *tokens, eof bool, input []b
|
||||
w.lastHeader = 0
|
||||
}
|
||||
|
||||
numLiterals, numOffsets := w.indexTokens(tokens, fillReuse && !sync)
|
||||
numLiterals, numOffsets := w.indexTokens(tokens, true)
|
||||
extraBits := 0
|
||||
ssize, storable := w.storedSize(input)
|
||||
|
||||
@@ -781,7 +781,7 @@ func (w *huffmanBitWriter) fillTokens() {
|
||||
// literalFreq and offsetFreq, and generates literalEncoding
|
||||
// and offsetEncoding.
|
||||
// The number of literal and offset tokens is returned.
|
||||
func (w *huffmanBitWriter) indexTokens(t *tokens, filled bool) (numLiterals, numOffsets int) {
|
||||
func (w *huffmanBitWriter) indexTokens(t *tokens, alwaysEOB bool) (numLiterals, numOffsets int) {
|
||||
//copy(w.literalFreq[:], t.litHist[:])
|
||||
*(*[256]uint16)(w.literalFreq[:]) = t.litHist
|
||||
//copy(w.literalFreq[256:], t.extraHist[:])
|
||||
@@ -791,9 +791,10 @@ func (w *huffmanBitWriter) indexTokens(t *tokens, filled bool) (numLiterals, num
|
||||
if t.n == 0 {
|
||||
return
|
||||
}
|
||||
if filled {
|
||||
return maxNumLit, maxNumDist
|
||||
if alwaysEOB {
|
||||
w.literalFreq[endBlockMarker] = 1
|
||||
}
|
||||
|
||||
// get the number of literals
|
||||
numLiterals = len(w.literalFreq)
|
||||
for w.literalFreq[numLiterals-1] == 0 {
|
||||
|
||||
+16
-4
@@ -61,13 +61,19 @@ var bitWriterPool = sync.Pool{
|
||||
},
|
||||
}
|
||||
|
||||
// tokensPool contains tokens struct objects that can be reused
|
||||
var tokensPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &tokens{}
|
||||
},
|
||||
}
|
||||
|
||||
// StatelessDeflate allows compressing directly to a Writer without retaining state.
|
||||
// When returning everything will be flushed.
|
||||
// Up to 8KB of an optional dictionary can be given which is presumed to precede the block.
|
||||
// Longer dictionaries will be truncated and will still produce valid output.
|
||||
// Sending nil dictionary is perfectly fine.
|
||||
func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error {
|
||||
var dst tokens
|
||||
bw := bitWriterPool.Get().(*huffmanBitWriter)
|
||||
bw.reset(out)
|
||||
defer func() {
|
||||
@@ -91,6 +97,12 @@ func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error {
|
||||
// For subsequent loops, keep shallow dict reference to avoid alloc+copy.
|
||||
var inDict []byte
|
||||
|
||||
dst := tokensPool.Get().(*tokens)
|
||||
dst.Reset()
|
||||
defer func() {
|
||||
tokensPool.Put(dst)
|
||||
}()
|
||||
|
||||
for len(in) > 0 {
|
||||
todo := in
|
||||
if len(inDict) > 0 {
|
||||
@@ -113,9 +125,9 @@ func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error {
|
||||
}
|
||||
// Compress
|
||||
if len(inDict) == 0 {
|
||||
statelessEnc(&dst, todo, int16(len(dict)))
|
||||
statelessEnc(dst, todo, int16(len(dict)))
|
||||
} else {
|
||||
statelessEnc(&dst, inDict[:maxStatelessDict+len(todo)], maxStatelessDict)
|
||||
statelessEnc(dst, inDict[:maxStatelessDict+len(todo)], maxStatelessDict)
|
||||
}
|
||||
isEof := eof && len(in) == 0
|
||||
|
||||
@@ -129,7 +141,7 @@ func StatelessDeflate(out io.Writer, in []byte, eof bool, dict []byte) error {
|
||||
// If we removed less than 1/16th, huffman compress the block.
|
||||
bw.writeBlockHuff(isEof, uncompressed, len(in) == 0)
|
||||
} else {
|
||||
bw.writeBlockDynamic(&dst, isEof, uncompressed, len(in) == 0)
|
||||
bw.writeBlockDynamic(dst, isEof, uncompressed, len(in) == 0)
|
||||
}
|
||||
if len(in) > 0 {
|
||||
// Retain a dict if we have more
|
||||
|
||||
+19
-9
@@ -39,9 +39,6 @@ type Decoder struct {
|
||||
|
||||
frame *frameDec
|
||||
|
||||
// Custom dictionaries.
|
||||
dicts map[uint32]*dict
|
||||
|
||||
// streamWg is the waitgroup for all streams
|
||||
streamWg sync.WaitGroup
|
||||
}
|
||||
@@ -101,12 +98,10 @@ func NewReader(r io.Reader, opts ...DOption) (*Decoder, error) {
|
||||
d.current.err = ErrDecoderNilInput
|
||||
}
|
||||
|
||||
// Transfer option dicts.
|
||||
d.dicts = make(map[uint32]*dict, len(d.o.dicts))
|
||||
for _, dc := range d.o.dicts {
|
||||
d.dicts[dc.id] = dc
|
||||
// Initialize dict map if needed.
|
||||
if d.o.dicts == nil {
|
||||
d.o.dicts = make(map[uint32]*dict)
|
||||
}
|
||||
d.o.dicts = nil
|
||||
|
||||
// Create decoders
|
||||
d.decoders = make(chan *blockDec, d.o.concurrent)
|
||||
@@ -238,6 +233,21 @@ func (d *Decoder) Reset(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetWithOptions will reset the decoder and apply the given options
|
||||
// for the next stream or DecodeAll operation.
|
||||
// Options are applied on top of the existing options.
|
||||
// Some options cannot be changed on reset and will return an error.
|
||||
func (d *Decoder) ResetWithOptions(r io.Reader, opts ...DOption) error {
|
||||
d.o.resetOpt = true
|
||||
defer func() { d.o.resetOpt = false }()
|
||||
for _, o := range opts {
|
||||
if err := o(&d.o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return d.Reset(r)
|
||||
}
|
||||
|
||||
// drainOutput will drain the output until errEndOfStream is sent.
|
||||
func (d *Decoder) drainOutput() {
|
||||
if d.current.cancel != nil {
|
||||
@@ -930,7 +940,7 @@ decodeStream:
|
||||
}
|
||||
|
||||
func (d *Decoder) setDict(frame *frameDec) (err error) {
|
||||
dict, ok := d.dicts[frame.DictionaryID]
|
||||
dict, ok := d.o.dicts[frame.DictionaryID]
|
||||
if ok {
|
||||
if debugDecoder {
|
||||
println("setting dict", frame.DictionaryID)
|
||||
|
||||
+52
-8
@@ -20,10 +20,11 @@ type decoderOptions struct {
|
||||
concurrent int
|
||||
maxDecodedSize uint64
|
||||
maxWindowSize uint64
|
||||
dicts []*dict
|
||||
dicts map[uint32]*dict
|
||||
ignoreChecksum bool
|
||||
limitToCap bool
|
||||
decodeBufsBelow int
|
||||
resetOpt bool
|
||||
}
|
||||
|
||||
func (o *decoderOptions) setDefault() {
|
||||
@@ -42,8 +43,15 @@ func (o *decoderOptions) setDefault() {
|
||||
|
||||
// WithDecoderLowmem will set whether to use a lower amount of memory,
|
||||
// but possibly have to allocate more while running.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithDecoderLowmem(b bool) DOption {
|
||||
return func(o *decoderOptions) error { o.lowMem = b; return nil }
|
||||
return func(o *decoderOptions) error {
|
||||
if o.resetOpt && b != o.lowMem {
|
||||
return errors.New("WithDecoderLowmem cannot be changed on Reset")
|
||||
}
|
||||
o.lowMem = b
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDecoderConcurrency sets the number of created decoders.
|
||||
@@ -53,18 +61,23 @@ func WithDecoderLowmem(b bool) DOption {
|
||||
// inflight blocks.
|
||||
// When decoding streams and setting maximum to 1,
|
||||
// no async decoding will be done.
|
||||
// The value supplied must be at least 0.
|
||||
// When a value of 0 is provided GOMAXPROCS will be used.
|
||||
// By default this will be set to 4 or GOMAXPROCS, whatever is lower.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithDecoderConcurrency(n int) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if n < 0 {
|
||||
return errors.New("concurrency must be at least 1")
|
||||
return errors.New("concurrency must be at least 0")
|
||||
}
|
||||
newVal := n
|
||||
if n == 0 {
|
||||
o.concurrent = runtime.GOMAXPROCS(0)
|
||||
} else {
|
||||
o.concurrent = n
|
||||
newVal = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
if o.resetOpt && newVal != o.concurrent {
|
||||
return errors.New("WithDecoderConcurrency cannot be changed on Reset")
|
||||
}
|
||||
o.concurrent = newVal
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -73,6 +86,7 @@ func WithDecoderConcurrency(n int) DOption {
|
||||
// non-streaming operations or maximum window size for streaming operations.
|
||||
// This can be used to control memory usage of potentially hostile content.
|
||||
// Maximum is 1 << 63 bytes. Default is 64GiB.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithDecoderMaxMemory(n uint64) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if n == 0 {
|
||||
@@ -92,16 +106,20 @@ func WithDecoderMaxMemory(n uint64) DOption {
|
||||
// "zstd --train" from the Zstandard reference implementation.
|
||||
//
|
||||
// If several dictionaries with the same ID are provided, the last one will be used.
|
||||
// Can be changed with ResetWithOptions.
|
||||
//
|
||||
// [dictionary format]: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format
|
||||
func WithDecoderDicts(dicts ...[]byte) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if o.dicts == nil {
|
||||
o.dicts = make(map[uint32]*dict)
|
||||
}
|
||||
for _, b := range dicts {
|
||||
d, err := loadDict(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.dicts = append(o.dicts, d)
|
||||
o.dicts[d.id] = d
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -109,12 +127,16 @@ func WithDecoderDicts(dicts ...[]byte) DOption {
|
||||
|
||||
// WithDecoderDictRaw registers a dictionary that may be used by the decoder.
|
||||
// The slice content can be arbitrary data.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithDecoderDictRaw(id uint32, content []byte) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if bits.UintSize > 32 && uint(len(content)) > dictMaxLength {
|
||||
return fmt.Errorf("dictionary of size %d > 2GiB too large", len(content))
|
||||
}
|
||||
o.dicts = append(o.dicts, &dict{id: id, content: content, offsets: [3]int{1, 4, 8}})
|
||||
if o.dicts == nil {
|
||||
o.dicts = make(map[uint32]*dict)
|
||||
}
|
||||
o.dicts[id] = &dict{id: id, content: content, offsets: [3]int{1, 4, 8}}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -124,6 +146,7 @@ func WithDecoderDictRaw(id uint32, content []byte) DOption {
|
||||
// The Decoder will likely allocate more memory based on the WithDecoderLowmem setting.
|
||||
// If WithDecoderMaxMemory is set to a lower value, that will be used.
|
||||
// Default is 512MB, Maximum is ~3.75 TB as per zstandard spec.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithDecoderMaxWindow(size uint64) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if size < MinWindowSize {
|
||||
@@ -141,6 +164,7 @@ func WithDecoderMaxWindow(size uint64) DOption {
|
||||
// or any size set in WithDecoderMaxMemory.
|
||||
// This can be used to limit decoding to a specific maximum output size.
|
||||
// Disabled by default.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithDecodeAllCapLimit(b bool) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
o.limitToCap = b
|
||||
@@ -153,17 +177,37 @@ func WithDecodeAllCapLimit(b bool) DOption {
|
||||
// This typically uses less allocations but will have the full decompressed object in memory.
|
||||
// Note that DecodeAllCapLimit will disable this, as well as giving a size of 0 or less.
|
||||
// Default is 128KiB.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithDecodeBuffersBelow(size int) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if o.resetOpt && size != o.decodeBufsBelow {
|
||||
return errors.New("WithDecodeBuffersBelow cannot be changed on Reset")
|
||||
}
|
||||
o.decodeBufsBelow = size
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IgnoreChecksum allows to forcibly ignore checksum checking.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func IgnoreChecksum(b bool) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
o.ignoreChecksum = b
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDecoderDictDelete removes dictionaries by ID.
|
||||
// If no ids are passed, all dictionaries are deleted.
|
||||
// Should be used with ResetWithOptions.
|
||||
func WithDecoderDictDelete(ids ...uint32) DOption {
|
||||
return func(o *decoderOptions) error {
|
||||
if len(ids) == 0 {
|
||||
clear(o.dicts)
|
||||
}
|
||||
for _, id := range ids {
|
||||
delete(o.dicts, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+16
@@ -131,6 +131,22 @@ func (e *Encoder) Reset(w io.Writer) {
|
||||
s.frameContentSize = 0
|
||||
}
|
||||
|
||||
// ResetWithOptions will re-initialize the writer and apply the given options
|
||||
// as a new, independent stream.
|
||||
// Options are applied on top of the existing options.
|
||||
// Some options cannot be changed on reset and will return an error.
|
||||
func (e *Encoder) ResetWithOptions(w io.Writer, opts ...EOption) error {
|
||||
e.o.resetOpt = true
|
||||
defer func() { e.o.resetOpt = false }()
|
||||
for _, o := range opts {
|
||||
if err := o(&e.o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
e.Reset(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetContentSize will reset and set a content size for the next stream.
|
||||
// If the bytes written does not match the size given an error will be returned
|
||||
// when calling Close().
|
||||
|
||||
+41
-3
@@ -14,6 +14,7 @@ type EOption func(*encoderOptions) error
|
||||
|
||||
// options retains accumulated state of multiple options.
|
||||
type encoderOptions struct {
|
||||
resetOpt bool
|
||||
concurrent int
|
||||
level EncoderLevel
|
||||
single *bool
|
||||
@@ -71,19 +72,28 @@ func (o encoderOptions) encoder() encoder {
|
||||
|
||||
// WithEncoderCRC will add CRC value to output.
|
||||
// Output will be 4 bytes larger.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithEncoderCRC(b bool) EOption {
|
||||
return func(o *encoderOptions) error { o.crc = b; return nil }
|
||||
}
|
||||
|
||||
// WithEncoderConcurrency will set the concurrency,
|
||||
// meaning the maximum number of encoders to run concurrently.
|
||||
// The value supplied must be at least 1.
|
||||
// The value supplied must be at least 0.
|
||||
// When a value of 0 is provided GOMAXPROCS will be used.
|
||||
// For streams, setting a value of 1 will disable async compression.
|
||||
// By default this will be set to GOMAXPROCS.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithEncoderConcurrency(n int) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
if n <= 0 {
|
||||
return fmt.Errorf("concurrency must be at least 1")
|
||||
if n < 0 {
|
||||
return errors.New("concurrency must at least 0")
|
||||
}
|
||||
if n == 0 {
|
||||
n = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
if o.resetOpt && n != o.concurrent {
|
||||
return errors.New("WithEncoderConcurrency cannot be changed on Reset")
|
||||
}
|
||||
o.concurrent = n
|
||||
return nil
|
||||
@@ -95,6 +105,7 @@ func WithEncoderConcurrency(n int) EOption {
|
||||
// A larger value will enable better compression but allocate more memory and,
|
||||
// for above-default values, take considerably longer.
|
||||
// The default value is determined by the compression level and max 8MB.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithWindowSize(n int) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
switch {
|
||||
@@ -105,6 +116,9 @@ func WithWindowSize(n int) EOption {
|
||||
case (n & (n - 1)) != 0:
|
||||
return errors.New("window size must be a power of 2")
|
||||
}
|
||||
if o.resetOpt && n != o.windowSize {
|
||||
return errors.New("WithWindowSize cannot be changed on Reset")
|
||||
}
|
||||
|
||||
o.windowSize = n
|
||||
o.customWindow = true
|
||||
@@ -122,6 +136,7 @@ func WithWindowSize(n int) EOption {
|
||||
// n must be > 0 and <= 1GB, 1<<30 bytes.
|
||||
// The padded area will be filled with data from crypto/rand.Reader.
|
||||
// If `EncodeAll` is used with data already in the destination, the total size will be multiple of this.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithEncoderPadding(n int) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
if n <= 0 {
|
||||
@@ -215,12 +230,16 @@ func (e EncoderLevel) String() string {
|
||||
}
|
||||
|
||||
// WithEncoderLevel specifies a predefined compression level.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithEncoderLevel(l EncoderLevel) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
switch {
|
||||
case l <= speedNotSet || l >= speedLast:
|
||||
return fmt.Errorf("unknown encoder level")
|
||||
}
|
||||
if o.resetOpt && l != o.level {
|
||||
return errors.New("WithEncoderLevel cannot be changed on Reset")
|
||||
}
|
||||
o.level = l
|
||||
if !o.customWindow {
|
||||
switch o.level {
|
||||
@@ -248,6 +267,7 @@ func WithEncoderLevel(l EncoderLevel) EOption {
|
||||
// WithZeroFrames will encode 0 length input as full frames.
|
||||
// This can be needed for compatibility with zstandard usage,
|
||||
// but is not needed for this package.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithZeroFrames(b bool) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
o.fullZero = b
|
||||
@@ -259,6 +279,7 @@ func WithZeroFrames(b bool) EOption {
|
||||
// Disabling this will skip incompressible data faster, but in cases with no matches but
|
||||
// skewed character distribution compression is lost.
|
||||
// Default value depends on the compression level selected.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithAllLitEntropyCompression(b bool) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
o.customALEntropy = true
|
||||
@@ -270,6 +291,7 @@ func WithAllLitEntropyCompression(b bool) EOption {
|
||||
// WithNoEntropyCompression will always skip entropy compression of literals.
|
||||
// This can be useful if content has matches, but unlikely to benefit from entropy
|
||||
// compression. Usually the slight speed improvement is not worth enabling this.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithNoEntropyCompression(b bool) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
o.noEntropy = b
|
||||
@@ -287,6 +309,7 @@ func WithNoEntropyCompression(b bool) EOption {
|
||||
// This is only a recommendation, each decoder is free to support higher or lower limits, depending on local limitations.
|
||||
// If this is not specified, block encodes will automatically choose this based on the input size and the window size.
|
||||
// This setting has no effect on streamed encodes.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithSingleSegment(b bool) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
o.single = &b
|
||||
@@ -298,8 +321,12 @@ func WithSingleSegment(b bool) EOption {
|
||||
// slower encoding speed.
|
||||
// This will not change the window size which is the primary function for reducing
|
||||
// memory usage. See WithWindowSize.
|
||||
// Cannot be changed with ResetWithOptions.
|
||||
func WithLowerEncoderMem(b bool) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
if o.resetOpt && b != o.lowMem {
|
||||
return errors.New("WithLowerEncoderMem cannot be changed on Reset")
|
||||
}
|
||||
o.lowMem = b
|
||||
return nil
|
||||
}
|
||||
@@ -311,6 +338,7 @@ func WithLowerEncoderMem(b bool) EOption {
|
||||
// "zstd --train" from the Zstandard reference implementation.
|
||||
//
|
||||
// The encoder *may* choose to use no dictionary instead for certain payloads.
|
||||
// Can be changed with ResetWithOptions.
|
||||
//
|
||||
// [dictionary format]: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format
|
||||
func WithEncoderDict(dict []byte) EOption {
|
||||
@@ -328,6 +356,7 @@ func WithEncoderDict(dict []byte) EOption {
|
||||
//
|
||||
// The slice content may contain arbitrary data. It will be used as an initial
|
||||
// history.
|
||||
// Can be changed with ResetWithOptions.
|
||||
func WithEncoderDictRaw(id uint32, content []byte) EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
if bits.UintSize > 32 && uint(len(content)) > dictMaxLength {
|
||||
@@ -337,3 +366,12 @@ func WithEncoderDictRaw(id uint32, content []byte) EOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEncoderDictDelete clears the dictionary, so no dictionary will be used.
|
||||
// Should be used with ResetWithOptions.
|
||||
func WithEncoderDictDelete() EOption {
|
||||
return func(o *encoderOptions) error {
|
||||
o.dict = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -152,19 +152,22 @@ type Mapping map[Subject][]WeightedMapping
|
||||
func (m *Mapping) Validate(vr *ValidationResults) {
|
||||
for ubFrom, wm := range (map[Subject][]WeightedMapping)(*m) {
|
||||
ubFrom.Validate(vr)
|
||||
perCluster := make(map[string]uint8)
|
||||
total := uint8(0)
|
||||
perCluster := make(map[string]uint32)
|
||||
total := uint32(0)
|
||||
for _, e := range wm {
|
||||
e.Subject.Validate(vr)
|
||||
if e.GetWeight() > 100 {
|
||||
vr.AddError("Mapping %q has a weight %d that exceeds 100", ubFrom, e.GetWeight())
|
||||
}
|
||||
if e.Cluster != "" {
|
||||
t := perCluster[e.Cluster]
|
||||
t += e.Weight
|
||||
t += uint32(e.GetWeight())
|
||||
perCluster[e.Cluster] = t
|
||||
if t > 100 {
|
||||
vr.AddError("Mapping %q in cluster %q exceeds 100%% among all of it's weighted to mappings", ubFrom, e.Cluster)
|
||||
}
|
||||
} else {
|
||||
total += e.GetWeight()
|
||||
total += uint32(e.GetWeight())
|
||||
}
|
||||
}
|
||||
if total > 100 {
|
||||
@@ -286,7 +289,7 @@ func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
|
||||
tvr := CreateValidationResults()
|
||||
a.Trace.Destination.Validate(tvr)
|
||||
if !tvr.IsEmpty() {
|
||||
vr.AddError(fmt.Sprintf("the account Trace.Destination %s", tvr.Issues[0].Description))
|
||||
vr.AddError("the account Trace.Destination %s", tvr.Issues[0].Description)
|
||||
}
|
||||
if a.Trace.Destination.HasWildCards() {
|
||||
vr.AddError("the account Trace.Destination subject %q is not a valid publish subject", a.Trace.Destination)
|
||||
@@ -325,7 +328,7 @@ func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
|
||||
a.Info.Validate(vr)
|
||||
|
||||
if err := a.ClusterTraffic.Valid(); err != nil {
|
||||
vr.AddError(err.Error())
|
||||
vr.AddError("%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -63,6 +63,9 @@ func formatJwt(kind string, jwtString string) ([]byte, error) {
|
||||
func DecorateSeed(seed []byte) ([]byte, error) {
|
||||
w := bytes.NewBuffer(nil)
|
||||
ts := bytes.TrimSpace(seed)
|
||||
if len(ts) < 2 {
|
||||
return nil, errors.New("seed is too short")
|
||||
}
|
||||
pre := string(ts[0:2])
|
||||
kind := ""
|
||||
switch pre {
|
||||
@@ -138,6 +141,18 @@ func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("nkey seed is not an user seed")
|
||||
}
|
||||
|
||||
kp, err := nkeys.FromSeed(seed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pk, err := kp.PublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pk != gc.Claims().Subject {
|
||||
return nil, fmt.Errorf("nkey seed does not match the jwt subject")
|
||||
}
|
||||
|
||||
d, err := DecorateSeed(seed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+9
@@ -26,6 +26,12 @@ import (
|
||||
|
||||
const libVersion = 2
|
||||
|
||||
// MaxTokenSize is the maximum size of a JWT token in bytes
|
||||
const MaxTokenSize = 1024 * 1024 // 1MB
|
||||
|
||||
// ErrTokenTooLarge is returned when a token exceeds MaxTokenSize
|
||||
var ErrTokenTooLarge = errors.New("token too large")
|
||||
|
||||
type identifier struct {
|
||||
Type ClaimType `json:"type,omitempty"`
|
||||
GenericFields `json:"nats,omitempty"`
|
||||
@@ -56,6 +62,9 @@ type v1ClaimsDataDeletedFields struct {
|
||||
// doesn't match the expected algorithm, or the claim is
|
||||
// not valid or verification fails an error is returned.
|
||||
func Decode(token string) (Claims, error) {
|
||||
if len(token) > MaxTokenSize {
|
||||
return nil, fmt.Errorf("token size %d exceeds maximum of %d bytes: %w", len(token), MaxTokenSize, ErrTokenTooLarge)
|
||||
}
|
||||
// must have 3 chunks
|
||||
chunks := strings.Split(token, ".")
|
||||
if len(chunks) != 3 {
|
||||
|
||||
+12
-7
@@ -126,7 +126,8 @@ type Imports []*Import
|
||||
|
||||
// Validate checks if an import is valid for the wrapping account
|
||||
func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
|
||||
toSet := make(map[Subject]struct{}, len(*i))
|
||||
// Group subjects by account to check for overlaps only within the same account
|
||||
subsByAcct := make(map[string]map[Subject]struct{}, len(*i))
|
||||
for _, v := range *i {
|
||||
if v == nil {
|
||||
vr.AddError("null import is not allowed")
|
||||
@@ -140,15 +141,19 @@ func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
|
||||
if sub == "" {
|
||||
sub = v.Subject
|
||||
}
|
||||
for k := range toSet {
|
||||
if sub.IsContainedIn(k) || k.IsContainedIn(sub) {
|
||||
vr.AddError("overlapping subject namespace for %q and %q", sub, k)
|
||||
// Check for overlapping subjects only within the same account
|
||||
for subOther := range subsByAcct[v.Account] {
|
||||
if sub.IsContainedIn(subOther) || subOther.IsContainedIn(sub) {
|
||||
vr.AddError("overlapping subject namespace for %q and %q in same account %q", sub, subOther, v.Account)
|
||||
}
|
||||
}
|
||||
if _, ok := toSet[sub]; ok {
|
||||
vr.AddError("overlapping subject namespace for %q", v.To)
|
||||
if subsByAcct[v.Account] == nil {
|
||||
subsByAcct[v.Account] = make(map[Subject]struct{}, len(*i))
|
||||
}
|
||||
toSet[sub] = struct{}{}
|
||||
if _, ok := subsByAcct[v.Account][sub]; ok {
|
||||
vr.AddError("overlapping subject namespace for %q in account %q", sub, v.Account)
|
||||
}
|
||||
subsByAcct[v.Account][sub] = struct{}{}
|
||||
}
|
||||
v.Validate(acctPubKey, vr)
|
||||
}
|
||||
|
||||
+2
-2
@@ -71,12 +71,12 @@ func ParseServerVersion(version string) (int, int, int, error) {
|
||||
// Validate checks the validity of the operators contents
|
||||
func (o *Operator) Validate(vr *ValidationResults) {
|
||||
if err := o.validateAccountServerURL(); err != nil {
|
||||
vr.AddError(err.Error())
|
||||
vr.AddError("%s", err.Error())
|
||||
}
|
||||
|
||||
for _, v := range o.validateOperatorServiceURLs() {
|
||||
if v != nil {
|
||||
vr.AddError(v.Error())
|
||||
vr.AddError("%s", v.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-39
@@ -60,6 +60,9 @@ type parser struct {
|
||||
|
||||
// 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
|
||||
@@ -111,42 +114,28 @@ func ParseFileWithChecks(fp string) (map[string]any, error) {
|
||||
return p.mapping, nil
|
||||
}
|
||||
|
||||
// cleanupUsedEnvVars will recursively remove all already used
|
||||
// environment variables which might be in the parsed tree.
|
||||
func cleanupUsedEnvVars(m map[string]any) {
|
||||
for k, v := range m {
|
||||
t := v.(*token)
|
||||
if t.usedVariable {
|
||||
delete(m, k)
|
||||
continue
|
||||
}
|
||||
// Cleanup any other env var that is still in the map.
|
||||
if tm, ok := t.value.(map[string]any); ok {
|
||||
cleanupUsedEnvVars(tm)
|
||||
}
|
||||
// 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) {
|
||||
data, err := os.ReadFile(fp)
|
||||
m, err := ParseFileWithChecks(fp)
|
||||
if err != nil {
|
||||
return nil, _EMPTY_, err
|
||||
}
|
||||
p, err := parse(string(data), fp, true)
|
||||
digest, err := configDigest(m)
|
||||
if err != nil {
|
||||
return nil, _EMPTY_, err
|
||||
}
|
||||
// Filter out any environment variables before taking the digest.
|
||||
cleanupUsedEnvVars(p.mapping)
|
||||
digest := sha256.New()
|
||||
e := json.NewEncoder(digest)
|
||||
err = e.Encode(p.mapping)
|
||||
if err != nil {
|
||||
return nil, _EMPTY_, err
|
||||
}
|
||||
return p.mapping, fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
|
||||
return m, digest, nil
|
||||
}
|
||||
|
||||
type token struct {
|
||||
@@ -180,16 +169,37 @@ func (t *token) Position() int {
|
||||
return t.item.pos
|
||||
}
|
||||
|
||||
func parse(data, fp string, pedantic bool) (p *parser, err error) {
|
||||
p = &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,
|
||||
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
|
||||
@@ -199,16 +209,16 @@ func parse(data, fp string, pedantic bool) (p *parser, err error) {
|
||||
// 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 nil, fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
|
||||
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 nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) next() item {
|
||||
@@ -453,11 +463,18 @@ func (p *parser) lookupVariable(varReference string) (any, bool, error) {
|
||||
}
|
||||
|
||||
// If we are here, we have exhausted our context maps and still not found anything.
|
||||
// Parse from the environment.
|
||||
// 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 vmap, err := Parse(fmt.Sprintf("%s=%s", pkey, vStr)); err == nil {
|
||||
v, ok := vmap[pkey]
|
||||
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
|
||||
|
||||
+10
-10
@@ -235,19 +235,19 @@ func (d *DN) RDNsMatch(other *DN) bool {
|
||||
if len(d.RDNs) != len(other.RDNs) {
|
||||
return false
|
||||
}
|
||||
|
||||
CheckNextRDN:
|
||||
matched := make([]bool, len(other.RDNs))
|
||||
for _, irdn := range d.RDNs {
|
||||
for _, ordn := range other.RDNs {
|
||||
if (len(irdn.Attributes) == len(ordn.Attributes)) &&
|
||||
(irdn.hasAllAttributes(ordn.Attributes) && ordn.hasAllAttributes(irdn.Attributes)) {
|
||||
// Found the RDN, check if next one matches.
|
||||
continue CheckNextRDN
|
||||
found := false
|
||||
for j, ordn := range other.RDNs {
|
||||
if !matched[j] && irdn.Equal(ordn) {
|
||||
matched[j] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Could not find a matching individual RDN, auth fails.
|
||||
return false
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
+21
-10
@@ -138,6 +138,12 @@ type sconns struct {
|
||||
leafs int32
|
||||
}
|
||||
|
||||
// clampInt64ToInt32 safely converts an int64 limit to int32,
|
||||
// clamping values to the [math.MinInt32, math.MaxInt32] range.
|
||||
func clampInt64ToInt32(v int64) int32 {
|
||||
return int32(max(math.MinInt32, min(math.MaxInt32, v)))
|
||||
}
|
||||
|
||||
// Import stream mapping struct
|
||||
type streamImport struct {
|
||||
acc *Account
|
||||
@@ -299,6 +305,7 @@ func (a *Account) shallowCopy(na *Account) {
|
||||
na.Nkey = a.Nkey
|
||||
na.Issuer = a.Issuer
|
||||
na.traceDest, na.traceDestSampling = a.traceDest, a.traceDestSampling
|
||||
na.nrgAccount = a.nrgAccount
|
||||
|
||||
if a.imports.streams != nil {
|
||||
na.imports.streams = make([]*streamImport, 0, len(a.imports.streams))
|
||||
@@ -1609,10 +1616,12 @@ func (a *Account) checkServiceImportsForCycles(from string, visited map[string]b
|
||||
}
|
||||
// Push ourselves and check si.acc
|
||||
visited[a.Name] = true
|
||||
if subjectIsSubsetMatch(si.from, from) {
|
||||
from = si.from
|
||||
// Make a copy to not overwrite the passed value.
|
||||
f := from
|
||||
if subjectIsSubsetMatch(si.from, f) {
|
||||
f = si.from
|
||||
}
|
||||
if err := si.acc.checkServiceImportsForCycles(from, visited); err != nil {
|
||||
if err := si.acc.checkServiceImportsForCycles(f, visited); err != nil {
|
||||
return err
|
||||
}
|
||||
a.mu.RLock()
|
||||
@@ -1667,10 +1676,12 @@ func (a *Account) checkStreamImportsForCycles(to string, visited map[string]bool
|
||||
}
|
||||
// Push ourselves and check si.acc
|
||||
visited[a.Name] = true
|
||||
if subjectIsSubsetMatch(si.to, to) {
|
||||
to = si.to
|
||||
// Make a copy to not overwrite the passed value.
|
||||
t := to
|
||||
if subjectIsSubsetMatch(si.to, t) {
|
||||
t = si.to
|
||||
}
|
||||
if err := si.acc.checkStreamImportsForCycles(to, visited); err != nil {
|
||||
if err := si.acc.checkStreamImportsForCycles(t, visited); err != nil {
|
||||
return err
|
||||
}
|
||||
a.mu.RLock()
|
||||
@@ -3715,10 +3726,10 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
|
||||
// Now do limits if they are present.
|
||||
a.mu.Lock()
|
||||
a.msubs = int32(ac.Limits.Subs)
|
||||
a.mpay = int32(ac.Limits.Payload)
|
||||
a.mconns = int32(ac.Limits.Conn)
|
||||
a.mleafs = int32(ac.Limits.LeafNodeConn)
|
||||
a.msubs = clampInt64ToInt32(ac.Limits.Subs)
|
||||
a.mpay = clampInt64ToInt32(ac.Limits.Payload)
|
||||
a.mconns = clampInt64ToInt32(ac.Limits.Conn)
|
||||
a.mleafs = clampInt64ToInt32(ac.Limits.LeafNodeConn)
|
||||
a.disallowBearer = ac.Limits.DisallowBearer
|
||||
// Check for any revocations
|
||||
if len(ac.Revocations) > 0 {
|
||||
|
||||
+37
-8
@@ -421,7 +421,9 @@ func (c *client) matchesPinnedCert(tlsPinnedCerts PinnedCertSet) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
mustacheRE = regexp.MustCompile(`{{2}([^}]+)}{2}`)
|
||||
mustacheRE = regexp.MustCompile(`{{2}([^}]+)}{2}`)
|
||||
maxPermTemplateSubjectExpansions = 4096
|
||||
errPermTemplateExpansionLimit error = fmt.Errorf("template expansion exceeds limit")
|
||||
)
|
||||
|
||||
func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.UserClaims, acc *Account) (jwt.UserPermissionLimits, error) {
|
||||
@@ -456,11 +458,11 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
|
||||
return p
|
||||
}
|
||||
isTag := func(op string) []string {
|
||||
if strings.EqualFold("tag(", op[:4]) && strings.HasSuffix(op, ")") {
|
||||
if len(op) >= 4 && strings.EqualFold("tag(", op[:4]) && strings.HasSuffix(op, ")") {
|
||||
v := strings.TrimPrefix(op, "tag(")
|
||||
v = strings.TrimSuffix(v, ")")
|
||||
return []string{"tag", v}
|
||||
} else if strings.EqualFold("account-tag(", op[:12]) && strings.HasSuffix(op, ")") {
|
||||
} else if len(op) >= 12 && strings.EqualFold("account-tag(", op[:12]) && strings.HasSuffix(op, ")") {
|
||||
v := strings.TrimPrefix(op, "account-tag(")
|
||||
v = strings.TrimSuffix(v, ")")
|
||||
return []string{"account-tag", v}
|
||||
@@ -529,7 +531,7 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
|
||||
// generate an invalid subject?
|
||||
values[tokenNum] = []string{" "}
|
||||
}
|
||||
} else if failOnBadSubject {
|
||||
} else {
|
||||
return nil, fmt.Errorf("template operation in %q: %q is not defined", list[i], op)
|
||||
}
|
||||
}
|
||||
@@ -544,6 +546,20 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
|
||||
return nil, fmt.Errorf("generated invalid subject")
|
||||
}
|
||||
} else {
|
||||
expCount := 1
|
||||
for _, v := range values {
|
||||
if len(v) == 0 {
|
||||
expCount = 0
|
||||
break
|
||||
}
|
||||
if expCount > maxPermTemplateSubjectExpansions/len(v) {
|
||||
return nil, fmt.Errorf("%w: %d", errPermTemplateExpansionLimit, maxPermTemplateSubjectExpansions)
|
||||
}
|
||||
expCount *= len(v)
|
||||
}
|
||||
if len(emittedList) > maxPermTemplateSubjectExpansions-expCount {
|
||||
return nil, fmt.Errorf("%w: %d", errPermTemplateExpansionLimit, maxPermTemplateSubjectExpansions)
|
||||
}
|
||||
a := nArrayCartesianProduct(values...)
|
||||
for _, aa := range a {
|
||||
subj := list[i]
|
||||
@@ -588,6 +604,7 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
|
||||
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (authorized bool) {
|
||||
var (
|
||||
nkey *NkeyUser
|
||||
ujwt string
|
||||
juc *jwt.UserClaims
|
||||
acc *Account
|
||||
user *User
|
||||
@@ -782,16 +799,23 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
|
||||
// Check if we have trustedKeys defined in the server. If so we require a user jwt.
|
||||
if s.trustedKeys != nil {
|
||||
if c.opts.JWT == _EMPTY_ && opts.DefaultSentinel != _EMPTY_ {
|
||||
c.opts.JWT = opts.DefaultSentinel
|
||||
ujwt = c.opts.JWT
|
||||
if ujwt == _EMPTY_ && c.isMqtt() {
|
||||
// For MQTT, we pass the password as the JWT too, but do so here so it's not
|
||||
// publicly exposed in the client options if it isn't a JWT.
|
||||
ujwt = c.opts.Password
|
||||
}
|
||||
if c.opts.JWT == _EMPTY_ {
|
||||
if ujwt == _EMPTY_ && opts.DefaultSentinel != _EMPTY_ {
|
||||
c.opts.JWT = opts.DefaultSentinel
|
||||
ujwt = c.opts.JWT
|
||||
}
|
||||
if ujwt == _EMPTY_ {
|
||||
s.mu.Unlock()
|
||||
c.Debugf("Authentication requires a user JWT")
|
||||
return false
|
||||
}
|
||||
// So we have a valid user jwt here.
|
||||
juc, err = jwt.DecodeUserClaims(c.opts.JWT)
|
||||
juc, err = jwt.DecodeUserClaims(ujwt)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
c.Debugf("User JWT not valid: %v", err)
|
||||
@@ -1061,6 +1085,11 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
// Hold onto the user's public key.
|
||||
c.mu.Lock()
|
||||
c.pubKey = juc.Subject
|
||||
// If this is a MQTT client, we purposefully didn't populate the JWT as it could contain
|
||||
// a password or token. Now we know it's a valid JWT, we can populate it.
|
||||
if c.isMqtt() {
|
||||
c.opts.JWT = ujwt
|
||||
}
|
||||
c.tags = juc.Tags
|
||||
c.nameTag = juc.Name
|
||||
c.mu.Unlock()
|
||||
|
||||
+15
-8
@@ -32,6 +32,14 @@ const (
|
||||
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) (authorized bool, errStr string) {
|
||||
isOperatorMode := len(opts.TrustedKeys) > 0
|
||||
@@ -50,6 +58,13 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequi
|
||||
} 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
|
||||
@@ -66,9 +81,6 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequi
|
||||
xkp, xkey = s.xkp, s.info.XKey
|
||||
}
|
||||
|
||||
// FIXME: so things like the server ID that get assigned, are used as a sort of nonce - but
|
||||
// reality is that the keypair here, is generated, so the response generated a JWT has to be
|
||||
// this user - no replay possible
|
||||
// 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()
|
||||
@@ -234,11 +246,6 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequi
|
||||
}
|
||||
|
||||
processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
|
||||
titleCase := func(m string) string {
|
||||
r := []rune(m)
|
||||
return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
|
||||
}
|
||||
|
||||
arc, err := decodeResponse(rc, rmsg, racc)
|
||||
if err != nil {
|
||||
c.authViolation()
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023-2024 The NATS Authors
|
||||
// Copyright 2023-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
|
||||
+254
-116
@@ -1,4 +1,4 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// 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
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -35,8 +36,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/klauspost/compress/s2"
|
||||
"github.com/nats-io/jwt/v2"
|
||||
"github.com/nats-io/nats-server/v2/internal/fastrand"
|
||||
@@ -154,7 +153,6 @@ const (
|
||||
compressionNegotiated // Marks if this connection has negotiated compression level with remote.
|
||||
didTLSFirst // Marks if this connection requested and was accepted doing the TLS handshake first (prior to INFO).
|
||||
isSlowConsumer // Marks connection as a slow consumer.
|
||||
firstPong // Marks if this is the first PONG received
|
||||
)
|
||||
|
||||
// set the flag (would be equivalent to set the boolean to true)
|
||||
@@ -870,6 +868,11 @@ func (c *client) registerWithAccount(acc *Account) error {
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
// This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...)
|
||||
if c.isClosed() && !isInternalClient(c.kind) {
|
||||
c.mu.Unlock()
|
||||
return ErrConnectionClosed
|
||||
}
|
||||
kind := c.kind
|
||||
srv := c.srv
|
||||
c.acc = acc
|
||||
@@ -929,14 +932,14 @@ func (c *client) applyAccountLimits() {
|
||||
c.msubs = jwt.NoLimit
|
||||
if c.opts.JWT != _EMPTY_ { // user jwt implies account
|
||||
if uc, _ := jwt.DecodeUserClaims(c.opts.JWT); uc != nil {
|
||||
atomic.StoreInt32(&c.mpay, int32(uc.Limits.Payload))
|
||||
c.msubs = int32(uc.Limits.Subs)
|
||||
atomic.StoreInt32(&c.mpay, clampInt64ToInt32(uc.Limits.Payload))
|
||||
c.msubs = clampInt64ToInt32(uc.Limits.Subs)
|
||||
if uc.IssuerAccount != _EMPTY_ && uc.IssuerAccount != uc.Issuer {
|
||||
if scope, ok := c.acc.signingKeys[uc.Issuer]; ok {
|
||||
if userScope, ok := scope.(*jwt.UserScope); ok {
|
||||
// if signing key disappeared or changed and we don't get here, the client will be disconnected
|
||||
c.mpay = int32(userScope.Template.Limits.Payload)
|
||||
c.msubs = int32(userScope.Template.Limits.Subs)
|
||||
c.mpay = clampInt64ToInt32(userScope.Template.Limits.Payload)
|
||||
c.msubs = clampInt64ToInt32(userScope.Template.Limits.Subs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1354,6 +1357,13 @@ func (c *client) flushClients(budget time.Duration) time.Time {
|
||||
return last
|
||||
}
|
||||
|
||||
func (c *client) resetReadLoopStallTime() {
|
||||
if c.in.tst >= stallClientMaxDuration {
|
||||
c.rateLimitFormatWarnf("Producer was stalled for a total of %v", c.in.tst.Round(time.Millisecond))
|
||||
}
|
||||
c.in.tst = 0
|
||||
}
|
||||
|
||||
// readLoop is the main socket read functionality.
|
||||
// Runs in its own Go routine.
|
||||
func (c *client) readLoop(pre []byte) {
|
||||
@@ -1431,21 +1441,6 @@ func (c *client) readLoop(pre []byte) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if ws {
|
||||
bufs, err = c.wsRead(wsr, reader, b[:n])
|
||||
if bufs == nil && err != nil {
|
||||
if err != io.EOF {
|
||||
c.Errorf("read error: %v", err)
|
||||
}
|
||||
c.closeConnection(closedStateForErr(err))
|
||||
return
|
||||
} else if bufs == nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
bufs[0] = b[:n]
|
||||
}
|
||||
|
||||
// Check if the account has mappings and if so set the local readcache flag.
|
||||
// We check here to make sure any changes such as config reload are reflected here.
|
||||
if c.kind == CLIENT || c.kind == LEAF {
|
||||
@@ -1463,17 +1458,32 @@ func (c *client) readLoop(pre []byte) {
|
||||
c.in.bytes = 0
|
||||
c.in.subs = 0
|
||||
|
||||
if ws {
|
||||
err = c.wsReadAndParse(wsr, reader, b[:n])
|
||||
if err != nil {
|
||||
// Match the normal parse path: any already-buffered deliveries
|
||||
// need their pending flush signals drained before we close.
|
||||
c.flushClients(0)
|
||||
if err != io.EOF {
|
||||
c.Errorf("read error: %v", err)
|
||||
}
|
||||
c.closeConnection(closedStateForErr(err))
|
||||
return
|
||||
}
|
||||
c.resetReadLoopStallTime()
|
||||
goto postParse
|
||||
} else {
|
||||
bufs[0] = b[:n]
|
||||
}
|
||||
|
||||
// Main call into parser for inbound data. This will generate callouts
|
||||
// to process messages, etc.
|
||||
for i := 0; i < len(bufs); i++ {
|
||||
if err := c.parse(bufs[i]); err != nil {
|
||||
if err == ErrMinimumVersionRequired {
|
||||
// Special case here, currently only for leaf node connections.
|
||||
// When process the CONNECT protocol, if the minimum version
|
||||
// required was not met, an error was printed and sent back to
|
||||
// the remote, and connection was closed after a certain delay
|
||||
// (to avoid "rapid" reconnection from the remote).
|
||||
// We don't need to do any of the things below, simply return.
|
||||
// processLeafConnect() already sent the rejection and closed
|
||||
// the connection, so there is nothing else to do here.
|
||||
return
|
||||
}
|
||||
if dur := time.Since(c.in.start); dur >= readLoopReportThreshold {
|
||||
@@ -1490,13 +1500,10 @@ func (c *client) readLoop(pre []byte) {
|
||||
}
|
||||
return
|
||||
}
|
||||
// Clear total stalled time here.
|
||||
if c.in.tst >= stallClientMaxDuration {
|
||||
c.rateLimitFormatWarnf("Producer was stalled for a total of %v", c.in.tst.Round(time.Millisecond))
|
||||
}
|
||||
c.in.tst = 0
|
||||
c.resetReadLoopStallTime()
|
||||
}
|
||||
|
||||
postParse:
|
||||
// If we are a ROUTER/LEAF and have processed an INFO, it is possible that
|
||||
// we are asked to switch to compression now.
|
||||
if checkCompress && c.in.flags.isSet(switchToCompression) {
|
||||
@@ -1687,9 +1694,11 @@ func (c *client) flushOutbound() bool {
|
||||
|
||||
cw.Reset(&bb)
|
||||
for _, buf := range collapsed {
|
||||
if _, err = cw.Write(buf); err != nil {
|
||||
break
|
||||
if err == nil {
|
||||
_, err = cw.Write(buf)
|
||||
}
|
||||
// Return always after consumed or error.
|
||||
nbPoolPut(buf)
|
||||
}
|
||||
if err == nil {
|
||||
err = cw.Close()
|
||||
@@ -2114,41 +2123,37 @@ func (c *client) processErr(errStr string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Password pattern matcher.
|
||||
var passPat = regexp.MustCompile(`"?\s*pass\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)`)
|
||||
var tokenPat = regexp.MustCompile(`"?\s*auth_token\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)`)
|
||||
// Matcher for pass/password and auth_token fields.
|
||||
var prefixAuthPat = regexp.MustCompile(`"?\s*(?:auth_token\S*?|pass\S*?)"?\s*[:=]\s*"?([^",\r\n}]*)`)
|
||||
|
||||
// Exact matcher for fields sig, proxy_sig and nkey.
|
||||
// Overlapping field "sig" does not match inside "proxy_sig".
|
||||
var exactAuthPat = regexp.MustCompile(`(?:^|[^A-Za-z0-9_])"?\s*(?:proxy_sig|nkey|sig)"?\s*[:=]\s*"?([^",\r\n}]*)`)
|
||||
|
||||
// removeSecretsFromTrace removes any notion of passwords/tokens from trace
|
||||
// messages for logging.
|
||||
func removeSecretsFromTrace(arg []byte) []byte {
|
||||
buf := redact("pass", passPat, arg)
|
||||
return redact("auth_token", tokenPat, buf)
|
||||
buf := redact(prefixAuthPat, arg)
|
||||
return redact(exactAuthPat, buf)
|
||||
}
|
||||
|
||||
func redact(name string, pat *regexp.Regexp, proto []byte) []byte {
|
||||
if !bytes.Contains(proto, []byte(name)) {
|
||||
func redact(pat *regexp.Regexp, proto []byte) []byte {
|
||||
m := pat.FindAllSubmatchIndex(proto, -1)
|
||||
if len(m) == 0 {
|
||||
return proto
|
||||
}
|
||||
// Take a copy of the connect proto just for the trace message.
|
||||
var _arg [4096]byte
|
||||
buf := append(_arg[:0], proto...)
|
||||
|
||||
m := pat.FindAllSubmatchIndex(buf, -1)
|
||||
if len(m) == 0 {
|
||||
return proto
|
||||
}
|
||||
|
||||
redactedPass := []byte("[REDACTED]")
|
||||
for _, i := range m {
|
||||
if len(i) < 4 {
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
match := m[i]
|
||||
if len(match) < 4 {
|
||||
continue
|
||||
}
|
||||
start := i[2]
|
||||
end := i[3]
|
||||
|
||||
start, end := match[2], match[3]
|
||||
// Replace value substring.
|
||||
buf = append(buf[:start], append(redactedPass, buf[end:]...)...)
|
||||
break
|
||||
}
|
||||
return buf
|
||||
}
|
||||
@@ -2684,11 +2689,9 @@ func (c *client) processPong() {
|
||||
c.rtt = computeRTT(c.rttStart)
|
||||
srv := c.srv
|
||||
reorderGWs := c.kind == GATEWAY && c.gw.outbound
|
||||
firstPong := c.flags.setIfNotSet(firstPong)
|
||||
var ri *routeInfo
|
||||
// When receiving the first PONG, for a route with pooling, we may be
|
||||
// instructed to start a new route.
|
||||
if firstPong && c.kind == ROUTER && c.route != nil {
|
||||
// For a route with pooling, we may be instructed to start a new route.
|
||||
if c.kind == ROUTER && c.route != nil && c.route.startNewRoute != nil {
|
||||
ri = c.route.startNewRoute
|
||||
c.route.startNewRoute = nil
|
||||
}
|
||||
@@ -2732,9 +2735,12 @@ func (c *client) updateS2AutoCompressionLevel(co *CompressionOpts, compression *
|
||||
}
|
||||
|
||||
// Will return the parts from the raw wire msg.
|
||||
// We return the `hdr` as a slice that is capped to the length of the headers
|
||||
// so that if the caller later tries to append to the returned header slice it
|
||||
// does not affect the message content.
|
||||
func (c *client) msgParts(data []byte) (hdr []byte, msg []byte) {
|
||||
if c != nil && c.pa.hdr > 0 {
|
||||
return data[:c.pa.hdr], data[c.pa.hdr:]
|
||||
return data[:c.pa.hdr:c.pa.hdr], data[c.pa.hdr:]
|
||||
}
|
||||
return nil, data
|
||||
}
|
||||
@@ -2803,9 +2809,10 @@ func (c *client) processHeaderPub(arg, remaining []byte) error {
|
||||
// look for the tracing header and if found, we will generate a
|
||||
// trace event with the max payload ingress error.
|
||||
// Do this only for CLIENT connections.
|
||||
if c.kind == CLIENT && len(remaining) > 0 {
|
||||
if td := getHeader(MsgTraceDest, remaining); len(td) > 0 {
|
||||
c.initAndSendIngressErrEvent(remaining, string(td), ErrMaxPayload)
|
||||
if c.kind == CLIENT && c.pa.hdr > 0 && len(remaining) > 0 {
|
||||
hdr := remaining[:min(len(remaining), c.pa.hdr)]
|
||||
if td, ok := c.allowedMsgTraceDest(hdr, false); ok && td != _EMPTY_ {
|
||||
c.initAndSendIngressErrEvent(hdr, td, ErrMaxPayload)
|
||||
}
|
||||
}
|
||||
c.maxPayloadViolation(c.pa.size, maxPayload)
|
||||
@@ -3022,7 +3029,7 @@ func (c *client) processSubEx(subject, queue, bsid []byte, cb msgHandler, noForw
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
if err := c.addShadowSubscriptions(acc, sub, true); err != nil {
|
||||
if err := c.addShadowSubscriptions(acc, sub); err != nil {
|
||||
c.Errorf(err.Error())
|
||||
}
|
||||
|
||||
@@ -3052,10 +3059,7 @@ type ime struct {
|
||||
// If the client's account has stream imports and there are matches for this
|
||||
// subscription's subject, then add shadow subscriptions in the other accounts
|
||||
// that export this subject.
|
||||
//
|
||||
// enact=false allows MQTT clients to get the list of shadow subscriptions
|
||||
// without enacting them, in order to first obtain matching "retained" messages.
|
||||
func (c *client) addShadowSubscriptions(acc *Account, sub *subscription, enact bool) error {
|
||||
func (c *client) addShadowSubscriptions(acc *Account, sub *subscription) error {
|
||||
if acc == nil {
|
||||
return ErrMissingAccount
|
||||
}
|
||||
@@ -3158,7 +3162,7 @@ func (c *client) addShadowSubscriptions(acc *Account, sub *subscription, enact b
|
||||
for i := 0; i < len(ims); i++ {
|
||||
ime := &ims[i]
|
||||
// We will create a shadow subscription.
|
||||
nsub, err := c.addShadowSub(sub, ime, enact)
|
||||
nsub, err := c.addShadowSub(sub, ime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -3175,7 +3179,7 @@ func (c *client) addShadowSubscriptions(acc *Account, sub *subscription, enact b
|
||||
}
|
||||
|
||||
// Add in the shadow subscription.
|
||||
func (c *client) addShadowSub(sub *subscription, ime *ime, enact bool) (*subscription, error) {
|
||||
func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error) {
|
||||
c.mu.Lock()
|
||||
nsub := *sub // copy
|
||||
c.mu.Unlock()
|
||||
@@ -3203,10 +3207,6 @@ func (c *client) addShadowSub(sub *subscription, ime *ime, enact bool) (*subscri
|
||||
}
|
||||
// Else use original subject
|
||||
|
||||
if !enact {
|
||||
return &nsub, nil
|
||||
}
|
||||
|
||||
c.Debugf("Creating import subscription on %q from account %q", nsub.subject, im.acc.Name)
|
||||
|
||||
if err := im.acc.sl.Insert(&nsub); err != nil {
|
||||
@@ -3237,7 +3237,7 @@ func (c *client) canSubscribe(subject string, optQueue ...string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
allowed := true
|
||||
allowed, checkAllow := true, true
|
||||
|
||||
// Optional queue group.
|
||||
var queue string
|
||||
@@ -3245,8 +3245,16 @@ func (c *client) canSubscribe(subject string, optQueue ...string) bool {
|
||||
queue = optQueue[0]
|
||||
}
|
||||
|
||||
// For CLIENT connections that are MQTT we will implicitly allow anything that starts with
|
||||
// the "$MQTT.sub." or "$MQTT.deliver.pubrel." prefix. For other types of connections, we
|
||||
// will implicitly allow anything that starts with the full "$MQTT." prefix. However,
|
||||
// we don't just return here, we skip the check for "allow" but will check "deny".
|
||||
if (c.isMqtt() && (strings.HasPrefix(subject, mqttSubPrefix) || strings.HasPrefix(subject, mqttPubRelDeliverySubjectPrefix))) ||
|
||||
(c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix)) {
|
||||
checkAllow = false
|
||||
}
|
||||
// Check allow list. If no allow list that means all are allowed. Deny can overrule.
|
||||
if c.perms.sub.allow != nil {
|
||||
if checkAllow && c.perms.sub.allow != nil {
|
||||
r := c.perms.sub.allow.Match(subject)
|
||||
allowed = len(r.psubs) > 0
|
||||
if queue != _EMPTY_ && len(r.qsubs) > 0 {
|
||||
@@ -3338,7 +3346,7 @@ func (c *client) unsubscribe(acc *Account, sub *subscription, force, remove bool
|
||||
sub.shadow = nil
|
||||
if len(shadowSubs) > 0 {
|
||||
isSpokeLeaf = c.isSpokeLeafNode()
|
||||
updateRoute = !isSpokeLeaf && (c.kind == CLIENT || c.kind == SYSTEM || c.kind == LEAF) && c.srv != nil
|
||||
updateRoute = !isSpokeLeaf && (c.kind == CLIENT || c.kind == SYSTEM || c.kind == LEAF || c.kind == JETSTREAM) && c.srv != nil
|
||||
}
|
||||
sub.close()
|
||||
c.mu.Unlock()
|
||||
@@ -4052,6 +4060,41 @@ func (c *client) pubAllowed(subject string) bool {
|
||||
return c.pubAllowedFullCheck(subject, true, false)
|
||||
}
|
||||
|
||||
// allowedMsgTraceDest returns the trace destination if present and authorized.
|
||||
// It only considers static publish permissions and does not consume dynamic
|
||||
// reply permissions because the client is not publishing the trace event itself.
|
||||
func (c *client) allowedMsgTraceDest(hdr []byte, hasLock bool) (string, bool) {
|
||||
if len(hdr) == 0 {
|
||||
return _EMPTY_, true
|
||||
}
|
||||
td := sliceHeader(MsgTraceDest, hdr)
|
||||
if len(td) == 0 {
|
||||
return _EMPTY_, true
|
||||
}
|
||||
dest := bytesToString(td)
|
||||
if c.kind == CLIENT {
|
||||
if hasGWRoutedReplyPrefix(td) {
|
||||
return dest, false
|
||||
}
|
||||
var acc *Account
|
||||
var srv *Server
|
||||
if !hasLock {
|
||||
c.mu.Lock()
|
||||
}
|
||||
acc, srv = c.acc, c.srv
|
||||
if !hasLock {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
if bytes.HasPrefix(td, clientNRGPrefix) && srv != nil && acc != srv.SystemAccount() {
|
||||
return dest, false
|
||||
}
|
||||
}
|
||||
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowedFullCheck(dest, false, hasLock) {
|
||||
return dest, false
|
||||
}
|
||||
return dest, true
|
||||
}
|
||||
|
||||
// pubAllowedFullCheck checks on all publish permissioning depending
|
||||
// on the flag for dynamic reply permissions.
|
||||
func (c *client) pubAllowedFullCheck(subject string, fullCheck, hasLock bool) bool {
|
||||
@@ -4063,9 +4106,15 @@ func (c *client) pubAllowedFullCheck(subject string, fullCheck, hasLock bool) bo
|
||||
if ok {
|
||||
return v.(bool)
|
||||
}
|
||||
allowed := true
|
||||
allowed, checkAllow := true, true
|
||||
// For any connections, other than CLIENT, we will implicitly allow anything that
|
||||
// starts with the "$MQTT." prefix. However, we don't just return here,
|
||||
// we skip the check for "allow" but will check "deny".
|
||||
if c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix) {
|
||||
checkAllow = false
|
||||
}
|
||||
// Cache miss, check allow then deny as needed.
|
||||
if c.perms.pub.allow != nil {
|
||||
if checkAllow && c.perms.pub.allow != nil {
|
||||
np, _ := c.perms.pub.allow.NumInterest(subject)
|
||||
allowed = np != 0
|
||||
}
|
||||
@@ -4181,10 +4230,19 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
|
||||
genidAddr := &acc.sl.genid
|
||||
|
||||
// Check pub permissions
|
||||
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowedFullCheck(string(c.pa.subject), true, true) {
|
||||
c.mu.Unlock()
|
||||
c.pubPermissionViolation(c.pa.subject)
|
||||
return false, true
|
||||
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) {
|
||||
if !c.pubAllowedFullCheck(string(c.pa.subject), true, true) {
|
||||
c.mu.Unlock()
|
||||
c.pubPermissionViolation(c.pa.subject)
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
if c.pa.hdr > 0 {
|
||||
if td, ok := c.allowedMsgTraceDest(msg[:c.pa.hdr], true); !ok {
|
||||
c.mu.Unlock()
|
||||
c.pubPermissionViolation(stringToBytes(td))
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -4384,28 +4442,43 @@ func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tra
|
||||
return rsi
|
||||
}
|
||||
|
||||
// Will remove a header if present.
|
||||
func removeHeaderIfPresent(hdr []byte, key string) []byte {
|
||||
start := getHeaderKeyIndex(key, hdr)
|
||||
// key can't be first and we want to check that it is preceded by a '\n'
|
||||
if start < 1 || hdr[start-1] != '\n' {
|
||||
// Will remove a status and description from the header if present.
|
||||
func removeHeaderStatusIfPresent(hdr []byte) []byte {
|
||||
k := []byte("NATS/1.0")
|
||||
kl, i := len(k), bytes.IndexByte(hdr, '\r')
|
||||
if !bytes.HasPrefix(hdr, k) || i <= kl {
|
||||
return hdr
|
||||
}
|
||||
index := start + len(key)
|
||||
if index >= len(hdr) || hdr[index] != ':' {
|
||||
return hdr
|
||||
}
|
||||
end := bytes.Index(hdr[start:], []byte(_CRLF_))
|
||||
if end < 0 {
|
||||
return hdr
|
||||
}
|
||||
hdr = append(hdr[:start], hdr[start+end+len(_CRLF_):]...)
|
||||
if len(hdr) <= len(emptyHdrLine) {
|
||||
hdr = append(hdr[:kl], hdr[i:]...)
|
||||
if len(hdr) == len(emptyHdrLine) {
|
||||
return nil
|
||||
}
|
||||
return hdr
|
||||
}
|
||||
|
||||
// Will remove a header if present.
|
||||
func removeHeaderIfPresent(hdr []byte, key string) []byte {
|
||||
for {
|
||||
start := getHeaderKeyIndex(key, hdr)
|
||||
// key can't be first and we want to check that it is preceded by a '\n'
|
||||
if start < 1 || hdr[start-1] != '\n' {
|
||||
return hdr
|
||||
}
|
||||
index := start + len(key)
|
||||
if index >= len(hdr) || hdr[index] != ':' {
|
||||
return hdr
|
||||
}
|
||||
end := bytes.Index(hdr[start:], []byte(_CRLF_))
|
||||
if end < 0 {
|
||||
return hdr
|
||||
}
|
||||
hdr = append(hdr[:start], hdr[start+end+len(_CRLF_):]...)
|
||||
if len(hdr) <= len(emptyHdrLine) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeHeaderIfPrefixPresent(hdr []byte, prefix string) []byte {
|
||||
var index int
|
||||
for {
|
||||
@@ -4560,6 +4633,19 @@ func getHeaderKeyIndex(key string, hdr []byte) int {
|
||||
}
|
||||
}
|
||||
|
||||
// setHeader will replace the value of the first existing key `key`
|
||||
// with the given value `val`, or add this new key at the end of
|
||||
// the headers.
|
||||
//
|
||||
// Note: If the key does not exist, or if it exists but the new value
|
||||
// would make the resulting byte slice larger than the original one,
|
||||
// a new byte slice is returned and the original is left untouched.
|
||||
// This is to prevent situations where caller may have a `hdr` and
|
||||
// `msg` that are the parts of an underlying buffer. Extending the
|
||||
// `hdr` would otherwise overwrite the `msg` part.
|
||||
//
|
||||
// If the new value is smaller, then the original `hdr` byte slice
|
||||
// is modified.
|
||||
func setHeader(key, val string, hdr []byte) []byte {
|
||||
start := getHeaderKeyIndex(key, hdr)
|
||||
if start >= 0 {
|
||||
@@ -4574,15 +4660,45 @@ func setHeader(key, val string, hdr []byte) []byte {
|
||||
return hdr // malformed headers
|
||||
}
|
||||
valEnd += valStart
|
||||
suffix := slices.Clone(hdr[valEnd:])
|
||||
newHdr := append(hdr[:valStart], val...)
|
||||
return append(newHdr, suffix...)
|
||||
// Length of the existing value (before the `\r`)
|
||||
oldValLen := valEnd - valStart
|
||||
// This is how many extra bytes we need for the new value.
|
||||
// If <= 0, it means that we need less and so will reuse the `hdr` buffer.
|
||||
if extra := len(val) - oldValLen; extra > 0 {
|
||||
// Check that we don't overflow an "int".
|
||||
if rem := math.MaxInt - hdrLen; rem < extra {
|
||||
// We don't grow, and return the existing header.
|
||||
return hdr
|
||||
}
|
||||
// The new size is the old size plus the extra bytes.
|
||||
newHdrSize := hdrLen + extra
|
||||
newHdr := make([]byte, newHdrSize)
|
||||
// Copy the parts from `hdr` and `val` into the new buffer.
|
||||
n := copy(newHdr, hdr[:valStart])
|
||||
n += copy(newHdr[n:], val)
|
||||
copy(newHdr[n:], hdr[valEnd:])
|
||||
return newHdr
|
||||
}
|
||||
// We can write in place since it fits in the existing `hdr` buffer.
|
||||
n := copy(hdr[valStart:], val)
|
||||
n += copy(hdr[valStart+n:], hdr[valEnd:])
|
||||
hdr = hdr[:valStart+n]
|
||||
return hdr
|
||||
}
|
||||
if len(hdr) > 0 && bytes.HasSuffix(hdr, []byte("\r\n")) {
|
||||
hdr = hdr[:len(hdr)-2]
|
||||
val += "\r\n"
|
||||
}
|
||||
return fmt.Appendf(hdr, "%s: %s\r\n", key, val)
|
||||
// Create the new buffer based on length of existing one and
|
||||
// length of the new "<key>: <value>\r\n". Protect against "int" overflow.
|
||||
newSize := uint64(len(hdr)) + uint64(len(key)) + 1 + 1 + uint64(len(val)) + 2
|
||||
if newSize > uint64(math.MaxInt) {
|
||||
// We don't grow, and return the existing header.
|
||||
return hdr
|
||||
}
|
||||
newHdr := make([]byte, 0, int(newSize))
|
||||
newHdr = append(newHdr, hdr...)
|
||||
return fmt.Appendf(newHdr, "%s: %s\r\n", key, val)
|
||||
}
|
||||
|
||||
// For bytes.HasPrefix below.
|
||||
@@ -4697,16 +4813,33 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
|
||||
if !isResponse {
|
||||
isSysImport := siAcc == c.srv.SystemAccount()
|
||||
var ci *ClientInfo
|
||||
if hadPrevSi && c.pa.hdr >= 0 {
|
||||
var cis ClientInfo
|
||||
if err := json.Unmarshal(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr]), &cis); err == nil {
|
||||
ci = &cis
|
||||
var cis *ClientInfo
|
||||
if c.pa.hdr >= 0 {
|
||||
var hci ClientInfo
|
||||
if err := json.Unmarshal(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr]), &hci); err == nil {
|
||||
cis = &hci
|
||||
}
|
||||
}
|
||||
if c.kind == LEAF && c.pa.hdr >= 0 && len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) > 0 {
|
||||
// Leaf nodes may forward a Nats-Request-Info from a remote domain,
|
||||
// but the local server must replace it with the identity of the
|
||||
// authenticated leaf connection instead of trusting forwarded values.
|
||||
ci = c.getClientInfo(share)
|
||||
if hadPrevSi {
|
||||
ci.Service = acc.Name
|
||||
// Check if we are moving into a share details account from a non-shared
|
||||
// and add in server and cluster details.
|
||||
if !share && (si.share || isSysImport) {
|
||||
c.addServerAndClusterInfo(ci)
|
||||
}
|
||||
} else if !share && isSysImport {
|
||||
c.addServerAndClusterInfo(ci)
|
||||
}
|
||||
} else if hadPrevSi && cis != nil {
|
||||
ci = cis
|
||||
ci.Service = acc.Name
|
||||
// Check if we are moving into a share details account from a non-shared
|
||||
// and add in server and cluster details.
|
||||
if !share && (si.share || isSysImport) {
|
||||
c.addServerAndClusterInfo(ci)
|
||||
}
|
||||
} else if c.kind != LEAF || c.pa.hdr < 0 || len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) == 0 {
|
||||
ci = c.getClientInfo(share)
|
||||
@@ -4714,12 +4847,6 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
|
||||
if !share && isSysImport {
|
||||
c.addServerAndClusterInfo(ci)
|
||||
}
|
||||
} else if c.kind == LEAF && (si.share || isSysImport) {
|
||||
// We have a leaf header here for ci, augment as above.
|
||||
ci = c.getClientInfo(si.share)
|
||||
if !si.share && isSysImport {
|
||||
c.addServerAndClusterInfo(ci)
|
||||
}
|
||||
}
|
||||
// Set clientInfo if present.
|
||||
if ci != nil {
|
||||
@@ -5341,8 +5468,10 @@ sendToRoutesOrLeafs:
|
||||
// If we do have a deliver subject we need to do something with it.
|
||||
// Again this is when JetStream (but possibly others) wants the system
|
||||
// to rewrite the delivered subject. The way we will do that is place it
|
||||
// at the end of the reply subject if it exists.
|
||||
if len(deliver) > 0 && len(reply) > 0 {
|
||||
// at the end of the reply subject if it exists. But only if this wasn't
|
||||
// already performed, otherwise we'd end up with a duplicate '@' suffix
|
||||
// resulting in a protocol error.
|
||||
if len(deliver) > 0 && len(reply) > 0 && !remapped {
|
||||
reply = append(reply, '@')
|
||||
reply = append(reply, deliver...)
|
||||
}
|
||||
@@ -5501,6 +5630,9 @@ func (c *client) processPingTimer() {
|
||||
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
||||
pingInterval = opts.Cluster.PingInterval
|
||||
}
|
||||
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
||||
pingInterval = opts.Websocket.PingInterval
|
||||
}
|
||||
pingInterval = adjustPingInterval(c.kind, pingInterval)
|
||||
now := time.Now()
|
||||
needRTT := c.rtt == 0 || now.Sub(c.rttStart) > DEFAULT_RTT_MEASUREMENT_INTERVAL
|
||||
@@ -5583,6 +5715,9 @@ func (c *client) setPingTimer() {
|
||||
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
||||
d = opts.Cluster.PingInterval
|
||||
}
|
||||
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
||||
d = opts.Websocket.PingInterval
|
||||
}
|
||||
d = adjustPingInterval(c.kind, d)
|
||||
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
|
||||
}
|
||||
@@ -5788,7 +5923,7 @@ func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) {
|
||||
oldShadows := sub.shadow
|
||||
sub.shadow = nil
|
||||
c.mu.Unlock()
|
||||
c.addShadowSubscriptions(acc, sub, true)
|
||||
c.addShadowSubscriptions(acc, sub)
|
||||
for _, nsub := range oldShadows {
|
||||
nsub.im.acc.sl.Remove(nsub)
|
||||
}
|
||||
@@ -6617,6 +6752,9 @@ func (c *client) setFirstPingTimer() {
|
||||
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
||||
d = opts.Cluster.PingInterval
|
||||
}
|
||||
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
||||
d = opts.Websocket.PingInterval
|
||||
}
|
||||
if !opts.DisableShortFirstPing {
|
||||
if c.kind != CLIENT {
|
||||
if d > firstPingInterval {
|
||||
|
||||
+29
-22
@@ -131,19 +131,22 @@ func detectProxyProtoVersion(conn net.Conn) (version int, header []byte, err err
|
||||
|
||||
// readProxyProtoV1Header parses PROXY protocol v1 text format.
|
||||
// Expects the "PROXY " prefix (6 bytes) to have already been consumed.
|
||||
func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, error) {
|
||||
// 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, fmt.Errorf("failed to read v1 line: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to read v1 line: %w", err)
|
||||
}
|
||||
|
||||
line = buf[:len(line)+n]
|
||||
@@ -151,7 +154,8 @@ func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, error) {
|
||||
// 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 - extract just the line portion
|
||||
// Found CRLF - keep any over-read bytes for the client parser.
|
||||
remaining = append(remaining, line[i+2:]...)
|
||||
line = line[:i]
|
||||
goto foundCRLF
|
||||
}
|
||||
@@ -159,7 +163,7 @@ func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, error) {
|
||||
}
|
||||
|
||||
// Exceeded max length without finding CRLF
|
||||
return nil, fmt.Errorf("%w: v1 line too long", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: v1 line too long", errProxyProtoInvalid)
|
||||
|
||||
foundCRLF:
|
||||
// Get parts from the protocol
|
||||
@@ -167,17 +171,17 @@ foundCRLF:
|
||||
|
||||
// Validate format
|
||||
if len(parts) < 1 {
|
||||
return nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||
}
|
||||
|
||||
// Handle UNKNOWN (health check, like v2 LOCAL)
|
||||
if parts[0] == proxyProtoV1Unknown {
|
||||
return nil, nil
|
||||
return nil, remaining, nil
|
||||
}
|
||||
|
||||
// Must have exactly 5 parts: protocol, src-ip, dst-ip, src-port, dst-port
|
||||
if len(parts) != 5 {
|
||||
return nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
|
||||
}
|
||||
|
||||
protocol := parts[0]
|
||||
@@ -185,29 +189,29 @@ foundCRLF:
|
||||
dstIP := net.ParseIP(parts[2])
|
||||
|
||||
if srcIP == nil || dstIP == nil {
|
||||
return nil, fmt.Errorf("%w: invalid address", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: invalid address", errProxyProtoInvalid)
|
||||
}
|
||||
|
||||
// Parse ports
|
||||
srcPort, err := strconv.ParseUint(parts[3], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid source port: %w", err)
|
||||
return nil, nil, fmt.Errorf("invalid source port: %w", err)
|
||||
}
|
||||
|
||||
dstPort, err := strconv.ParseUint(parts[4], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid dest port: %w", err)
|
||||
return nil, nil, fmt.Errorf("invalid dest port: %w", err)
|
||||
}
|
||||
|
||||
// Validate protocol matches IP version
|
||||
if protocol == proxyProtoV1TCP4 && srcIP.To4() == nil {
|
||||
return nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
|
||||
}
|
||||
if protocol == proxyProtoV1TCP6 && srcIP.To4() != nil {
|
||||
return nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
|
||||
return nil, nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
|
||||
}
|
||||
if protocol != proxyProtoV1TCP4 && protocol != proxyProtoV1TCP6 {
|
||||
return nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
|
||||
return nil, nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
|
||||
}
|
||||
|
||||
return &proxyProtoAddr{
|
||||
@@ -215,25 +219,27 @@ foundCRLF:
|
||||
srcPort: uint16(srcPort),
|
||||
dstIP: dstIP,
|
||||
dstPort: uint16(dstPort),
|
||||
}, nil
|
||||
}, 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, error) {
|
||||
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, err
|
||||
return nil, nil, err
|
||||
}
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
|
||||
// Detect version
|
||||
version, firstBytes, err := detectProxyProtoVersion(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch version {
|
||||
@@ -244,25 +250,26 @@ func readProxyProtoHeader(conn net.Conn) (*proxyProtoAddr, error) {
|
||||
// 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, fmt.Errorf("failed to read v2 signature: %w", err)
|
||||
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, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
|
||||
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, fmt.Errorf("failed to read v2 header: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to read v2 header: %w", err)
|
||||
}
|
||||
|
||||
// Continue with parsing
|
||||
return parseProxyProtoV2Header(conn, header)
|
||||
addr, err := parseProxyProtoV2Header(conn, header)
|
||||
return addr, nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
|
||||
return nil, nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ func init() {
|
||||
|
||||
const (
|
||||
// VERSION is the current version for the server.
|
||||
VERSION = "2.12.2"
|
||||
VERSION = "2.12.6"
|
||||
|
||||
// PROTO is the currently supported protocol.
|
||||
// 0 was the original
|
||||
|
||||
+187
-123
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 The NATS Authors
|
||||
// Copyright 2019-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
|
||||
@@ -42,7 +42,6 @@ import (
|
||||
const (
|
||||
JSPullRequestPendingMsgs = "Nats-Pending-Messages"
|
||||
JSPullRequestPendingBytes = "Nats-Pending-Bytes"
|
||||
JSPullRequestWrongPinID = "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n\r\n"
|
||||
JSPullRequestNatsPinId = "Nats-Pin-Id"
|
||||
)
|
||||
|
||||
@@ -512,7 +511,7 @@ type consumer struct {
|
||||
// Details described in ADR-42.
|
||||
|
||||
// currentPinId is the current nuid for the pinned consumer.
|
||||
// If the Consumer is running in `PriorityPinnedClient` mode, server will
|
||||
// If the Consumer is running in `PriorityPinnedClient` mode, server will
|
||||
// pick up a new nuid and assign it to first pending pull request.
|
||||
currentPinId string
|
||||
/// pinnedTtl is the remaining time before the current PinId expires.
|
||||
@@ -825,7 +824,7 @@ func checkConsumerCfg(
|
||||
return NewJSStreamInvalidConfigError(ErrBadSubject)
|
||||
}
|
||||
for inner, ssubject := range subjectFilters {
|
||||
if inner != outer && SubjectsCollide(subject, ssubject) {
|
||||
if inner != outer && subjectIsSubsetMatch(subject, ssubject) {
|
||||
return NewJSConsumerOverlappingSubjectFiltersError()
|
||||
}
|
||||
}
|
||||
@@ -963,7 +962,7 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
}
|
||||
|
||||
mset.mu.RLock()
|
||||
s, jsa, cfg, acc := mset.srv, mset.jsa, mset.cfg, mset.acc
|
||||
s, js, jsa, cfg, acc := mset.srv, mset.js, mset.jsa, mset.cfg, mset.acc
|
||||
mset.mu.RUnlock()
|
||||
|
||||
// If we do not have the consumer currently assigned to us in cluster mode we will proceed but warn.
|
||||
@@ -1030,11 +1029,11 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
}
|
||||
if cName != _EMPTY_ {
|
||||
if eo, ok := mset.consumers[cName]; ok {
|
||||
mset.mu.Unlock()
|
||||
if action == ActionCreate {
|
||||
ocfg := eo.config()
|
||||
copyConsumerMetadata(config, &ocfg)
|
||||
if !reflect.DeepEqual(config, &ocfg) {
|
||||
mset.mu.Unlock()
|
||||
return nil, NewJSConsumerAlreadyExistsError()
|
||||
}
|
||||
}
|
||||
@@ -1042,9 +1041,11 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
if cfg.Retention == WorkQueuePolicy {
|
||||
subjects := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects)
|
||||
if !mset.partitionUnique(cName, subjects) {
|
||||
mset.mu.Unlock()
|
||||
return nil, NewJSConsumerWQConsumerNotUniqueError()
|
||||
}
|
||||
}
|
||||
mset.mu.Unlock()
|
||||
err := eo.updateConfig(config)
|
||||
if err == nil {
|
||||
return eo, nil
|
||||
@@ -1057,17 +1058,22 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
return nil, NewJSConsumerDoesNotExistError()
|
||||
}
|
||||
|
||||
// Check for any limits, if the config for the consumer sets a limit we check against that
|
||||
// but if not we use the value from account limits, if account limits is more restrictive
|
||||
// than stream config we prefer the account limits to handle cases where account limits are
|
||||
// updated during the lifecycle of the stream
|
||||
maxc := cfg.MaxConsumers
|
||||
if maxc <= 0 || (selectedLimits.MaxConsumers > 0 && selectedLimits.MaxConsumers < maxc) {
|
||||
maxc = selectedLimits.MaxConsumers
|
||||
}
|
||||
if maxc > 0 && mset.numPublicConsumers() >= maxc {
|
||||
mset.mu.Unlock()
|
||||
return nil, NewJSMaximumConsumersLimitError()
|
||||
// If we're clustered we've already done this check, only do this if we're a standalone server.
|
||||
// But if we're standalone, only enforce if we're not recovering, since the MaxConsumers could've
|
||||
// been updated while we already had more consumers on disk.
|
||||
if !s.JetStreamIsClustered() && s.standAloneMode() && !isRecovering {
|
||||
// Check for any limits, if the config for the consumer sets a limit we check against that
|
||||
// but if not we use the value from account limits, if account limits is more restrictive
|
||||
// than stream config we prefer the account limits to handle cases where account limits are
|
||||
// updated during the lifecycle of the stream
|
||||
maxc := cfg.MaxConsumers
|
||||
if maxc <= 0 || (selectedLimits.MaxConsumers > 0 && selectedLimits.MaxConsumers < maxc) {
|
||||
maxc = selectedLimits.MaxConsumers
|
||||
}
|
||||
if maxc > 0 && mset.numPublicConsumers() >= maxc {
|
||||
mset.mu.Unlock()
|
||||
return nil, NewJSMaximumConsumersLimitError()
|
||||
}
|
||||
}
|
||||
|
||||
// Check on stream type conflicts with WorkQueues.
|
||||
@@ -1134,6 +1140,13 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
created: time.Now().UTC(),
|
||||
}
|
||||
|
||||
// Add created timestamp used for the store, must match that of the consumer assignment if it exists.
|
||||
if ca != nil {
|
||||
js.mu.RLock()
|
||||
o.created = ca.Created
|
||||
js.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Bind internal client to the user account.
|
||||
o.client.registerWithAccount(a)
|
||||
// Bind to the system account.
|
||||
@@ -1186,7 +1199,7 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
|
||||
// Setup our storage if not a direct consumer.
|
||||
if !config.Direct {
|
||||
store, err := mset.store.ConsumerStore(o.name, config)
|
||||
store, err := mset.store.ConsumerStore(o.name, o.created, config)
|
||||
if err != nil {
|
||||
mset.mu.Unlock()
|
||||
o.deleteWithoutAdvisory()
|
||||
@@ -1206,14 +1219,13 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
|
||||
|
||||
// If we have multiple filter subjects, create a sublist which we will use
|
||||
// in calling store.LoadNextMsgMulti.
|
||||
if len(o.cfg.FilterSubjects) > 0 {
|
||||
o.filters = gsl.NewSublist[struct{}]()
|
||||
for _, filter := range o.cfg.FilterSubjects {
|
||||
o.filters.Insert(filter, struct{}{})
|
||||
}
|
||||
} else {
|
||||
// Make sure this is nil otherwise.
|
||||
if len(o.subjf) <= 1 {
|
||||
o.filters = nil
|
||||
} else {
|
||||
o.filters = gsl.NewSublist[struct{}]()
|
||||
for _, filter := range o.subjf {
|
||||
o.filters.Insert(filter.subject, struct{}{})
|
||||
}
|
||||
}
|
||||
|
||||
if o.store != nil && o.store.HasState() {
|
||||
@@ -1393,8 +1405,12 @@ func (o *consumer) monitorQuitC() <-chan struct{} {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
// Recreate if a prior monitor routine was stopped.
|
||||
if o.mqch == nil {
|
||||
o.mqch = make(chan struct{})
|
||||
}
|
||||
return o.mqch
|
||||
}
|
||||
|
||||
@@ -1535,7 +1551,6 @@ func (o *consumer) setLeader(isLeader bool) {
|
||||
if o.cfg.AckPolicy != AckNone {
|
||||
if o.ackSub, err = o.subscribeInternal(o.ackSubj, o.pushAck); err != nil {
|
||||
o.mu.Unlock()
|
||||
o.deleteWithoutAdvisory()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1544,7 +1559,6 @@ func (o *consumer) setLeader(isLeader bool) {
|
||||
// Will error if wrong mode to provide feedback to users.
|
||||
if o.reqSub, err = o.subscribeInternal(o.nextMsgSubj, o.processNextMsgReq); err != nil {
|
||||
o.mu.Unlock()
|
||||
o.deleteWithoutAdvisory()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1554,7 +1568,6 @@ func (o *consumer) setLeader(isLeader bool) {
|
||||
fcsubj := fmt.Sprintf(jsFlowControl, stream, o.name)
|
||||
if o.fcSub, err = o.subscribeInternal(fcsubj, o.processFlowControl); err != nil {
|
||||
o.mu.Unlock()
|
||||
o.deleteWithoutAdvisory()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1680,6 +1693,7 @@ func (o *consumer) setLeader(isLeader bool) {
|
||||
} else if o.srv.gateway.enabled {
|
||||
stopAndClearTimer(&o.gwdtmr)
|
||||
}
|
||||
o.unassignPinId()
|
||||
// If we were the leader make sure to drain queued up acks.
|
||||
if wasLeader {
|
||||
o.ackMsgs.drain()
|
||||
@@ -2039,6 +2053,7 @@ func (o *consumer) deleteNotActive() {
|
||||
if o.srv != nil {
|
||||
qch = o.srv.quitCh
|
||||
}
|
||||
oqch := o.qch
|
||||
o.mu.Unlock()
|
||||
if js != nil {
|
||||
cqch = js.clusterQuitC()
|
||||
@@ -2087,6 +2102,9 @@ func (o *consumer) deleteNotActive() {
|
||||
return
|
||||
case <-cqch:
|
||||
return
|
||||
case <-oqch:
|
||||
// The consumer has stopped already, likely by an earlier delete proposal being applied.
|
||||
return
|
||||
}
|
||||
js.mu.RLock()
|
||||
if js.shuttingDown {
|
||||
@@ -2394,7 +2412,8 @@ func (o *consumer) updateConfig(cfg *ConsumerConfig) error {
|
||||
|
||||
// Check for Subject Filters update.
|
||||
newSubjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects)
|
||||
if !subjectSliceEqual(newSubjects, o.subjf.subjects()) {
|
||||
updatedFilters := !subjectSliceEqual(newSubjects, o.subjf.subjects())
|
||||
if updatedFilters {
|
||||
newSubjf := make(subjectFilters, 0, len(newSubjects))
|
||||
for _, newFilter := range newSubjects {
|
||||
fs := &subjectFilter{
|
||||
@@ -2433,15 +2452,17 @@ func (o *consumer) updateConfig(cfg *ConsumerConfig) error {
|
||||
// Allowed but considered no-op, [Description, SampleFrequency, MaxWaiting, HeadersOnly]
|
||||
o.cfg = *cfg
|
||||
|
||||
// Cleanup messages that lost interest.
|
||||
if o.retention == InterestPolicy {
|
||||
o.mu.Unlock()
|
||||
o.cleanupNoInterestMessages(o.mset, false)
|
||||
o.mu.Lock()
|
||||
}
|
||||
if updatedFilters {
|
||||
// Cleanup messages that lost interest.
|
||||
if o.retention == InterestPolicy {
|
||||
o.mu.Unlock()
|
||||
o.cleanupNoInterestMessages(o.mset, false)
|
||||
o.mu.Lock()
|
||||
}
|
||||
|
||||
// Re-calculate num pending on update.
|
||||
o.streamNumPending()
|
||||
// Re-calculate num pending on update.
|
||||
o.streamNumPending()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2831,14 +2852,10 @@ func (o *consumer) releaseAnyPendingRequests(isAssigned bool) {
|
||||
if o.mset == nil || o.outq == nil || o.waiting.len() == 0 {
|
||||
return
|
||||
}
|
||||
var hdr []byte
|
||||
if !isAssigned {
|
||||
hdr = []byte("NATS/1.0 409 Consumer Deleted\r\n\r\n")
|
||||
}
|
||||
|
||||
wq := o.waiting
|
||||
for wr := wq.head; wr != nil; {
|
||||
if hdr != nil {
|
||||
if !isAssigned {
|
||||
hdr := []byte("NATS/1.0 409 Consumer Deleted\r\n\r\n")
|
||||
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
||||
}
|
||||
next := wr.next
|
||||
@@ -3042,6 +3059,11 @@ func (o *consumer) setStoreState(state *ConsumerState) error {
|
||||
err := o.store.Update(state)
|
||||
if err == nil {
|
||||
o.applyState(state)
|
||||
} else if err == ErrStoreOldUpdate {
|
||||
// Our store already has a newer state, which is normal during recovery
|
||||
// when the consumer was loaded from disk before the meta snapshot state
|
||||
// was applied.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -3133,6 +3155,12 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
|
||||
})
|
||||
}
|
||||
|
||||
np, err := o.checkNumPending()
|
||||
if err != nil {
|
||||
o.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := o.cfg
|
||||
info := &ConsumerInfo{
|
||||
Stream: o.stream,
|
||||
@@ -3149,7 +3177,7 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
|
||||
},
|
||||
NumAckPending: len(o.pending),
|
||||
NumRedelivered: len(o.rdc),
|
||||
NumPending: o.checkNumPending(),
|
||||
NumPending: np,
|
||||
PushBound: o.isPushMode() && o.active,
|
||||
TimeStamp: time.Now().UTC(),
|
||||
PriorityGroups: priorityGroups,
|
||||
@@ -3900,7 +3928,12 @@ func (o *consumer) setPinnedTimer(priorityGroup string) {
|
||||
} else {
|
||||
o.pinnedTtl = time.AfterFunc(o.cfg.PinnedTTL, func() {
|
||||
o.mu.Lock()
|
||||
o.currentPinId = _EMPTY_
|
||||
// Skip if already unset.
|
||||
if o.currentPinId == _EMPTY_ {
|
||||
o.mu.Unlock()
|
||||
return
|
||||
}
|
||||
o.unassignPinId()
|
||||
o.sendUnpinnedAdvisoryLocked(priorityGroup, "timeout")
|
||||
o.mu.Unlock()
|
||||
o.signalNewMessages()
|
||||
@@ -3908,6 +3941,28 @@ func (o *consumer) setPinnedTimer(priorityGroup string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock should be held.
|
||||
func (o *consumer) assignNewPinId(wr *waitingRequest) {
|
||||
if wr.priorityGroup == nil || wr.priorityGroup.Group == _EMPTY_ {
|
||||
return
|
||||
}
|
||||
o.currentPinId = nuid.Next()
|
||||
o.pinnedTS = time.Now().UTC()
|
||||
wr.priorityGroup.Id = o.currentPinId
|
||||
o.setPinnedTimer(wr.priorityGroup.Group)
|
||||
o.sendPinnedAdvisoryLocked(wr.priorityGroup.Group)
|
||||
}
|
||||
|
||||
// Lock should be held.
|
||||
func (o *consumer) unassignPinId() {
|
||||
o.currentPinId = _EMPTY_
|
||||
o.pinnedTS = time.Time{}
|
||||
if o.pinnedTtl != nil {
|
||||
o.pinnedTtl.Stop()
|
||||
o.pinnedTtl = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return next waiting request. This will check for expirations but not noWait or interest.
|
||||
// That will be handled by processWaiting.
|
||||
// Lock should be held.
|
||||
@@ -3918,11 +3973,6 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
|
||||
// Check if server needs to assign a new pin id.
|
||||
needNewPin := o.currentPinId == _EMPTY_ && o.cfg.PriorityPolicy == PriorityPinnedClient
|
||||
// As long as we support only one priority group, we can capture that group here and reuse it.
|
||||
var priorityGroup string
|
||||
if len(o.cfg.PriorityGroups) > 0 {
|
||||
priorityGroup = o.cfg.PriorityGroups[0]
|
||||
}
|
||||
|
||||
numCycled := 0
|
||||
for wr := o.waiting.peek(); !o.waiting.isEmpty(); wr = o.waiting.peek() {
|
||||
@@ -3956,15 +4006,12 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
if wr.expires.IsZero() || time.Now().Before(wr.expires) {
|
||||
if needNewPin {
|
||||
if wr.priorityGroup.Id == _EMPTY_ {
|
||||
o.currentPinId = nuid.Next()
|
||||
o.pinnedTS = time.Now().UTC()
|
||||
wr.priorityGroup.Id = o.currentPinId
|
||||
o.setPinnedTimer(priorityGroup)
|
||||
|
||||
o.assignNewPinId(wr)
|
||||
} else {
|
||||
// There is pin id set, but not a matching one. Send a notification to the client and remove the request.
|
||||
// Probably this is the old pin id.
|
||||
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, []byte(JSPullRequestWrongPinID), nil, nil, 0))
|
||||
hdr := fmt.Appendf(nil, "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b)
|
||||
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
||||
o.waiting.removeCurrent()
|
||||
if o.node != nil {
|
||||
o.removeClusterPendingRequest(wr.reply)
|
||||
@@ -3985,7 +4032,8 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
continue
|
||||
} else {
|
||||
// There is pin id set, but not a matching one. Send a notification to the client and remove the request.
|
||||
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, []byte(JSPullRequestWrongPinID), nil, nil, 0))
|
||||
hdr := fmt.Appendf(nil, "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b)
|
||||
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
||||
o.waiting.removeCurrent()
|
||||
if o.node != nil {
|
||||
o.removeClusterPendingRequest(wr.reply)
|
||||
@@ -3997,9 +4045,13 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
|
||||
if o.cfg.PriorityPolicy == PriorityOverflow {
|
||||
if wr.priorityGroup != nil &&
|
||||
// If both limits are zero we don't cycle and the request will be fulfilled.
|
||||
(wr.priorityGroup.MinPending > 0 || wr.priorityGroup.MinAckPending > 0) &&
|
||||
// We need to check o.npc+1, because before calling nextWaiting, we do o.npc--
|
||||
(wr.priorityGroup.MinPending > 0 && wr.priorityGroup.MinPending > o.npc+1 ||
|
||||
wr.priorityGroup.MinAckPending > 0 && wr.priorityGroup.MinAckPending > int64(len(o.pending))) {
|
||||
// If one OR the other limit is exceeded, we want to fulfill the request.
|
||||
// This is an inverted check. For clarity, we check the positive condition and negate.
|
||||
!((wr.priorityGroup.MinPending > 0 && wr.priorityGroup.MinPending <= o.npc+1) ||
|
||||
(wr.priorityGroup.MinAckPending > 0 && wr.priorityGroup.MinAckPending <= int64(len(o.pending)))) {
|
||||
o.waiting.cycle()
|
||||
numCycled++
|
||||
// We're done cycling through the requests.
|
||||
@@ -4010,19 +4062,10 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
}
|
||||
}
|
||||
if wr.acc.sl.HasInterest(wr.interest) {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
} else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
} else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
}
|
||||
} else {
|
||||
@@ -4180,15 +4223,7 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
|
||||
sendErr(400, "Bad Request - Priority Group missing")
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, group := range o.cfg.PriorityGroups {
|
||||
if group == priorityGroup.Group {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if !slices.Contains(o.cfg.PriorityGroups, priorityGroup.Group) {
|
||||
sendErr(400, "Bad Request - Invalid Priority Group")
|
||||
return
|
||||
}
|
||||
@@ -4426,6 +4461,8 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
|
||||
// scheduled for redelivery, but it has been removed from the stream.
|
||||
// o.processTerm is called in a goroutine so could run after we get here.
|
||||
// That will correct the pending state and delivery/ack floors, so just skip here.
|
||||
pmsg.returnToPool()
|
||||
pmsg = nil
|
||||
continue
|
||||
}
|
||||
return pmsg, dc, err
|
||||
@@ -4453,6 +4490,7 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
|
||||
sm, err := o.mset.store.LoadMsg(seq, &pmsg.StoreMsg)
|
||||
if sm == nil || err != nil {
|
||||
pmsg.returnToPool()
|
||||
pmsg = nil
|
||||
}
|
||||
o.sseq++
|
||||
return pmsg, 1, err
|
||||
@@ -4964,6 +5002,7 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
|
||||
o.addToRedeliverQueue(pmsg.seq)
|
||||
}
|
||||
pmsg.returnToPool()
|
||||
pmsg = nil
|
||||
goto waitForMsgs
|
||||
}
|
||||
|
||||
@@ -4974,6 +5013,7 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
|
||||
select {
|
||||
case <-qch:
|
||||
pmsg.returnToPool()
|
||||
pmsg = nil
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
@@ -4994,6 +5034,7 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
|
||||
select {
|
||||
case <-qch:
|
||||
pmsg.returnToPool()
|
||||
pmsg = nil
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
@@ -5097,17 +5138,22 @@ func (o *consumer) setMaxPendingBytes(limit int) {
|
||||
// The race is a getNextMsg skips a deleted msg, and then the decStreamPending call fires.
|
||||
// This does some quick sanity checks to see if we should re-calculate num pending.
|
||||
// Lock should be held.
|
||||
func (o *consumer) checkNumPending() uint64 {
|
||||
func (o *consumer) checkNumPending() (uint64, error) {
|
||||
if o.mset != nil && o.mset.store != nil {
|
||||
var state StreamState
|
||||
o.mset.store.FastState(&state)
|
||||
npc := o.numPending()
|
||||
if o.sseq > state.LastSeq && npc > 0 || npc > state.Msgs {
|
||||
// Re-calculate.
|
||||
o.streamNumPending()
|
||||
// Make sure we can't report more messages than there are.
|
||||
// TODO(nat): It's not great that this means consumer info has side effects,
|
||||
// since we can't know whether anyone will call it or not. The previous num
|
||||
// pending calculation that this replaces had the same problem though.
|
||||
if o.sseq > state.LastSeq {
|
||||
o.npc = 0
|
||||
} else if npc > 0 {
|
||||
o.npc = int64(min(npc, state.Msgs, state.LastSeq-o.sseq+1))
|
||||
}
|
||||
}
|
||||
return o.numPending()
|
||||
return o.numPending(), nil
|
||||
}
|
||||
|
||||
// Lock should be held.
|
||||
@@ -5134,7 +5180,7 @@ func (o *consumer) checkNumPendingOnEOF() {
|
||||
}
|
||||
|
||||
// Call into streamNumPending after acquiring the consumer lock.
|
||||
func (o *consumer) streamNumPendingLocked() uint64 {
|
||||
func (o *consumer) streamNumPendingLocked() (uint64, error) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
return o.streamNumPending()
|
||||
@@ -5143,22 +5189,25 @@ func (o *consumer) streamNumPendingLocked() uint64 {
|
||||
// Will force a set from the stream store of num pending.
|
||||
// Depends on delivery policy, for last per subject we calculate differently.
|
||||
// Lock should be held.
|
||||
func (o *consumer) streamNumPending() uint64 {
|
||||
func (o *consumer) streamNumPending() (uint64, error) {
|
||||
if o.mset == nil || o.mset.store == nil {
|
||||
o.npc, o.npf = 0, 0
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
npc, npf, err := o.calculateNumPending()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
npc, npf := o.calculateNumPending()
|
||||
o.npc, o.npf = int64(npc), npf
|
||||
return o.numPending()
|
||||
return o.numPending(), nil
|
||||
}
|
||||
|
||||
// Will calculate num pending but only requires a read lock.
|
||||
// Depends on delivery policy, for last per subject we calculate differently.
|
||||
// At least RLock should be held.
|
||||
func (o *consumer) calculateNumPending() (npc, npf uint64) {
|
||||
func (o *consumer) calculateNumPending() (npc, npf uint64, err error) {
|
||||
if o.mset == nil || o.mset.store == nil {
|
||||
return 0, 0
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
isLastPerSubject := o.cfg.DeliverPolicy == DeliverLastPerSubject
|
||||
@@ -5349,6 +5398,15 @@ func (o *consumer) trackPending(sseq, dseq uint64) {
|
||||
o.pending = make(map[uint64]*Pending)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if p, ok := o.pending[sseq]; ok {
|
||||
// Update timestamp but keep original consumer delivery sequence.
|
||||
// So do not update p.Sequence.
|
||||
p.Timestamp = now.UnixNano()
|
||||
} else {
|
||||
o.pending[sseq] = &Pending{dseq, now.UnixNano()}
|
||||
}
|
||||
|
||||
// We could have a backoff that set a timer higher than what we need for this message.
|
||||
// In that case, reset to lowest backoff required for a message redelivery.
|
||||
minDelay := o.ackWait(0)
|
||||
@@ -5361,18 +5419,10 @@ func (o *consumer) trackPending(sseq, dseq uint64) {
|
||||
}
|
||||
minDelay = o.ackWait(o.cfg.BackOff[bi])
|
||||
}
|
||||
minDeadline := time.Now().Add(minDelay)
|
||||
minDeadline := now.Add(minDelay)
|
||||
if o.ptmr == nil || o.ptmrEnd.After(minDeadline) {
|
||||
o.resetPtmr(minDelay)
|
||||
}
|
||||
|
||||
if p, ok := o.pending[sseq]; ok {
|
||||
// Update timestamp but keep original consumer delivery sequence.
|
||||
// So do not update p.Sequence.
|
||||
p.Timestamp = time.Now().UnixNano()
|
||||
} else {
|
||||
o.pending[sseq] = &Pending{dseq, time.Now().UnixNano()}
|
||||
}
|
||||
}
|
||||
|
||||
// Credit back a failed delivery.
|
||||
@@ -6445,6 +6495,10 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
|
||||
if asflr&(1<<63) != 0 {
|
||||
return errAckFloorInvalid
|
||||
}
|
||||
dflr := asflr
|
||||
if len(state.Pending) > 0 && state.Delivered.Stream > dflr {
|
||||
dflr = state.Delivered.Stream
|
||||
}
|
||||
|
||||
// Check if the underlying stream's last sequence is less than our floor.
|
||||
// This can happen if the stream has been reset and has not caught up yet.
|
||||
@@ -6463,7 +6517,7 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
|
||||
}
|
||||
|
||||
var retryAsflr uint64
|
||||
for seq = fseq; asflr > 0 && seq <= asflr; seq++ {
|
||||
for seq = fseq; dflr > 0 && seq <= dflr; seq++ {
|
||||
if filters != nil {
|
||||
_, nseq, err = store.LoadNextMsgMulti(filters, seq, &smv)
|
||||
} else {
|
||||
@@ -6473,19 +6527,43 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
|
||||
if nseq > seq {
|
||||
seq = nseq
|
||||
}
|
||||
// Only ack though if no error and seq <= ack floor.
|
||||
if err == nil && seq <= asflr {
|
||||
didRemove := mset.ackMsg(o, seq)
|
||||
// Removing the message could fail. For example if clustered since we need to propose it.
|
||||
// Overwrite retry floor (only the first time) to allow us to check next time if the removal was successful.
|
||||
if didRemove && retryAsflr == 0 {
|
||||
retryAsflr = seq
|
||||
if err == nil {
|
||||
// Only ack though if no error and seq <= ack floor.
|
||||
if seq <= asflr {
|
||||
didRemove := mset.ackMsg(o, seq)
|
||||
// Removing the message could fail. For example if clustered since we need to propose it.
|
||||
// Overwrite retry floor (only the first time) to allow us to check next time if the removal was successful.
|
||||
if didRemove && retryAsflr == 0 {
|
||||
retryAsflr = seq
|
||||
}
|
||||
} else if seq <= dflr {
|
||||
// Store the first entry above our ack floor, so we don't need to look it up again on retryAsflr=0.
|
||||
if retryAsflr == 0 {
|
||||
retryAsflr = seq
|
||||
}
|
||||
// If we have pending, we will need to walk through to delivered in case we missed any of those acks as well.
|
||||
if _, ok := state.Pending[seq]; !ok {
|
||||
// The filters are already taken into account,
|
||||
mset.ackMsg(o, seq)
|
||||
}
|
||||
}
|
||||
} else if err == ErrStoreEOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
// If retry floor was not overwritten, set to ack floor+1, we don't need to account for any retries below it.
|
||||
// However, our ack floor may be lower than the next message we can receive, so we correct it upward if needed.
|
||||
if retryAsflr == 0 {
|
||||
retryAsflr = asflr + 1
|
||||
if filters != nil {
|
||||
_, nseq, err = store.LoadNextMsgMulti(filters, asflr+1, &smv)
|
||||
} else {
|
||||
_, nseq, err = store.LoadNextMsg(filter, wc, asflr+1, &smv)
|
||||
}
|
||||
if err == nil {
|
||||
retryAsflr = max(asflr+1, nseq)
|
||||
} else if err == ErrStoreEOF {
|
||||
retryAsflr = ss.LastSeq + 1
|
||||
}
|
||||
}
|
||||
|
||||
o.mu.Lock()
|
||||
@@ -6494,21 +6572,7 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
|
||||
if retryAsflr > o.chkflr {
|
||||
o.chkflr = retryAsflr
|
||||
}
|
||||
// See if we need to process this update if our parent stream is not a limits policy stream.
|
||||
state, _ = o.store.State()
|
||||
o.mu.Unlock()
|
||||
|
||||
// If we have pending, we will need to walk through to delivered in case we missed any of those acks as well.
|
||||
if state != nil && len(state.Pending) > 0 && state.AckFloor.Stream > 0 {
|
||||
for seq := state.AckFloor.Stream + 1; seq <= state.Delivered.Stream; seq++ {
|
||||
if _, ok := state.Pending[seq]; !ok {
|
||||
// Want to call needAck since it is filter aware.
|
||||
if o.needAck(seq, _EMPTY_) {
|
||||
mset.ackMsg(o, seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -215,6 +215,9 @@ var (
|
||||
|
||||
// ErrMinimumVersionRequired is returned when a connection is not at the minimum version required.
|
||||
ErrMinimumVersionRequired = errors.New("minimum version required")
|
||||
// ErrLeafNodeMinVersionRejected is the leafnode protocol error prefix used
|
||||
// when rejecting a remote due to leafnodes.min_version.
|
||||
ErrLeafNodeMinVersionRejected = errors.New("connection rejected since minimum version required is")
|
||||
|
||||
// ErrInvalidMappingDestination is used for all subject mapping destination errors
|
||||
ErrInvalidMappingDestination = errors.New("invalid mapping destination")
|
||||
@@ -251,6 +254,9 @@ type mappingDestinationErr struct {
|
||||
}
|
||||
|
||||
func (e *mappingDestinationErr) Error() string {
|
||||
if e.token == _EMPTY_ {
|
||||
return e.err.Error()
|
||||
}
|
||||
return fmt.Sprintf("%s in %s", e.err, e.token)
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -1998,5 +1998,15 @@
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSClusterServerMemberChangeInflightErr",
|
||||
"code": 400,
|
||||
"error_code": 10202,
|
||||
"description": "cluster member change is in progress",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
}
|
||||
]
|
||||
+15
@@ -1500,6 +1500,8 @@ func (s *Server) initEventTracking() {
|
||||
type UserInfo struct {
|
||||
UserID string `json:"user"`
|
||||
Account string `json:"account"`
|
||||
AccountName string `json:"account_name,omitempty"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
Permissions *Permissions `json:"permissions,omitempty"`
|
||||
Expires time.Duration `json:"expires,omitempty"`
|
||||
}
|
||||
@@ -1519,9 +1521,22 @@ func (s *Server) userInfoReq(sub *subscription, c *client, _ *Account, subject,
|
||||
return
|
||||
}
|
||||
|
||||
// Look up the requester's account directly from ci.Account rather than
|
||||
// using the acc returned by getRequestInfo, which may resolve to the
|
||||
// service account (ci.Service) when the request arrives via a chained
|
||||
// service import.
|
||||
var accountName string
|
||||
if ci.Account != _EMPTY_ {
|
||||
if reqAcc, _ := s.LookupAccount(ci.Account); reqAcc != nil {
|
||||
accountName = reqAcc.getNameTag()
|
||||
}
|
||||
}
|
||||
|
||||
response.Data = &UserInfo{
|
||||
UserID: ci.User,
|
||||
Account: ci.Account,
|
||||
AccountName: accountName,
|
||||
UserName: ci.NameTag,
|
||||
Permissions: c.publicPermissions(),
|
||||
Expires: c.claimExpiration(),
|
||||
}
|
||||
|
||||
+891
-360
File diff suppressed because it is too large.
Load diff
+49
-86
@@ -17,9 +17,6 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server/stree"
|
||||
)
|
||||
|
||||
// Sublist is a routing mechanism to handle subject distribution and
|
||||
@@ -51,6 +48,11 @@ var (
|
||||
// unnecessary allocations.
|
||||
type SimpleSublist = GenericSublist[struct{}]
|
||||
|
||||
// NewSimpleSublist will create a simple sublist.
|
||||
func NewSimpleSublist() *SimpleSublist {
|
||||
return &GenericSublist[struct{}]{root: newLevel[struct{}]()}
|
||||
}
|
||||
|
||||
// A GenericSublist stores and efficiently retrieves subscriptions.
|
||||
type GenericSublist[T comparable] struct {
|
||||
sync.RWMutex
|
||||
@@ -246,7 +248,9 @@ func matchLevelForAny[T comparable](l *level[T], toks []string, np *int) bool {
|
||||
if np != nil {
|
||||
*np += len(n.subs)
|
||||
}
|
||||
return len(n.subs) > 0
|
||||
if len(n.subs) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if pwc != nil {
|
||||
if np != nil {
|
||||
@@ -365,6 +369,36 @@ func (s *GenericSublist[T]) Remove(subject string, value T) error {
|
||||
return s.remove(subject, value, true)
|
||||
}
|
||||
|
||||
// HasInterestStartingIn is a helper for subject tree intersection.
|
||||
func (s *GenericSublist[T]) HasInterestStartingIn(subj string) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
var _tokens [64]string
|
||||
tokens := tokenizeSubjectIntoSlice(_tokens[:0], subj)
|
||||
return hasInterestStartingIn(s.root, tokens)
|
||||
}
|
||||
|
||||
func hasInterestStartingIn[T comparable](l *level[T], tokens []string) bool {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return true
|
||||
}
|
||||
token := tokens[0]
|
||||
if l.fwc != nil {
|
||||
return true
|
||||
}
|
||||
found := false
|
||||
if pwc := l.pwc; pwc != nil {
|
||||
found = found || hasInterestStartingIn(pwc.next, tokens[1:])
|
||||
}
|
||||
if n := l.nodes[token]; n != nil {
|
||||
found = found || hasInterestStartingIn(n.next, tokens[1:])
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// pruneNode is used to prune an empty node from the tree.
|
||||
func (l *level[T]) pruneNode(n *node[T], t string) {
|
||||
if n == nil {
|
||||
@@ -458,86 +492,15 @@ func visitLevel[T comparable](l *level[T], depth int) int {
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
// IntersectStree will match all items in the given subject tree that
|
||||
// have interest expressed in the given sublist. The callback will only be called
|
||||
// once for each subject, regardless of overlapping subscriptions in the sublist.
|
||||
func IntersectStree[T1 any, T2 comparable](st *stree.SubjectTree[T1], sl *GenericSublist[T2], cb func(subj []byte, entry *T1)) {
|
||||
var _subj [255]byte
|
||||
intersectStree(st, sl.root, _subj[:0], cb)
|
||||
}
|
||||
|
||||
func intersectStree[T1 any, T2 comparable](st *stree.SubjectTree[T1], r *level[T2], subj []byte, cb func(subj []byte, entry *T1)) {
|
||||
nsubj := subj
|
||||
if len(nsubj) > 0 {
|
||||
nsubj = append(subj, '.')
|
||||
}
|
||||
if r.fwc != nil {
|
||||
// We've reached a full wildcard, do a FWC match on the stree at this point
|
||||
// and don't keep iterating downward.
|
||||
nsubj := append(nsubj, '>')
|
||||
st.Match(nsubj, cb)
|
||||
return
|
||||
}
|
||||
if r.pwc != nil {
|
||||
// We've found a partial wildcard. We'll keep iterating downwards, but first
|
||||
// check whether there's interest at this level (without triggering dupes) and
|
||||
// match if so.
|
||||
var done bool
|
||||
nsubj := append(nsubj, '*')
|
||||
if len(r.pwc.subs) > 0 {
|
||||
st.Match(nsubj, cb)
|
||||
done = true
|
||||
}
|
||||
if r.pwc.next.numNodes() > 0 {
|
||||
intersectStree(st, r.pwc.next, nsubj, cb)
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Normal node with subject literals, keep iterating.
|
||||
for t, n := range r.nodes {
|
||||
if r.pwc != nil && r.pwc.next.numNodes() > 0 && n.next.numNodes() > 0 {
|
||||
// A wildcard at the next level will already visit these descendents
|
||||
// so skip so we don't callback the same subject more than once.
|
||||
continue
|
||||
}
|
||||
nsubj := append(nsubj, t...)
|
||||
if len(n.subs) > 0 {
|
||||
if subjectHasWildcard(bytesToString(nsubj)) {
|
||||
st.Match(nsubj, cb)
|
||||
} else {
|
||||
if e, ok := st.Find(nsubj); ok {
|
||||
cb(nsubj, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.next.numNodes() > 0 {
|
||||
intersectStree(st, n.next, nsubj, cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if a subject has any wildcard tokens.
|
||||
func subjectHasWildcard(subject string) bool {
|
||||
// This one exits earlier then !subjectIsLiteral(subject)
|
||||
for i, c := range subject {
|
||||
if c == pwc || c == fwc {
|
||||
if (i == 0 || subject[i-1] == btsep) &&
|
||||
(i+1 == len(subject) || subject[i+1] == btsep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note this will avoid a copy of the data used for the string, but it will also reference the existing slice's data pointer.
|
||||
// So this should be used sparingly when we know the encompassing byte slice's lifetime is the same.
|
||||
func bytesToString(b []byte) string {
|
||||
if len(b) == 0 {
|
||||
return _EMPTY_
|
||||
}
|
||||
p := unsafe.SliceData(b)
|
||||
return unsafe.String(p, len(b))
|
||||
// use similar to append. meaning, the updated slice will be returned
|
||||
func tokenizeSubjectIntoSlice(tts []string, subject string) []string {
|
||||
start := 0
|
||||
for i := 0; i < len(subject); i++ {
|
||||
if subject[i] == btsep {
|
||||
tts = append(tts, subject[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
tts = append(tts, subject[start:])
|
||||
return tts
|
||||
}
|
||||
+17
-18
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 The NATS Authors
|
||||
// Copyright 2019-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
|
||||
@@ -1142,6 +1142,12 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits, tq c
|
||||
}
|
||||
|
||||
js.mu.Lock()
|
||||
// Accounts get reset to nil on shutdown, since we re-acquire the locks here, we need to check again.
|
||||
if js.accounts == nil {
|
||||
js.mu.Unlock()
|
||||
return NewJSNotEnabledError()
|
||||
}
|
||||
|
||||
if jsa, ok := js.accounts[a.Name]; ok {
|
||||
a.mu.Lock()
|
||||
a.js = jsa
|
||||
@@ -1370,7 +1376,7 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits, tq c
|
||||
}
|
||||
obs, err := mset.addConsumerWithAssignment(&cfg.ConsumerConfig, _EMPTY_, nil, true, ActionCreateOrUpdate, false)
|
||||
if err != nil {
|
||||
s.Warnf(" Error adding consumer %q: %v", cfg.Name, err)
|
||||
s.Warnf(" Error adding consumer '%s > %s > %s': %v", a.Name, mset.name(), cfg.Name, err)
|
||||
continue
|
||||
}
|
||||
if isEphemeral {
|
||||
@@ -1379,9 +1385,6 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits, tq c
|
||||
if !cfg.Created.IsZero() {
|
||||
obs.setCreatedTime(cfg.Created)
|
||||
}
|
||||
if err != nil {
|
||||
s.Warnf(" Error restoring consumer %q state: %v", cfg.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1560,7 +1563,7 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits, tq c
|
||||
}
|
||||
|
||||
// Add in the stream.
|
||||
mset, err := a.addStream(&cfg.StreamConfig)
|
||||
mset, err := a.recoverStream(&cfg.StreamConfig)
|
||||
if err != nil {
|
||||
s.Warnf(" Error recreating stream %q: %v", cfg.Name, err)
|
||||
// If we removed a keyfile from above make sure to put it back.
|
||||
@@ -2362,8 +2365,10 @@ func tierName(replicas int) string {
|
||||
}
|
||||
|
||||
func isSameTier(cfgA, cfgB *StreamConfig) bool {
|
||||
a := max(1, cfgA.Replicas)
|
||||
b := max(1, cfgB.Replicas)
|
||||
// TODO (mh) this is where we could select based off a placement tag as well "qos:tier"
|
||||
return cfgA.Replicas == cfgB.Replicas
|
||||
return a == b
|
||||
}
|
||||
|
||||
func (jsa *jsAccount) jetStreamAndClustered() (*jetStream, bool) {
|
||||
@@ -2438,17 +2443,11 @@ func (jsa *jsAccount) wouldExceedLimits(storeType StorageType, tierName string,
|
||||
// Since tiers are flat we need to scale limit up by replicas when checking.
|
||||
if storeType == MemoryStorage {
|
||||
totalMem := inUse.total.mem + (int64(memStoreMsgSize(subj, hdr, msg)) * r)
|
||||
if selectedLimits.MemoryMaxStreamBytes > 0 && totalMem > selectedLimits.MemoryMaxStreamBytes*lr {
|
||||
return true, nil
|
||||
}
|
||||
if selectedLimits.MaxMemory >= 0 && totalMem > selectedLimits.MaxMemory*lr {
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
totalStore := inUse.total.store + (int64(fileStoreMsgSize(subj, hdr, msg)) * r)
|
||||
if selectedLimits.StoreMaxStreamBytes > 0 && totalStore > selectedLimits.StoreMaxStreamBytes*lr {
|
||||
return true, nil
|
||||
}
|
||||
if selectedLimits.MaxStore >= 0 && totalStore > selectedLimits.MaxStore*lr {
|
||||
return true, nil
|
||||
}
|
||||
@@ -2487,25 +2486,25 @@ func (js *jetStream) checkBytesLimits(selectedLimits *JetStreamAccountLimits, ad
|
||||
if addBytes < 0 {
|
||||
addBytes = 1
|
||||
}
|
||||
totalBytes := addBytes + maxBytesOffset
|
||||
totalBytes := addSaturate(addBytes, maxBytesOffset)
|
||||
|
||||
switch storage {
|
||||
case MemoryStorage:
|
||||
// Account limits defined.
|
||||
if selectedLimits.MaxMemory >= 0 && currentRes+totalBytes > selectedLimits.MaxMemory {
|
||||
if selectedLimits.MaxMemory >= 0 && (currentRes > selectedLimits.MaxMemory || totalBytes > selectedLimits.MaxMemory-currentRes) {
|
||||
return NewJSMemoryResourcesExceededError()
|
||||
}
|
||||
// Check if this server can handle request.
|
||||
if checkServer && js.memReserved+totalBytes > js.config.MaxMemory {
|
||||
if checkServer && (js.memReserved > js.config.MaxMemory || totalBytes > js.config.MaxMemory-js.memReserved) {
|
||||
return NewJSMemoryResourcesExceededError()
|
||||
}
|
||||
case FileStorage:
|
||||
// Account limits defined.
|
||||
if selectedLimits.MaxStore >= 0 && currentRes+totalBytes > selectedLimits.MaxStore {
|
||||
if selectedLimits.MaxStore >= 0 && (currentRes > selectedLimits.MaxStore || totalBytes > selectedLimits.MaxStore-currentRes) {
|
||||
return NewJSStorageResourcesExceededError()
|
||||
}
|
||||
// Check if this server can handle request.
|
||||
if checkServer && js.storeReserved+totalBytes > js.config.MaxStore {
|
||||
if checkServer && (js.storeReserved > js.config.MaxStore || totalBytes > js.config.MaxStore-js.storeReserved) {
|
||||
return NewJSStorageResourcesExceededError()
|
||||
}
|
||||
}
|
||||
|
||||
+432
-336
File diff suppressed because it is too large.
Load diff
+35
-30
@@ -611,41 +611,46 @@ func checkMsgHeadersPreClusteredProposal(
|
||||
// We need to deny here otherwise we'd need to bump CLFS, and it could succeed on some
|
||||
// peers and not others depending on consumer ack state (if interest policy).
|
||||
// So we deny here, if we allow that means we know it would succeed on every peer.
|
||||
if discard == DiscardNew && (maxMsgs > 0 || maxBytes > 0) {
|
||||
// Error if over DiscardNew per subject threshold.
|
||||
if discardNewPer {
|
||||
totalMsgsForSubject := i.ops
|
||||
if i, ok = mset.inflight[subject]; ok {
|
||||
totalMsgsForSubject += i.ops
|
||||
if discard == DiscardNew {
|
||||
if maxMsgs > 0 || maxBytes > 0 {
|
||||
// Track usual max msgs/bytes thresholds for DiscardNew.
|
||||
var state StreamState
|
||||
mset.store.FastState(&state)
|
||||
|
||||
totalMsgs := state.Msgs
|
||||
totalBytes := state.Bytes
|
||||
for _, i = range mset.inflight {
|
||||
totalMsgs += i.ops
|
||||
totalBytes += i.bytes
|
||||
}
|
||||
if maxMsgsPer > 0 && totalMsgsForSubject > uint64(maxMsgsPer) {
|
||||
err = ErrMaxMsgsPerSubject
|
||||
for _, i = range diff.inflight {
|
||||
totalMsgs += i.ops
|
||||
totalBytes += i.bytes
|
||||
}
|
||||
|
||||
if maxMsgs > 0 && totalMsgs > uint64(maxMsgs) {
|
||||
err = ErrMaxMsgs
|
||||
} else if maxBytes > 0 && totalBytes > uint64(maxBytes) {
|
||||
err = ErrMaxBytes
|
||||
}
|
||||
if err != nil {
|
||||
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
|
||||
}
|
||||
}
|
||||
|
||||
// Track usual max msgs/bytes thresholds for DiscardNew.
|
||||
var state StreamState
|
||||
mset.store.FastState(&state)
|
||||
|
||||
totalMsgs := state.Msgs
|
||||
totalBytes := state.Bytes
|
||||
for _, i = range mset.inflight {
|
||||
totalMsgs += i.ops
|
||||
totalBytes += i.bytes
|
||||
}
|
||||
for _, i = range diff.inflight {
|
||||
totalMsgs += i.ops
|
||||
totalBytes += i.bytes
|
||||
}
|
||||
|
||||
if maxMsgs > 0 && totalMsgs > uint64(maxMsgs) {
|
||||
err = ErrMaxMsgs
|
||||
} else if maxBytes > 0 && totalBytes > uint64(maxBytes) {
|
||||
err = ErrMaxBytes
|
||||
}
|
||||
if err != nil {
|
||||
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
|
||||
// Similarly, check DiscardNew per-subject threshold to not need to bump CLFS.
|
||||
if discardNewPer && maxMsgsPer > 0 {
|
||||
// Get the current total for this subject.
|
||||
totalMsgsForSubject := mset.store.SubjectsTotals(subject)[subject]
|
||||
// Add inflight count in this batch and for this stream.
|
||||
totalMsgsForSubject += i.ops
|
||||
if i, ok = mset.inflight[subject]; ok {
|
||||
totalMsgsForSubject += i.ops
|
||||
}
|
||||
if totalMsgsForSubject > uint64(maxMsgsPer) {
|
||||
err = ErrMaxMsgsPerSubject
|
||||
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1227
-549
File diff suppressed because it is too large.
Load diff
+14
@@ -59,6 +59,9 @@ const (
|
||||
// JSClusterRequiredErr JetStream clustering support required
|
||||
JSClusterRequiredErr ErrorIdentifier = 10010
|
||||
|
||||
// JSClusterServerMemberChangeInflightErr cluster member change is in progress
|
||||
JSClusterServerMemberChangeInflightErr ErrorIdentifier = 10202
|
||||
|
||||
// JSClusterServerNotMemberErr server is not a member of the cluster
|
||||
JSClusterServerNotMemberErr ErrorIdentifier = 10044
|
||||
|
||||
@@ -626,6 +629,7 @@ var (
|
||||
JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"},
|
||||
JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"},
|
||||
JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"},
|
||||
JSClusterServerMemberChangeInflightErr: {Code: 400, ErrCode: 10202, Description: "cluster member change is in progress"},
|
||||
JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"},
|
||||
JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"},
|
||||
JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"},
|
||||
@@ -1031,6 +1035,16 @@ func NewJSClusterRequiredError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSClusterRequiredErr]
|
||||
}
|
||||
|
||||
// NewJSClusterServerMemberChangeInflightError creates a new JSClusterServerMemberChangeInflightErr error: "cluster member change is in progress"
|
||||
func NewJSClusterServerMemberChangeInflightError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSClusterServerMemberChangeInflightErr]
|
||||
}
|
||||
|
||||
// NewJSClusterServerNotMemberError creates a new JSClusterServerNotMemberErr error: "server is not a member of the cluster"
|
||||
func NewJSClusterServerNotMemberError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import "strconv"
|
||||
|
||||
const (
|
||||
// JSApiLevel is the maximum supported JetStream API level for this server.
|
||||
JSApiLevel int = 2
|
||||
JSApiLevel int = 3
|
||||
|
||||
JSRequiredLevelMetadataKey = "_nats.req.level"
|
||||
JSServerVersionMetadataKey = "_nats.ver"
|
||||
|
||||
+38
-23
@@ -63,9 +63,9 @@ const (
|
||||
// LEAF connection as opposed to a CLIENT.
|
||||
leafNodeWSPath = "/leafnode"
|
||||
|
||||
// This is the time the server will wait, when receiving a CONNECT,
|
||||
// before closing the connection if the required minimum version is not met.
|
||||
leafNodeWaitBeforeClose = 5 * time.Second
|
||||
// When a soliciting leafnode is rejected because it does not meet the
|
||||
// configured minimum version, delay the next reconnect attempt by this long.
|
||||
leafNodeMinVersionReconnectDelay = 5 * time.Second
|
||||
)
|
||||
|
||||
type leaf struct {
|
||||
@@ -691,9 +691,8 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
|
||||
} else {
|
||||
s.Debugf("Trying to connect as leafnode to remote server on %q%s", rURL.Host, ipStr)
|
||||
|
||||
// Check if proxy is configured first, then check if URL supports it
|
||||
if proxyURL != _EMPTY_ && isWSURL(rURL) {
|
||||
// Use proxy for WebSocket connections - use original hostname, resolved IP for connection
|
||||
// Check if proxy is configured
|
||||
if proxyURL != _EMPTY_ {
|
||||
targetHost := rURL.Host
|
||||
// If URL doesn't include port, add the default port for the scheme
|
||||
if rURL.Port() == _EMPTY_ {
|
||||
@@ -2082,17 +2081,11 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
|
||||
if mv := s.getOpts().LeafNode.MinVersion; mv != _EMPTY_ {
|
||||
major, minor, update, _ := versionComponents(mv)
|
||||
if !versionAtLeast(proto.Version, major, minor, update) {
|
||||
// We are going to send back an INFO because otherwise recent
|
||||
// versions of the remote server would simply break the connection
|
||||
// after 2 seconds if not receiving it. Instead, we want the
|
||||
// other side to just "stall" until we finish waiting for the holding
|
||||
// period and close the connection below.
|
||||
// Send back an INFO so recent remote servers process the rejection
|
||||
// cleanly, then close immediately. The soliciting side applies the
|
||||
// reconnect delay when it processes the error.
|
||||
s.sendPermsAndAccountInfo(c)
|
||||
c.sendErrAndErr(fmt.Sprintf("connection rejected since minimum version required is %q", mv))
|
||||
select {
|
||||
case <-c.srv.quitCh:
|
||||
case <-time.After(leafNodeWaitBeforeClose):
|
||||
}
|
||||
c.sendErrAndErr(fmt.Sprintf("%s %q", ErrLeafNodeMinVersionRejected, mv))
|
||||
c.closeConnection(MinimumVersionRequired)
|
||||
return ErrMinimumVersionRequired
|
||||
}
|
||||
@@ -2760,6 +2753,14 @@ func (c *client) processLeafSub(argo []byte) (err error) {
|
||||
}
|
||||
|
||||
acc := c.acc
|
||||
// Guard against LS+ arriving before CONNECT has been processed, which
|
||||
// can happen when compression is enabled.
|
||||
if acc == nil {
|
||||
c.mu.Unlock()
|
||||
c.sendErr("Authorization Violation")
|
||||
c.closeConnection(ProtocolViolation)
|
||||
return nil
|
||||
}
|
||||
// Check if we have a loop.
|
||||
ldsPrefix := bytes.HasPrefix(sub.subject, []byte(leafNodeLoopDetectionSubjectPrefix))
|
||||
|
||||
@@ -2832,7 +2833,7 @@ func (c *client) processLeafSub(argo []byte) (err error) {
|
||||
|
||||
// Only add in shadow subs if a new sub or qsub.
|
||||
if osub == nil {
|
||||
if err := c.addShadowSubscriptions(acc, sub, true); err != nil {
|
||||
if err := c.addShadowSubscriptions(acc, sub); err != nil {
|
||||
c.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -2876,7 +2877,6 @@ func (c *client) processLeafUnsub(arg []byte) error {
|
||||
// Indicate any activity, so pub and sub or unsubs.
|
||||
c.in.subs++
|
||||
|
||||
acc := c.acc
|
||||
srv := c.srv
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -2885,6 +2885,15 @@ func (c *client) processLeafUnsub(arg []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
acc := c.acc
|
||||
// Guard against LS- arriving before CONNECT has been processed.
|
||||
if acc == nil {
|
||||
c.mu.Unlock()
|
||||
c.sendErr("Authorization Violation")
|
||||
c.closeConnection(ProtocolViolation)
|
||||
return nil
|
||||
}
|
||||
|
||||
spoke := c.isSpokeLeafNode()
|
||||
// We store local subs by account and subject and optionally queue name.
|
||||
// LS- will have the arg exactly as the key.
|
||||
@@ -2916,8 +2925,7 @@ func (c *client) processLeafUnsub(arg []byte) error {
|
||||
|
||||
func (c *client) processLeafHeaderMsgArgs(arg []byte) error {
|
||||
// Unroll splitArgs to avoid runtime/heap issues
|
||||
a := [MAX_MSG_ARGS][]byte{}
|
||||
args := a[:0]
|
||||
args := c.argsa[:0]
|
||||
start := -1
|
||||
for i, b := range arg {
|
||||
switch b {
|
||||
@@ -3000,8 +3008,7 @@ func (c *client) processLeafHeaderMsgArgs(arg []byte) error {
|
||||
|
||||
func (c *client) processLeafMsgArgs(arg []byte) error {
|
||||
// Unroll splitArgs to avoid runtime/heap issues
|
||||
a := [MAX_MSG_ARGS][]byte{}
|
||||
args := a[:0]
|
||||
args := c.argsa[:0]
|
||||
start := -1
|
||||
for i, b := range arg {
|
||||
switch b {
|
||||
@@ -3181,6 +3188,11 @@ func (c *client) leafProcessErr(errStr string) {
|
||||
c.Errorf("Leafnode connection dropped with same cluster name error. Delaying attempt to reconnect for %v", delay)
|
||||
return
|
||||
}
|
||||
if strings.Contains(errStr, ErrLeafNodeMinVersionRejected.Error()) {
|
||||
_, delay := c.setLeafConnectDelayIfSoliciting(leafNodeMinVersionReconnectDelay)
|
||||
c.Errorf("Leafnode connection dropped due to minimum version requirement. Delaying attempt to reconnect for %v", delay)
|
||||
return
|
||||
}
|
||||
|
||||
// We will look for Loop detected error coming from the other side.
|
||||
// If we solicit, set the connect delay.
|
||||
@@ -3203,7 +3215,10 @@ func (c *client) setLeafConnectDelayIfSoliciting(delay time.Duration) (string, t
|
||||
}
|
||||
c.leaf.remote.setConnectDelay(delay)
|
||||
}
|
||||
accName := c.acc.Name
|
||||
var accName string
|
||||
if c.acc != nil {
|
||||
accName = c.acc.Name
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return accName, delay
|
||||
}
|
||||
|
||||
+194
-54
@@ -19,7 +19,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -445,7 +444,6 @@ func (ms *memStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) {
|
||||
|
||||
// GetSeqFromTime looks for the first sequence number that has the message
|
||||
// with >= timestamp.
|
||||
// FIXME(dlc) - inefficient.
|
||||
func (ms *memStore) GetSeqFromTime(t time.Time) uint64 {
|
||||
ts := t.UnixNano()
|
||||
ms.mu.RLock()
|
||||
@@ -469,18 +467,57 @@ func (ms *memStore) GetSeqFromTime(t time.Time) uint64 {
|
||||
|
||||
last := lmsg.ts
|
||||
if ts == last {
|
||||
return ms.state.LastSeq
|
||||
return lmsg.seq
|
||||
}
|
||||
if ts > last {
|
||||
return ms.state.LastSeq + 1
|
||||
}
|
||||
index := sort.Search(len(ms.msgs), func(i int) bool {
|
||||
if msg := ms.msgs[ms.state.FirstSeq+uint64(i)]; msg != nil {
|
||||
return msg.ts >= ts
|
||||
|
||||
var (
|
||||
cts int64
|
||||
cseq uint64
|
||||
off uint64
|
||||
)
|
||||
|
||||
// Using a binary search, but need to be aware of interior deletes.
|
||||
fseq := ms.state.FirstSeq
|
||||
lseq := ms.state.LastSeq
|
||||
seq := lseq + 1
|
||||
loop:
|
||||
for fseq <= lseq {
|
||||
mid := fseq + (lseq-fseq)/2
|
||||
off = 0
|
||||
// Potentially skip over gaps. We keep the original middle but keep track of a
|
||||
// potential delete range with an offset.
|
||||
for {
|
||||
msg := ms.msgs[mid+off]
|
||||
if msg == nil {
|
||||
off++
|
||||
if mid+off <= lseq {
|
||||
continue
|
||||
} else {
|
||||
// Continue search to the left. Purposely ignore the skipped deletes here.
|
||||
lseq = mid - 1
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
cts = msg.ts
|
||||
cseq = msg.seq
|
||||
break
|
||||
}
|
||||
return false
|
||||
})
|
||||
return uint64(index) + ms.state.FirstSeq
|
||||
if cts >= ts {
|
||||
seq = cseq
|
||||
if mid == fseq {
|
||||
break
|
||||
}
|
||||
// Continue search to the left.
|
||||
lseq = mid - 1
|
||||
} else {
|
||||
// Continue search to the right (potentially skipping over interior deletes).
|
||||
fseq = mid + off + 1
|
||||
}
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
// FilteredState will return the SimpleState associated with the filtered subject and a proposed starting sequence.
|
||||
@@ -749,6 +786,10 @@ func (ms *memStore) allLastSeqsLocked() ([]uint64, error) {
|
||||
|
||||
seqs := make([]uint64, 0, ms.fss.Size())
|
||||
ms.fss.IterFast(func(subj []byte, ss *SimpleState) bool {
|
||||
// Check if we need to recalculate. We only care about the last sequence.
|
||||
if ss.lastNeedsUpdate {
|
||||
ms.recalculateForSubj(bytesToString(subj), ss)
|
||||
}
|
||||
seqs = append(seqs, ss.Last)
|
||||
return true
|
||||
})
|
||||
@@ -766,6 +807,7 @@ func (ms *memStore) filterIsAll(filters []string) bool {
|
||||
}
|
||||
// Sort so we can compare.
|
||||
slices.Sort(filters)
|
||||
slices.Sort(ms.cfg.Subjects)
|
||||
for i, subj := range filters {
|
||||
if !subjectIsSubsetMatch(ms.cfg.Subjects[i], subj) {
|
||||
return false
|
||||
@@ -777,8 +819,8 @@ func (ms *memStore) filterIsAll(filters []string) bool {
|
||||
// MultiLastSeqs will return a sorted list of sequences that match all subjects presented in filters.
|
||||
// We will not exceed the maxSeq, which if 0 becomes the store's last sequence.
|
||||
func (ms *memStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) {
|
||||
ms.mu.RLock()
|
||||
defer ms.mu.RUnlock()
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
|
||||
if len(ms.msgs) == 0 {
|
||||
return nil, nil
|
||||
@@ -806,6 +848,9 @@ func (ms *memStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed in
|
||||
|
||||
for _, filter := range filters {
|
||||
ms.fss.Match(stringToBytes(filter), func(subj []byte, ss *SimpleState) {
|
||||
if ss.lastNeedsUpdate {
|
||||
ms.recalculateForSubj(bytesToString(subj), ss)
|
||||
}
|
||||
if ss.Last <= maxSeq {
|
||||
addIfNotDupe(ss.Last)
|
||||
} else if ss.Msgs > 1 {
|
||||
@@ -861,17 +906,17 @@ func (ms *memStore) subjectsTotalsLocked(filterSubject string) map[string]uint64
|
||||
}
|
||||
|
||||
// NumPending will return the number of pending messages matching the filter subject starting at sequence.
|
||||
func (ms *memStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64) {
|
||||
func (ms *memStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64, err error) {
|
||||
// This needs to be a write lock, as filteredStateLocked can mutate the per-subject state.
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
|
||||
ss := ms.filteredStateLocked(sseq, filter, lastPerSubject)
|
||||
return ss.Msgs, ms.state.LastSeq
|
||||
return ss.Msgs, ms.state.LastSeq, nil
|
||||
}
|
||||
|
||||
// NumPending will return the number of pending messages matching any subject in the sublist starting at sequence.
|
||||
func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64) {
|
||||
func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64, err error) {
|
||||
if sl == nil {
|
||||
return ms.NumPending(sseq, fwcs, lastPerSubject)
|
||||
}
|
||||
@@ -886,7 +931,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
|
||||
}
|
||||
// If past the end no results.
|
||||
if sseq > ms.state.LastSeq {
|
||||
return 0, ms.state.LastSeq
|
||||
return 0, ms.state.LastSeq, nil
|
||||
}
|
||||
|
||||
update := func(fss *SimpleState) {
|
||||
@@ -906,7 +951,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
|
||||
var havePartial bool
|
||||
var totalSkipped uint64
|
||||
// We will track start and end sequences as we go.
|
||||
gsl.IntersectStree[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) {
|
||||
stree.IntersectGSL[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) {
|
||||
if fss.firstNeedsUpdate || fss.lastNeedsUpdate {
|
||||
ms.recalculateForSubj(bytesToString(subj), fss)
|
||||
}
|
||||
@@ -924,7 +969,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
|
||||
|
||||
// If we did not encounter any partials we can return here.
|
||||
if !havePartial {
|
||||
return ss.Msgs, ms.state.LastSeq
|
||||
return ss.Msgs, ms.state.LastSeq, nil
|
||||
}
|
||||
|
||||
// If we are here we need to scan the msgs.
|
||||
@@ -1015,7 +1060,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
|
||||
ss.Msgs -= adjust
|
||||
}
|
||||
|
||||
return ss.Msgs, ms.state.LastSeq
|
||||
return ss.Msgs, ms.state.LastSeq, nil
|
||||
}
|
||||
|
||||
// Will check the msg limit for this tracked subject.
|
||||
@@ -1463,6 +1508,12 @@ func (ms *memStore) compact(seq uint64) (uint64, error) {
|
||||
var purged, bytes uint64
|
||||
|
||||
ms.mu.Lock()
|
||||
// Short-circuit if the store was already compacted past this point.
|
||||
if ms.state.FirstSeq > seq {
|
||||
ms.mu.Unlock()
|
||||
return purged, nil
|
||||
}
|
||||
|
||||
cb := ms.scb
|
||||
if seq <= ms.state.LastSeq {
|
||||
fseq := ms.state.FirstSeq
|
||||
@@ -1621,7 +1672,8 @@ func (ms *memStore) SubjectForSeq(seq uint64) (string, error) {
|
||||
return _EMPTY_, ErrStoreMsgNotFound
|
||||
}
|
||||
if sm, ok := ms.msgs[seq]; ok {
|
||||
return sm.subj, nil
|
||||
// Copy the subject, as it's used elsewhere, and we've released the lock in the meantime.
|
||||
return copyString(sm.subj), nil
|
||||
}
|
||||
return _EMPTY_, ErrStoreMsgNotFound
|
||||
}
|
||||
@@ -1673,6 +1725,10 @@ func (ms *memStore) LoadLastMsg(subject string, smp *StoreMsg) (*StoreMsg, error
|
||||
} else if subjectIsLiteral(subject) {
|
||||
var ss *SimpleState
|
||||
if ss, ok = ms.fss.Find(stringToBytes(subject)); ok && ss.Msgs > 0 {
|
||||
// Check if we need to recalculate. We only care about the last sequence.
|
||||
if ss.lastNeedsUpdate {
|
||||
ms.recalculateForSubj(subject, ss)
|
||||
}
|
||||
sm, ok = ms.msgs[ss.Last]
|
||||
}
|
||||
} else if ss := ms.filteredStateLocked(1, subject, true); ss.Msgs > 0 {
|
||||
@@ -1731,6 +1787,78 @@ func (ms *memStore) LoadNextMsg(filter string, wc bool, start uint64, smp *Store
|
||||
return ms.loadNextMsgLocked(filter, wc, start, smp)
|
||||
}
|
||||
|
||||
// Find sequence bounds matching a wildcard filter from ms.fss.
|
||||
// Returns (first, last, true) if there is at least one matching
|
||||
// subject at or after start (start <= first <= last).
|
||||
// Returns (0, 0, false) if the subject does not exist or has no
|
||||
// messages at or after start.
|
||||
// Lock should be held.
|
||||
func (ms *memStore) nextWildcardMatchLocked(filter string, start uint64) (uint64, uint64, bool) {
|
||||
found := false
|
||||
first, last := ms.state.LastSeq, uint64(0)
|
||||
ms.fss.MatchUntil(stringToBytes(filter), func(subj []byte, ss *SimpleState) bool {
|
||||
ms.recalculateForSubj(string(subj), ss)
|
||||
|
||||
// Skip matches that are below our starting sequence
|
||||
if start > ss.Last {
|
||||
return true
|
||||
}
|
||||
|
||||
// A match was found, adjust the bounds accordingly
|
||||
found = true
|
||||
if ss.First < first {
|
||||
first = ss.First
|
||||
}
|
||||
if ss.Last > last {
|
||||
last = ss.Last
|
||||
}
|
||||
|
||||
// If first > start, there may be more matches between
|
||||
// start and first, in which case we keep searching.
|
||||
// If not, we have a match between start and last, we
|
||||
// can break out of the search.
|
||||
// This could be further optimized: if first and start
|
||||
// are "close", we could just extend the linear search,
|
||||
// especially if we know that the remaining ms.fss to
|
||||
// explore is large.
|
||||
return first > start
|
||||
})
|
||||
if !found {
|
||||
return 0, 0, false
|
||||
}
|
||||
return max(first, start), last, found
|
||||
}
|
||||
|
||||
// Find sequence bounds matching a literal filter from ms.fss.
|
||||
// Returns (first, last, true) if there is a matching literal
|
||||
// subject at or after start (start <= first <= last).
|
||||
// Returns (0, 0, false) if the subject does not exist or has no
|
||||
// messages at or after start.
|
||||
// Lock should be held.
|
||||
func (ms *memStore) nextLiteralMatchLocked(filter string, start uint64) (uint64, uint64, bool) {
|
||||
ss, ok := ms.fss.Find(stringToBytes(filter))
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
ms.recalculateForSubj(filter, ss)
|
||||
if start > ss.Last {
|
||||
return 0, 0, false
|
||||
}
|
||||
return max(start, ss.First), ss.Last, true
|
||||
}
|
||||
|
||||
// Returns true if LoadNextMsg should perform a linear scan,
|
||||
// false if it should use the subject tree to try to reduce
|
||||
// the search space.
|
||||
// Lock should be held.
|
||||
func (ms *memStore) shouldLinearScan(filter string, wc bool, start uint64) bool {
|
||||
// Skip scan of ms.fss if number of messages in the block are less than
|
||||
// 1/2 the number of subjects in ms.fss. Or we have a wc and lots of fss entries.
|
||||
const linearScanMaxFSS = 256
|
||||
isAll := filter == fwcs
|
||||
return isAll || 2*int(ms.state.LastSeq-start) < ms.fss.Size() || (wc && ms.fss.Size() > linearScanMaxFSS)
|
||||
}
|
||||
|
||||
// Lock should be held.
|
||||
func (ms *memStore) loadNextMsgLocked(filter string, wc bool, start uint64, smp *StoreMsg) (*StoreMsg, uint64, error) {
|
||||
if start < ms.state.FirstSeq {
|
||||
@@ -1747,46 +1875,24 @@ func (ms *memStore) loadNextMsgLocked(filter string, wc bool, start uint64, smp
|
||||
}
|
||||
isAll := filter == fwcs
|
||||
|
||||
// Skip scan of ms.fss if number of messages in the block are less than
|
||||
// 1/2 the number of subjects in ms.fss. Or we have a wc and lots of fss entries.
|
||||
const linearScanMaxFSS = 256
|
||||
doLinearScan := isAll || 2*int(ms.state.LastSeq-start) < ms.fss.Size() || (wc && ms.fss.Size() > linearScanMaxFSS)
|
||||
|
||||
// Initial setup.
|
||||
fseq, lseq := start, ms.state.LastSeq
|
||||
|
||||
if !doLinearScan {
|
||||
subs := []string{filter}
|
||||
if wc || isAll {
|
||||
subs = subs[:0]
|
||||
ms.fss.Match(stringToBytes(filter), func(subj []byte, val *SimpleState) {
|
||||
subs = append(subs, string(subj))
|
||||
})
|
||||
if !ms.shouldLinearScan(filter, wc, start) {
|
||||
var found bool
|
||||
if wc {
|
||||
fseq, lseq, found = ms.nextWildcardMatchLocked(filter, start)
|
||||
} else {
|
||||
fseq, lseq, found = ms.nextLiteralMatchLocked(filter, start)
|
||||
}
|
||||
fseq, lseq = ms.state.LastSeq, uint64(0)
|
||||
for _, subj := range subs {
|
||||
ss, ok := ms.fss.Find(stringToBytes(subj))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
|
||||
ms.recalculateForSubj(subj, ss)
|
||||
}
|
||||
if ss.First < fseq {
|
||||
fseq = ss.First
|
||||
}
|
||||
if ss.Last > lseq {
|
||||
lseq = ss.Last
|
||||
}
|
||||
}
|
||||
if fseq < start {
|
||||
fseq = start
|
||||
if !found {
|
||||
return nil, ms.state.LastSeq, ErrStoreEOF
|
||||
}
|
||||
}
|
||||
|
||||
eq := subjectsEqual
|
||||
if wc {
|
||||
eq = subjectIsSubsetMatch
|
||||
eq = matchLiteral
|
||||
}
|
||||
|
||||
for nseq := fseq; nseq <= lseq; nseq++ {
|
||||
@@ -1828,6 +1934,40 @@ func (ms *memStore) LoadPrevMsg(start uint64, smp *StoreMsg) (sm *StoreMsg, err
|
||||
return nil, ErrStoreEOF
|
||||
}
|
||||
|
||||
// LoadPrevMsgMulti will find the previous message matching any entry in the sublist.
|
||||
func (ms *memStore) LoadPrevMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
|
||||
// TODO(dlc) - for now simple linear walk to get started.
|
||||
ms.mu.RLock()
|
||||
defer ms.mu.RUnlock()
|
||||
|
||||
if start > ms.state.LastSeq {
|
||||
start = ms.state.LastSeq
|
||||
}
|
||||
|
||||
// If past the start no results.
|
||||
if start < ms.state.FirstSeq || ms.state.Msgs == 0 {
|
||||
return nil, ms.state.FirstSeq, ErrStoreEOF
|
||||
}
|
||||
|
||||
// Initial setup.
|
||||
fseq, lseq := start, ms.state.FirstSeq
|
||||
|
||||
for nseq := fseq; nseq >= lseq; nseq-- {
|
||||
sm, ok := ms.msgs[nseq]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if sl.HasInterest(sm.subj) {
|
||||
if smp == nil {
|
||||
smp = new(StoreMsg)
|
||||
}
|
||||
sm.copy(smp)
|
||||
return smp, nseq, nil
|
||||
}
|
||||
}
|
||||
return nil, ms.state.LastSeq, ErrStoreEOF
|
||||
}
|
||||
|
||||
// RemoveMsg will remove the message from this store.
|
||||
// Will return the number of bytes removed.
|
||||
func (ms *memStore) RemoveMsg(seq uint64) (bool, error) {
|
||||
@@ -2036,8 +2176,8 @@ func (ms *memStore) FastState(state *StreamState) {
|
||||
}
|
||||
|
||||
func (ms *memStore) State() StreamState {
|
||||
ms.mu.RLock()
|
||||
defer ms.mu.RUnlock()
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
|
||||
state := ms.state
|
||||
state.Consumers = ms.consumers
|
||||
@@ -2129,7 +2269,7 @@ type consumerMemStore struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (ms *memStore) ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error) {
|
||||
func (ms *memStore) ConsumerStore(name string, _ time.Time, cfg *ConsumerConfig) (ConsumerStore, error) {
|
||||
if ms == nil {
|
||||
return nil, fmt.Errorf("memstore is nil")
|
||||
}
|
||||
@@ -2259,7 +2399,7 @@ func (o *consumerMemStore) Update(state *ConsumerState) error {
|
||||
|
||||
// Check to see if this is an outdated update.
|
||||
if state.Delivered.Consumer < o.state.Delivered.Consumer || state.AckFloor.Stream < o.state.AckFloor.Stream {
|
||||
return fmt.Errorf("old update ignored")
|
||||
return ErrStoreOldUpdate
|
||||
}
|
||||
|
||||
o.state.Delivered = state.Delivered
|
||||
|
||||
Loaded 100 of 444 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user