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 | ||
|
|
ac072bee8f | ||
|
|
fa4cd8e279 | ||
|
|
7bdbd9c474 | ||
|
|
1d43bbea17 | ||
|
|
d19968bec7 | ||
|
|
4962328f0c | ||
|
|
80cf0ba72c | ||
|
|
26f5cd7493 | ||
|
|
52cd4abc15 | ||
|
|
4d5851cdff | ||
|
|
5390322e38 |
No files matched your search
+12
-2
@@ -36,8 +36,18 @@ ifndef DATE
|
||||
DATE := $(shell date -u '+%Y%m%d')
|
||||
endif
|
||||
|
||||
LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -s -w -X "$(OC_REPO)/pkg/version.String=$(STRING)" -X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" -X "$(OC_REPO)/pkg/version.Date=$(DATE)"
|
||||
DEBUG_LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -X "$(OC_REPO)/pkg/version.String=$(STRING)" -X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" -X "$(OC_REPO)/pkg/version.Date=$(DATE)"
|
||||
LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -s -w \
|
||||
-X "$(OC_REPO)/pkg/version.Edition=$(EDITION)" \
|
||||
-X "$(OC_REPO)/pkg/version.String=$(STRING)" \
|
||||
-X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" \
|
||||
-X "$(OC_REPO)/pkg/version.Date=$(DATE)"
|
||||
|
||||
DEBUG_LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn \
|
||||
-X "$(OC_REPO)/pkg/version.Edition=$(EDITION)" \
|
||||
-X "$(OC_REPO)/pkg/version.String=$(STRING)" \
|
||||
-X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" \
|
||||
-X "$(OC_REPO)/pkg/version.Date=$(DATE)"
|
||||
|
||||
DOCKER_LDFLAGS += -X "$(OC_REPO)/pkg/config/defaults.BaseDataPathType=path" -X "$(OC_REPO)/pkg/config/defaults.BaseDataPathValue=/var/lib/opencloud"
|
||||
DOCKER_LDFLAGS += -X "$(OC_REPO)/pkg/config/defaults.BaseConfigPathType=path" -X "$(OC_REPO)/pkg/config/defaults.BaseConfigPathValue=/etc/opencloud"
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# The test runner source for UI tests
|
||||
WEB_COMMITID=50e3fff6a518361d59cba864a927470f313b6f91
|
||||
WEB_COMMITID=c247f854a95b7bcaf10f27577458d2f566b2f939
|
||||
WEB_BRANCH=stable-4.2
|
||||
|
||||
+531
-350
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,98 @@
|
||||
# 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! ❤️
|
||||
|
||||
@ScharfViktor, @fschade, @kulmann, @micbar, @prashant-gurung899
|
||||
|
||||
### ✅ Tests
|
||||
|
||||
- [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)]
|
||||
|
||||
## [4.0.0](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.0) - 2025-12-01
|
||||
|
||||
### ❤️ 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,8 +1,9 @@
|
||||
FROM golang:alpine3.22 AS build
|
||||
FROM golang:alpine3.23 AS build
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION
|
||||
ARG STRING
|
||||
ARG EDITION
|
||||
|
||||
RUN apk add bash make git curl gcc musl-dev libc-dev binutils-gold inotify-tools vips-dev
|
||||
|
||||
@@ -13,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)
|
||||
|
||||
+12
-2
@@ -105,9 +105,19 @@ func createResource(ctx context.Context, serviceName string) (*resource.Resource
|
||||
return resource.New(ctx,
|
||||
// Reads OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME
|
||||
resource.WithFromEnv(),
|
||||
// Host and process information
|
||||
// Host Information
|
||||
resource.WithHost(),
|
||||
resource.WithProcess(),
|
||||
// Process Information
|
||||
// Resource WithProcessOwner is deliberately omitted because
|
||||
// inside containers where process might run as an arbitrary
|
||||
// uid without a username associated this would fail.
|
||||
resource.WithProcessPID(),
|
||||
resource.WithProcessCommandArgs(),
|
||||
resource.WithProcessExecutableName(),
|
||||
resource.WithProcessExecutablePath(),
|
||||
resource.WithProcessRuntimeDescription(),
|
||||
resource.WithProcessRuntimeName(),
|
||||
resource.WithProcessRuntimeVersion(),
|
||||
// Service attributes
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName(serviceName),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package version
|
||||
|
||||
// InitEdition exports the private edition initialization func for testing
|
||||
var InitEdition = initEdition
|
||||
+60
-3
@@ -1,9 +1,27 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// Dev is used as a placeholder.
|
||||
Dev = "dev"
|
||||
// EditionDev indicates the development build channel was used to build the binary.
|
||||
EditionDev = Dev
|
||||
// EditionRolling indicates the rolling release build channel was used to build the binary.
|
||||
EditionRolling = "rolling"
|
||||
// EditionStable indicates the stable release build channel was used to build the binary.
|
||||
EditionStable = "stable"
|
||||
// EditionLTS indicates the lts release build channel was used to build the binary.
|
||||
EditionLTS = "lts"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -16,22 +34,61 @@ 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.0-rc.3+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
|
||||
//Date = time.Now().Format("20060102")
|
||||
Date = "dev"
|
||||
Date = Dev
|
||||
|
||||
// Legacy defines the old long 4 number OpenCloud version needed for some clients
|
||||
Legacy = "0.1.0.0"
|
||||
// LegacyString defines the old OpenCloud version needed for some clients
|
||||
LegacyString = "0.1.0"
|
||||
|
||||
// Edition describes the build channel (stable, rolling, nightly, daily, dev)
|
||||
Edition = Dev // default for self-compiled builds
|
||||
)
|
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
if err := initEdition(); err != nil {
|
||||
logger.New().Error().Err(err).Msg("falling back to dev")
|
||||
}
|
||||
}
|
||||
|
||||
func initEdition() error {
|
||||
regularEditions := []string{EditionDev, EditionRolling, EditionStable}
|
||||
versionedEditions := []string{EditionLTS}
|
||||
if !slices.ContainsFunc(slices.Concat(regularEditions, versionedEditions), func(s string) bool {
|
||||
isRegularEdition := slices.Contains(regularEditions, Edition)
|
||||
if isRegularEdition && s == Edition {
|
||||
return true
|
||||
}
|
||||
|
||||
// handle editions with a version
|
||||
editionParts := strings.Split(Edition, "-")
|
||||
if len(editionParts) != 2 { // a versioned edition channel must consist of exactly 2 parts.
|
||||
return false
|
||||
}
|
||||
|
||||
isVersionedEdition := slices.Contains(versionedEditions, editionParts[0])
|
||||
if !isVersionedEdition { // not all channels can contain version information
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := semver.NewVersion(editionParts[1])
|
||||
return err == nil
|
||||
}) {
|
||||
Edition = Dev
|
||||
return fmt.Errorf(`unknown edition channel "%s"`, Edition)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compiled returns the compile time of this service.
|
||||
func Compiled() time.Time {
|
||||
if Date == "dev" {
|
||||
if Date == Dev {
|
||||
return time.Now()
|
||||
}
|
||||
t, _ := time.Parse("20060102", Date)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
)
|
||||
|
||||
func TestChannel(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
got string
|
||||
valid bool
|
||||
}{
|
||||
"no channel, defaults to dev": {
|
||||
got: "",
|
||||
valid: false,
|
||||
},
|
||||
"dev channel": {
|
||||
got: version.EditionDev,
|
||||
valid: true,
|
||||
},
|
||||
"rolling channel": {
|
||||
got: version.EditionRolling,
|
||||
valid: true,
|
||||
},
|
||||
"stable channel": {
|
||||
got: version.EditionStable,
|
||||
valid: true,
|
||||
},
|
||||
"lts channel without version": {
|
||||
got: version.EditionLTS,
|
||||
valid: false,
|
||||
},
|
||||
"lts-1.0.0 channel": {
|
||||
got: fmt.Sprintf("%s-1", version.EditionLTS),
|
||||
valid: true,
|
||||
},
|
||||
"lts-one invalid version": {
|
||||
got: fmt.Sprintf("%s-one", version.EditionLTS),
|
||||
valid: false,
|
||||
},
|
||||
"known channel with version": {
|
||||
got: fmt.Sprintf("%s-1", version.EditionStable),
|
||||
valid: false,
|
||||
},
|
||||
"unknown channel": {
|
||||
got: "foo",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
version.Edition = test.got
|
||||
|
||||
switch err := version.InitEdition(); {
|
||||
case err != nil && !test.valid && version.Edition != version.Dev: // if a given edition is unknown, the value is always dev
|
||||
fallthrough
|
||||
case test.valid != (err == nil):
|
||||
t.Fatalf("invalid edition: %s", version.Edition)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ type Config struct {
|
||||
EnableFederatedSharingIncoming bool `yaml:"enable_federated_sharing_incoming" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING" desc:"Changing this value is NOT supported. Enables support for incoming federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
|
||||
EnableFederatedSharingOutgoing bool `yaml:"enable_federated_sharing_outgoing" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING" desc:"Changing this value is NOT supported. Enables support for outgoing federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
|
||||
SearchMinLength int `yaml:"search_min_length" env:"FRONTEND_SEARCH_MIN_LENGTH" desc:"Minimum number of characters to enter before a client should start a search for Share receivers. This setting can be used to customize the user experience if e.g too many results are displayed." introductionVersion:"1.0.0"`
|
||||
Edition string `yaml:"edition" env:"OC_EDITION;FRONTEND_EDITION" desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
|
||||
Edition string `desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
|
||||
DisableSSE bool `yaml:"disable_sse" env:"OC_DISABLE_SSE;FRONTEND_DISABLE_SSE" desc:"When set to true, clients are informed that the Server-Sent Events endpoint is not accessible." introductionVersion:"1.0.0"`
|
||||
DisableRadicale bool `yaml:"disable_radicale" env:"FRONTEND_DISABLE_RADICALE" desc:"When set to true, clients are informed that the Radicale (CalDAV/CardDAV) is not accessible." introductionVersion:"4.0.0"`
|
||||
DefaultLinkPermissions int `yaml:"default_link_permissions" env:"FRONTEND_DEFAULT_LINK_PERMISSIONS" desc:"Defines the default permissions a link is being created with. Possible values are 0 (= internal link, for instance members only) and 1 (= public link with viewer permissions). Defaults to 1." introductionVersion:"1.0.0"`
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
"github.com/opencloud-eu/opencloud/services/frontend/pkg/config"
|
||||
)
|
||||
|
||||
@@ -87,7 +88,7 @@ func DefaultConfig() *config.Config {
|
||||
DefaultUploadProtocol: "tus",
|
||||
DefaultLinkPermissions: 1,
|
||||
SearchMinLength: 3,
|
||||
Edition: "",
|
||||
Edition: version.Edition,
|
||||
CheckForUpdates: true,
|
||||
Checksums: config.Checksums{
|
||||
SupportedTypes: []string{"sha1", "md5", "adler32"},
|
||||
|
||||
@@ -346,7 +346,7 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string
|
||||
},
|
||||
"version": map[string]interface{}{
|
||||
"product": "OpenCloud",
|
||||
"edition": "",
|
||||
"edition": version.Edition,
|
||||
"major": version.ParsedLegacy().Major(),
|
||||
"minor": version.ParsedLegacy().Minor(),
|
||||
"micro": version.ParsedLegacy().Patch(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -80,5 +80,5 @@ type Status struct {
|
||||
Product string
|
||||
ProductName string
|
||||
ProductVersion string
|
||||
Edition string `yaml:"edition" env:"OC_EDITION;OCDAV_EDITION" desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
|
||||
Edition string `desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func DefaultConfig() *config.Config {
|
||||
ProductVersion: version.GetString(),
|
||||
Product: "OpenCloud",
|
||||
ProductName: "OpenCloud",
|
||||
Edition: "",
|
||||
Edition: version.Edition,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -136,6 +136,9 @@ func (p Web) getPayload() (payload []byte, err error) {
|
||||
p.config.Web.Config.Apps = make([]string, 0)
|
||||
}
|
||||
|
||||
// ensure that the server url has a trailing slash
|
||||
p.config.Web.Config.Server = strings.TrimRight(p.config.Web.Config.Server, "/") + "/"
|
||||
|
||||
return json.Marshal(p.config.Web.Config)
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -904,7 +904,7 @@ class CliContext implements Context {
|
||||
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
|
||||
$storagePath = $this->getUsersStoragePath();
|
||||
$body = [
|
||||
"command" => "xattr -p -slz " . escapeshellarg($attribute) . " $storagePath/$userUuid/$file",
|
||||
"command" => "getfattr -n " . escapeshellarg($attribute) . " --only-values $storagePath/$userUuid/$file",
|
||||
"raw" => true
|
||||
];
|
||||
$this->featureContext->setResponse(CliHelper::runCommand($body));
|
||||
|
||||
@@ -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"
|
||||
@@ -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
Loaded 100 of 453 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user