diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9ef3e047da4..c431a36cb40 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,8 +18,25 @@ updates: default-days: 30 - package-ecosystem: "github-actions" directory: "/" - schedule: + schedule: interval: monthly open-pull-requests-limit: 1 cooldown: default-days: 7 + - package-ecosystem: "docker-compose" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + open-pull-requests-limit: 1 + cooldown: + default-days: 7 + groups: + docker-compose: + patterns: + - "*" + allow: + - dependency-name: "ghcr.io/pkimetal/pkimetal" + - dependency-name: "jaegertracing/all-in-one" + - dependency-name: "minio/minio" + - dependency-name: "minio/mc" diff --git a/.github/workflows/boulder-ci.yml b/.github/workflows/boulder-ci.yml index 559be38538b..b22fa6586bb 100644 --- a/.github/workflows/boulder-ci.yml +++ b/.github/workflows/boulder-ci.yml @@ -36,7 +36,7 @@ jobs: matrix: # Add additional docker image tags here and all tests will be run with the additional image. BOULDER_TOOLS_TAG: - - go1.26.3_2026-05-22 + - go1.26.3_2026-06-15 # Tests command definitions. Use the entire "docker compose" command you want to run. tests: # Run ./test.sh --help for a description of each of the flags. @@ -66,7 +66,7 @@ jobs: # use in tests. It will be set appropriately for each tag in the list # defined in the matrix. BOULDER_TOOLS_TAG: ${{ matrix.BOULDER_TOOLS_TAG }} - BOULDER_VTCOMBOSERVER_TAG: vitessv23.0.0_2026-03-05 + BOULDER_VTCOMBOSERVER_TAG: vitessv23.0.0_2026-06-09 # Sequence of tasks that will be executed as part of the job. steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 41cbe01b27d..29ad77952f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,8 +22,8 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.gitignore b/.gitignore index 6545c0c980a..d3b34996c98 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ test/secrets/badkeyrevoker_dburl test/secrets/cert_checker_dburl test/secrets/incidents_dburl test/secrets/incidents_admin_dburl +test/secrets/mtpublisher_dburl test/secrets/revoker_dburl test/secrets/sa_dburl test/secrets/sa_ro_dburl diff --git a/Makefile b/Makefile index cb38dc8a95e..7902e4d1a58 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,8 @@ VERSION ?= 1.0.0 EPOCH ?= 1 MAINTAINER ?= "Community" +GO ?= go + # TODO(#8410): Remove pardot-test-srv when we've fully migrated to # salesforce-test-srv. CMDS = admin boulder ceremony ct-test-srv salesforce-test-srv pardot-test-srv chall-test-srv zendesk-test-srv @@ -44,7 +46,7 @@ bin/pardot-test-srv: bin/salesforce-test-srv build_cmds: | $(OBJDIR) echo $(OBJECTS) - GOBIN=$(OBJDIR) go install -mod=vendor $(GO_BUILD_FLAGS) ./... + GOBIN=$(OBJDIR) $(GO) install -mod=vendor $(GO_BUILD_FLAGS) ./... # Building a .deb requires `fpm` from https://github.com/jordansissel/fpm # which you can install with `gem install fpm`. diff --git a/bdns/dns.go b/bdns/dns.go index d6c050e58b0..bb147bb2d32 100644 --- a/bdns/dns.go +++ b/bdns/dns.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "net/http" "strconv" @@ -17,7 +18,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/metrics" ) @@ -226,7 +227,12 @@ func (c *impl) exchangeOne(ctx context.Context, hostname string, qtype uint16) ( }).Observe(rtt.Seconds()) if err != nil { - c.log.Infof("logDNSError chosenServer=[%s] hostname=[%s] queryType=[%s] err=[%s]", chosenServer, hostname, qtypeStr, err) + c.log.Info(ctx, "logDNSError", + slog.String("chosenServer", chosenServer), + slog.String("hostname", hostname), + slog.String("qtype", qtypeStr), + blog.Error(err), + ) // Check if the error is a network timeout, rather than a local context // timeout. If it is, retry instead of giving up. diff --git a/bdns/dns_test.go b/bdns/dns_test.go index be964aea14f..5c083d25155 100644 --- a/bdns/dns_test.go +++ b/bdns/dns_test.go @@ -22,7 +22,7 @@ import ( "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) @@ -283,7 +283,7 @@ func TestDNSNoServers(t *testing.T) { staticProvider, err := NewStaticProvider([]string{}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Hour, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Hour, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) _, resolver, err := obj.LookupA(context.Background(), "letsencrypt.org") test.AssertEquals(t, resolver, "") @@ -306,7 +306,7 @@ func TestDNSOneServer(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) _, resolver, err := obj.LookupA(context.Background(), "letsencrypt.org") test.AssertNotError(t, err, "No message") @@ -317,7 +317,7 @@ func TestDNSDuplicateServers(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr, dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) _, resolver, err := obj.LookupA(context.Background(), "letsencrypt.org") test.AssertNotError(t, err, "No message") @@ -328,7 +328,7 @@ func TestDNSServFail(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) bad := "servfail.com" _, _, err = obj.LookupTXT(context.Background(), "servfail.com") @@ -348,7 +348,7 @@ func TestDNSLookupTXT(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) _, _, err = obj.LookupTXT(context.Background(), "letsencrypt.org") test.AssertNotError(t, err, "No message") @@ -363,7 +363,7 @@ func TestDNSLookupA(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) for _, tc := range []struct { name string @@ -448,7 +448,7 @@ func TestDNSLookupAAAA(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) for _, tc := range []struct { name string @@ -533,7 +533,7 @@ func TestDNSNXDOMAIN(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) hostname := "nxdomain.letsencrypt.org" _, _, err = obj.LookupA(context.Background(), hostname) @@ -551,7 +551,7 @@ func TestDNSLookupCAA(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) + obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.NewMock(), tlsConfig) removeIDExp := regexp.MustCompile(" id: [[:digit:]]+") caas, resolver, err := obj.LookupCAA(context.Background(), "bracewel.net") @@ -759,7 +759,7 @@ func TestRetry(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), tc.maxTries, "", blog.UseMock(), tlsConfig) + testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), tc.maxTries, "", blog.NewMock(), tlsConfig) dr := testClient.(*impl) dr.exchanger = tc.te _, _, err = dr.LookupTXT(context.Background(), "example.com") @@ -796,7 +796,7 @@ func TestRetryMetrics(t *testing.T) { // context itself being cancelled. It should never see the error in the // testExchanger, because the fake exchanger (like the real http package) // checks for cancellation before doing any work. - testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, "", blog.UseMock(), tlsConfig) + testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, "", blog.NewMock(), tlsConfig) dr := testClient.(*impl) dr.exchanger = &testExchanger{errs: []error{errors.New("oops")}} ctx, cancel := context.WithCancel(t.Context()) @@ -815,7 +815,7 @@ func TestRetryMetrics(t *testing.T) { // Same as above, except rather than cancelling the context ourselves, we // let the go runtime cancel it as a result of a deadline in the past. - testClient = New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, "", blog.UseMock(), tlsConfig) + testClient = New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, "", blog.NewMock(), tlsConfig) dr = testClient.(*impl) dr.exchanger = &testExchanger{errs: []error{errors.New("oops")}} ctx, cancel = context.WithTimeout(t.Context(), -10*time.Hour) @@ -883,7 +883,7 @@ func TestRotateServerOnErr(t *testing.T) { test.AssertNotError(t, err, "Got error creating StaticProvider") maxTries := 5 - client := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), maxTries, "", blog.UseMock(), tlsConfig) + client := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), maxTries, "", blog.NewMock(), tlsConfig) // Configure a mock exchanger that will always return a retryable error for // servers A and B. This will force server "[2606:4700:4700::1111]:53" to do @@ -948,7 +948,7 @@ func TestDOHMetric(t *testing.T) { staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr}) test.AssertNotError(t, err, "Got error creating StaticProvider") - testClient := New(time.Second*11, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 0, "", blog.UseMock(), tlsConfig) + testClient := New(time.Second*11, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 0, "", blog.NewMock(), tlsConfig) resolver := testClient.(*impl) resolver.exchanger = &dohAlwaysRetryExchanger{err: &url.Error{Op: "read", Err: testTimeoutError(true)}} diff --git a/blog/attr.go b/blog/attr.go new file mode 100644 index 00000000000..f2ce2c3c56d --- /dev/null +++ b/blog/attr.go @@ -0,0 +1,61 @@ +package blog + +// This file contains helper functions that can be used throughout the boulder +// code base to ensure that certain commonly-logged values always have the same +// key name and value type. This prevents situations like sometimes calling the +// requesting account "requester" or "acct" or "regID"; or sometimes logging the +// authz ID as an integer and sometimes as a string. +// +// Any time we find ourselves logging the same slog.Attr from 3+ files we +// should consider adding a helper here instead. +// +// Note that several other attr keys are reserved and should not be used: +// - "time": used by the slog package +// - "level": used by the slog package +// - "msg": used by the slog package +// - "source": used by the slog package +// - "error": used by our blog.Error and blog.AuditError helpers +// - "audit": used by our blog.AuditError and blog.AuditInfo helpers + +import ( + "log/slog" + + "github.com/letsencrypt/boulder/identifier" +) + +// Acct returns a slog.Attr whose key is "acct" and whose value is the unique +// numeric ID of the account. +func Acct(acctID int64) slog.Attr { + return slog.Int64("acct", acctID) +} + +// Order returns a slog.Attr whose key is "order" and whose value is the unique +// numeric ID of the order. +func Order(orderID int64) slog.Attr { + return slog.Int64("order", orderID) +} + +// Authz returns a slog.Attr whose key is "authz" and whose value is the unique +// numeric ID of the authz. +func Authz(authzID int64) slog.Attr { + return slog.Int64("authz", authzID) +} + +// Serial returns a slog.Attr whose key is "serial" and whose value is the +// given string. The argument should be hex-encoded. +func Serial(serial string) slog.Attr { + return slog.String("serial", serial) +} + +// Idents returns a slog.Attr whose key is "idents" and whose value is a list +// of the given identifiers. +func Idents(idents ...identifier.ACMEIdentifier) slog.Attr { + return slog.Any("idents", idents) +} + +// Error returns a slog.Attr whose key is "error" and whose value is the value +// from err.Error(). This attribute is used automatically by methods that log +// at the error level, like blog.Logger.AuditError(). +func Error(err error) slog.Attr { + return slog.String("error", err.Error()) +} diff --git a/blog/attr_test.go b/blog/attr_test.go new file mode 100644 index 00000000000..37828660afa --- /dev/null +++ b/blog/attr_test.go @@ -0,0 +1,89 @@ +package blog + +import ( + "errors" + "log/slog" + "net/netip" + "testing" + + "github.com/letsencrypt/boulder/identifier" +) + +func TestAttrHelpers(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + got slog.Attr + wantKey string + wantVal slog.Value + }{ + { + name: "Acct", + got: Acct(42), + wantKey: "acct", + wantVal: slog.Int64Value(42), + }, + { + name: "Order", + got: Order(17), + wantKey: "order", + wantVal: slog.Int64Value(17), + }, + { + name: "Authz", + got: Authz(99), + wantKey: "authz", + wantVal: slog.Int64Value(99), + }, + { + name: "Serial", + got: Serial("deadbeef"), + wantKey: "serial", + wantVal: slog.StringValue("deadbeef"), + }, + { + name: "Error", + got: Error(errors.New("boom")), + wantKey: "error", + wantVal: slog.StringValue("boom"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.got.Key != tc.wantKey { + t.Errorf("attr key = %q, want %q", tc.got.Key, tc.wantKey) + } + if !tc.got.Value.Equal(tc.wantVal) { + t.Errorf("attr value = %v, want %v", tc.got.Value, tc.wantVal) + } + }) + } +} + +func TestIdentsAttr(t *testing.T) { + t.Parallel() + + // This test is separate from the above because the Idents helper accepts + // a variadic number of arguments. + attr := Idents(identifier.NewDNS("example.com"), identifier.NewIP(netip.MustParseAddr("12.34.56.78"))) + if attr.Key != "idents" { + t.Errorf("attr key = %q, want %q", attr.Key, "idents") + } + + idents, ok := attr.Value.Any().([]identifier.ACMEIdentifier) + if !ok { + t.Fatalf("idents attr value should be a slice of ACMEIdentifier, got %T", attr.Value.Any()) + } + if len(idents) != 2 { + t.Fatalf("got %d idents, want 2", len(idents)) + } + if idents[0].Value != "example.com" { + t.Errorf("idents[0].Value = %q, want %q", idents[0].Value, "example.com") + } + if idents[1].Value != "12.34.56.78" { + t.Errorf("idents[1].Value = %q, want %q", idents[1].Value, "12.34.56.78") + } +} diff --git a/blog/audit.go b/blog/audit.go new file mode 100644 index 00000000000..2180d037421 --- /dev/null +++ b/blog/audit.go @@ -0,0 +1,131 @@ +package blog + +// This file provides the scaffolding necessary to differentiate audit logs from +// non-audit logs. This consists of four parts: +// +// 1. A singleton slog.Attr which can be attached to audit records by the +// AuditLevel methods. +// 2. A Handler which contains two different sub-handlers, and which dispatches +// each Record to one of those handlers depending on whether that Record +// contains the singleton audit Attr. +// 3. An io.Writer which prepends the text `[AUDIT] ` to all messages written +// to it. +// 4. Finally, a constructor which accepts the same arguments as slog's +// NewTextHandler and NewJSONHandler and builds two copies of the handler, +// one of which has its writer wrapped in the auditWriter. + +import ( + "bytes" + "context" + "io" + "log/slog" +) + +// auditKey is the key used to identify the auditAttr. +const auditKey = "audit" + +// auditAttr is a singleton slog.Attr which is added to Records by AuditError +// and AuditInfo, and detected on records by auditHandler.Handle to decide +// which sub-Handler the Record should be routed to. +var auditAttr = slog.Bool(auditKey, true) + +// auditWriter implements the io.Writer interface. It prepends the string +// `[AUDIT] ` to each line written to it. +type auditWriter struct { + inner io.Writer +} + +var _ io.Writer = (*auditWriter)(nil) + +// Write implements the io.Writer interface. It prepends the string `[AUDIT] ` +// to its input and forwards the result to the inner io.Writer. +// +// The slog package guarantees that "each call to Handle results in a single +// serialized call to io.Writer.Write". Similarly, each call to this method also +// results in a single call to the wrapped io.Writer.Write. This means that we +// are prepending the audit tag exactly once per call to slog.Logger.Handle. +func (w *auditWriter) Write(in []byte) (int, error) { + out := bytes.Buffer{} + out.WriteString("[AUDIT] ") + out.Write(in) + _, err := out.WriteTo(w.inner) + // Return len(in), rather than the length returned from WriteTo, because the + // io.Writer contract is to return how many bytes *of the input* you wrote. + return len(in), err +} + +// newAuditHandler creates an auditHandler, wrapping the underlying handler +// (either text or json) provided by the go standard library. Which handler is +// wrapped is determined by the build environment: json for prod, and text for +// integration tests. +func newAuditHandler(w io.Writer, opts *slog.HandlerOptions) *auditHandler { + origReplaceAttr := opts.ReplaceAttr + opts.ReplaceAttr = func(groups []string, attr slog.Attr) slog.Attr { + // Since the auditWriter will add the [AUDIT] tag to its log lines, we don't + // want to log the audit=true attr itself. We check for full equality here, + // whereas Handle just checks for auditKey, to avoid dropping anything if + // someone accidentally and incorrectly adds an attr like + // slog.String("audit", "Here's some really important text"). + if attr.Equal(auditAttr) { + return slog.Attr{} + } + if origReplaceAttr != nil { + return origReplaceAttr(groups, attr) + } + return attr + } + + return &auditHandler{ + audit: stdlibHandler(&auditWriter{inner: w}, opts), + plain: stdlibHandler(w, opts), + } +} + +// auditHandler is a slog.Handler whose Enabled, WithAttr, and WithGroup methods +// call the corresponding methods on each of the wrapped Handlers, but whose +// Handle method calls the corresponding method only on one or the other of the +// wrapped Handlers, depending on whether the slog.Record indicates that this +// log line is an audit log or not. +type auditHandler struct { + audit slog.Handler + plain slog.Handler +} + +var _ slog.Handler = (*auditHandler)(nil) + +// Enabled returns true if either wrapped handler is enabled. Both wrapped +// Handlers should have been constructed with the same HandlerOptions, and +// therefore the same Leveler, so there should never be a discrepancy. +func (h *auditHandler) Enabled(ctx context.Context, l slog.Level) bool { + return h.audit.Enabled(ctx, l) || h.plain.Enabled(ctx, l) +} + +// Handle calls Handle on either the wrapped audit Handler or the wrapped plain +// Handler, depending on whether or not the input Record contains an attr with +// the audit key. +func (h *auditHandler) Handle(ctx context.Context, r slog.Record) error { + handler := h.plain + for attr := range r.Attrs { + if attr.Key == auditKey { + handler = h.audit + break + } + } + return handler.Handle(ctx, r) +} + +// WithAttrs calls WithAttrs on both wrapped Handlers. +func (h *auditHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &auditHandler{ + audit: h.audit.WithAttrs(attrs), + plain: h.plain.WithAttrs(attrs), + } +} + +// WithGroup calls WithGroup on both wrapped Handlers. +func (h *auditHandler) WithGroup(name string) slog.Handler { + return &auditHandler{ + audit: h.audit.WithGroup(name), + plain: h.plain.WithGroup(name), + } +} diff --git a/blog/audit_test.go b/blog/audit_test.go new file mode 100644 index 00000000000..56e7d8a49b6 --- /dev/null +++ b/blog/audit_test.go @@ -0,0 +1,69 @@ +package blog + +import ( + "bytes" + "testing" +) + +func TestAuditWriter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in string + want string + }{ + { + name: "empty", + in: "", + want: "[AUDIT] ", + }, + { + name: "simple", + in: "hello, world", + want: "[AUDIT] hello, world", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + w := &auditWriter{inner: &buf} + n, err := w.Write([]byte(tc.in)) + if err != nil { + t.Fatalf("auditWriter.Write returned error: %s", err) + } + if n != len(tc.in) { + t.Errorf("auditWriter.Write returned n=%d, want %d", n, len(tc.in)) + } + got := buf.String() + if got != tc.want { + t.Errorf("auditWriter wrote %q, want %q", got, tc.want) + } + }) + } + + // Each call to Write produces its own [AUDIT]-prefixed line. + t.Run("multiple writes", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + w := &auditWriter{inner: &buf} + for _, line := range []string{"foo", "bar"} { + n, err := w.Write([]byte(line)) + if err != nil { + t.Fatalf("auditWriter.Write(%q) returned error: %s", line, err) + } + if n != len(line) { + t.Errorf("auditWriter.Write(%q) returned n=%d, want %d", line, n, len(line)) + } + } + want := "[AUDIT] foo[AUDIT] bar" + got := buf.String() + if got != want { + t.Errorf("auditWriter wrote %q, want %q", got, want) + } + }) +} diff --git a/blog/checksum.go b/blog/checksum.go new file mode 100644 index 00000000000..5b696954b5d --- /dev/null +++ b/blog/checksum.go @@ -0,0 +1,64 @@ +package blog + +// This file implements our ability to prepend a checksum to the beginning of +// every log line. Since we want to be using the built-in TextHandler and +// JSONHandler, we have to do this at the level of an io.Writer which we pass +// to those Handlers. + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "hash/crc32" + "io" +) + +// newChecksumWriter returns a checksumWriter which wraps the given io.Writer. +func newChecksumWriter(inner io.Writer) *checksumWriter { + return &checksumWriter{inner: inner} +} + +// checksumWriter implements the io.Writer interface. It computes the CRC32 +// checksum of each line written to it before passing the result through to a +// wrapped io.Writer. It is intended for use as the io.Writer passed to a slog +// Handler. +type checksumWriter struct { + inner io.Writer +} + +var _ io.Writer = (*checksumWriter)(nil) + +// Write implements the io.Writer interface. It computes the CRC32 checksum of +// its input, concatenates the checksum and the original input separated by a +// space, and forwards the result to the inner io.Writer. +// +// The slog package guarantees that "each call to Handle results in a single +// serialized call to io.Writer.Write". Similarly, each call to this method also +// results in a single call to the wrapped io.Writer.Write. This means that we +// are computing writing exactly one checksum per call to slog.Logger.Handle. +func (w *checksumWriter) Write(in []byte) (int, error) { + // slog handlers terminate each record with a trailing newline. rsyslog + // strips that newline before writing the line to disk, and the log-validator + // computes its checksum over the stripped line. Strip it here too so the + // checksum we emit covers exactly the bytes the validator will see. + body := bytes.TrimSuffix(in, []byte{'\n'}) + out := bytes.Buffer{} + out.WriteString(LogLineChecksum(string(body))) + out.WriteString(" ") + out.Write(in) + _, err := out.WriteTo(w.inner) + // Return len(in), rather than the length returned from WriteTo, because the + // io.Writer contract is to return how many bytes *of the input* you wrote. + return len(in), err +} + +// LogLineChecksum computes a CRC32 over the log line, which can be checked to +// ensure no unexpected log corruption has occurred. This function is exported +// for use by the log-validator. +func LogLineChecksum(line string) string { + crc := crc32.ChecksumIEEE([]byte(line)) + buf := make([]byte, crc32.Size) + // Error is unreachable because we provide a supported type and buffer size + _, _ = binary.Encode(buf, binary.LittleEndian, crc) + return base64.RawURLEncoding.EncodeToString(buf) +} diff --git a/blog/checksum_test.go b/blog/checksum_test.go new file mode 100644 index 00000000000..a04f35731e5 --- /dev/null +++ b/blog/checksum_test.go @@ -0,0 +1,123 @@ +package blog + +import ( + "bytes" + "testing" +) + +func TestLogLineChecksum(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + line string + want string + }{ + { + name: "empty", + line: "", + // CRC32 of the empty string is 0. + want: "AAAAAA", + }, + { + name: "simple", + line: "hello, world", + // Deterministic base64url(CRC32("hello, world")). + want: "OnKr_w", + }, + { + name: "newline", + line: "hello, world\n", + // LogLineChecksum hashes every byte, so the trailing newline changes it. + want: "U3Qk9A", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := LogLineChecksum(tc.line) + if got != tc.want { + t.Errorf("LogLineChecksum(%q) = %q, want %q", tc.line, got, tc.want) + } + // Checksum should be deterministic across repeated calls. + if again := LogLineChecksum(tc.line); again != got { + t.Errorf("LogLineChecksum(%q) not deterministic: got %q then %q", tc.line, got, again) + } + }) + } + + // Different inputs produce different checksums. + if LogLineChecksum("foo") == LogLineChecksum("bar") { + t.Errorf("LogLineChecksum(%q) and LogLineChecksum(%q) should differ", "foo", "bar") + } +} + +func TestChecksumWriter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in string + want string + }{ + { + name: "empty", + in: "", + want: "AAAAAA ", + }, + { + name: "simple", + in: "hello, world", + want: "OnKr_w hello, world", + }, + { + name: "newline", + in: "hello, world\n", + // The checksumWriter knows that trailing newlines are actually line + // terminators, not part of the line itself, so it discards them before + // computing the checksum. Therefore the checksum should be the same. + want: "OnKr_w hello, world\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + w := newChecksumWriter(&buf) + n, err := w.Write([]byte(tc.in)) + if err != nil { + t.Fatalf("checksumWriter.Write returned error: %s", err) + } + if n != len(tc.in) { + t.Errorf("checksumWriter.Write returned n=%d, want %d", n, len(tc.in)) + } + if got := buf.String(); got != tc.want { + t.Errorf("checksumWriter wrote %q, want %q", got, tc.want) + } + }) + } + + // Each call to Write produces its own checksum-prefixed line. + t.Run("multiple writes", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + w := newChecksumWriter(&buf) + for _, line := range []string{"foo", "bar"} { + n, err := w.Write([]byte(line)) + if err != nil { + t.Fatalf("checksumWriter.Write(%q) returned error: %s", line, err) + } + if n != len(line) { + t.Errorf("checksumWriter.Write(%q) returned n=%d, want %d", line, n, len(line)) + } + } + want := LogLineChecksum("foo") + " foo" + LogLineChecksum("bar") + " bar" + if got := buf.String(); got != want { + t.Errorf("checksumWriter wrote %q, want %q", got, want) + } + }) +} diff --git a/blog/config.go b/blog/config.go new file mode 100644 index 00000000000..280eaae3f4e --- /dev/null +++ b/blog/config.go @@ -0,0 +1,48 @@ +package blog + +// This file specifies the format used to configure our loggers. It is +// embedded in almost every Config struct in //cmd/*/main.go. + +import ( + "log/slog" +) + +// Config defines the config for logging to syslog and stdout/stderr. The +// level meanings are as follows: +// +// -1: suppress all output +// 0: default, which is -1 for stdout and 6 for syslog +// 3: log only errors +// 4: log warnings and above +// 6: log info and above +// 7: log debug and above +// +// Values less than -1 or greater than 7 are invalid. Values in between the +// numbers documented above (e.g. 1) have the same effect as the next larger +// value (e.g. 3). +type Config struct { + // When absent or zero, this causes no logs to be emitted on stdout/stderr. + // Errors and warnings will be emitted on stderr if the configured level + // allows. + StdoutLevel int `validate:"min=-1,max=7"` + // When absent or zero, this defaults to logging all messages of level 6 + // or below. To disable syslog logging entirely, set this to -1. + SyslogLevel int `validate:"min=-1,max=7"` +} + +// syslogToSlogLevelMap allows us to map the integers used in our log config +// (which originally come from syslog levels) to the values used by slog. +func configToSlogLevel(l int) slog.Level { + switch l { + case 1, 2, 3: + return slog.LevelError + case 4, 5: + return slog.LevelWarn + case 6: + return slog.LevelInfo + case 7: + return slog.LevelDebug + default: + return slog.LevelInfo + } +} diff --git a/blog/config_test.go b/blog/config_test.go new file mode 100644 index 00000000000..2443d5e94f1 --- /dev/null +++ b/blog/config_test.go @@ -0,0 +1,38 @@ +package blog + +import ( + "log/slog" + "testing" +) + +func TestConfigToSlogLevel(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in int + want slog.Level + }{ + {name: "1 -> Error", in: 1, want: slog.LevelError}, + {name: "2 -> Error", in: 2, want: slog.LevelError}, + {name: "3 -> Error", in: 3, want: slog.LevelError}, + {name: "4 -> Warn", in: 4, want: slog.LevelWarn}, + {name: "5 -> Warn", in: 5, want: slog.LevelWarn}, + {name: "6 -> Info", in: 6, want: slog.LevelInfo}, + {name: "7 -> Debug", in: 7, want: slog.LevelDebug}, + // Unspecified values fall through to Info. + {name: "0 -> Info (default)", in: 0, want: slog.LevelInfo}, + {name: "-1 -> Info (default)", in: -1, want: slog.LevelInfo}, + {name: "99 -> Info (default)", in: 99, want: slog.LevelInfo}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := configToSlogLevel(tc.in) + if got != tc.want { + t.Errorf("configToSlogLevel(%d) = %s, want %s", tc.in, got, tc.want) + } + }) + } +} diff --git a/blog/context.go b/blog/context.go new file mode 100644 index 00000000000..26584c11824 --- /dev/null +++ b/blog/context.go @@ -0,0 +1,81 @@ +package blog + +// This file provides an exported utility for attaching slog.Attrs to a context +// object, so that they will be included in all subsequent log output. +// +// It also implements a slog.Handler which extracts stored attrs from a context +// and includes them in the resulting log line. This handler is always included +// as part of the handler chain in blog.New(). + +import ( + "context" + "log/slog" + "slices" +) + +// sloggerCtxKeyType exists to ensure that sloggerCtxKey is a wholly unique +// singleton that cannot collide with context keys used by other packages. +type sloggerCtxKeyType struct{} + +// sloggerCtxKey is the unique key used by this package to store and retrieve +// the slog.Attrs stored on a context.Context. +var sloggerCtxKey = sloggerCtxKeyType{} + +// fromContext retrieves the slog.Attrs from the context. It returns a copy to +// prevent callers from accidentally modifying the context's attrs in place. It +// returns nil if no attributes are attached. +func fromContext(ctx context.Context) []slog.Attr { + attrs, ok := ctx.Value(sloggerCtxKey).([]slog.Attr) + if attrs == nil || !ok { + return nil + } + return slices.Clone(attrs) +} + +// ContextWith returns a new context with the given attributes attached, in +// addition to any already-attached attrs. All subsequent log calls to which the +// resulting context is passed will include the provided slog.Attrs. +func ContextWith(ctx context.Context, attrs ...slog.Attr) context.Context { + a := append(fromContext(ctx), attrs...) + return context.WithValue(ctx, sloggerCtxKey, a) +} + +// contextHandler wraps another slog.Handler, extracting slog.Attrs from the +// context passed to Handle calls and attaching them to the resulting Record. +type contextHandler struct { + inner slog.Handler +} + +// Enabled reports whether the inner handler handles records at the given level. +func (c *contextHandler) Enabled(ctx context.Context, l slog.Level) bool { + return c.inner.Enabled(ctx, l) +} + +// Handle extracts the attributes attached to the context object, attaches them +// to the Record, and then passes it through to the underlying Handler. +// +// Order matters. Attributes from the Context will be emitted after any +// attributes already present in the Record. Callers that want a given attribute +// to be emitted last in log lines (e.g. an error) should ensure that attribute +// is (a) provided in the Context and (b) the last in the Context's list of +// attributes. See the logger.Error() implementation in this package for an +// example. +func (c *contextHandler) Handle(ctx context.Context, r slog.Record) error { + r = r.Clone() + r.AddAttrs(fromContext(ctx)...) + return c.inner.Handle(ctx, r) +} + +// WithAttrs returns a new contextHandler wrapping the inner handler with the +// given attrs added. We must implement this (rather than relying on embedding) +// so that the resulting handler remains a contextHandler, preserving context +// attr extraction in downstream slog.Logger.With calls. +func (c *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &contextHandler{inner: c.inner.WithAttrs(attrs)} +} + +// WithGroup returns a new contextHandler wrapping the inner handler with the +// given group name. See WithAttrs for why we implement this explicitly. +func (c *contextHandler) WithGroup(name string) slog.Handler { + return &contextHandler{inner: c.inner.WithGroup(name)} +} diff --git a/blog/context_test.go b/blog/context_test.go new file mode 100644 index 00000000000..787555ca481 --- /dev/null +++ b/blog/context_test.go @@ -0,0 +1,117 @@ +package blog + +import ( + "context" + "log/slog" + "strings" + "testing" +) + +func TestContextWith(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + ctxFn func(context.Context) context.Context + wantKeys []string + wantLog []string + }{ + { + name: "empty", + ctxFn: func(ctx context.Context) context.Context { return ctx }, + }, + { + name: "single layer", + ctxFn: func(ctx context.Context) context.Context { + return ContextWith(ctx, slog.String("k1", "v1"), slog.Int("k2", 2)) + }, + wantKeys: []string{"k1", "k2"}, + wantLog: []string{"k1=v1", "k2=2"}, + }, + { + name: "multi-layer", + ctxFn: func(ctx context.Context) context.Context { + // ContextWith should append to any existing attrs, not replace them. + ctx = ContextWith(ctx, slog.String("k1", "v1")) + ctx = ContextWith(ctx, slog.String("k2", "v2")) + ctx = ContextWith(ctx, slog.String("k3", "v3")) + return ctx + }, + wantKeys: []string{"k1", "k2", "k3"}, + wantLog: []string{"k1=v1", "k2=v2", "k3=v3"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := tc.ctxFn(t.Context()) + + attrs := fromContext(ctx) + if len(attrs) != len(tc.wantKeys) { + t.Fatalf("fromContext returned %d attrs, want %d", len(attrs), len(tc.wantKeys)) + } + for i, want := range tc.wantKeys { + if attrs[i].Key != want { + t.Errorf("attrs[%d].Key = %q, want %q", i, attrs[i].Key, want) + } + } + + l := NewMock() + l.Info(ctx, "hello") + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + for _, want := range tc.wantLog { + if !strings.Contains(got[0], want) { + t.Errorf("log line %q does not contain %q", got[0], want) + } + } + }) + } +} + +func TestSiblingContextIsolation(t *testing.T) { + t.Parallel() + + // Build a parent whose attrs slice has spare capacity. Without a defensive + // copy in ContextWith, two sibling appends share this backing array and + // trample each other's writes. + parentAttrs := make([]slog.Attr, 0, 4) + parentAttrs = append(parentAttrs, + slog.String("a", "1"), + slog.String("b", "2"), + slog.String("c", "3"), + ) + parent := context.WithValue(t.Context(), sloggerCtxKey, parentAttrs) + + child1 := ContextWith(parent, slog.String("child", "one")) + child2 := ContextWith(parent, slog.String("child", "two")) + + attrs1 := fromContext(child1) + last1 := attrs1[len(attrs1)-1].Value.String() + if last1 != "one" { + t.Errorf("child1's appended attr = %q, want %q (sibling overwrote it)", last1, "one") + } + attrs2 := fromContext(child2) + last2 := attrs2[len(attrs2)-1].Value.String() + if last2 != "two" { + t.Errorf("child2's appended attr = %q, want %q", last2, "two") + } + + l := NewMock() + l.Info(child1, "from child1") + l.Info(child2, "from child2") + got := l.GetAll() + if len(got) != 2 { + t.Fatalf("got %d log lines, want 2: %v", len(got), got) + } + if !strings.Contains(got[0], "child=one") { + t.Errorf("log line for child1 %q should contain child=one", got[0]) + } + if !strings.Contains(got[1], "child=two") { + t.Errorf("log line for child2 %q should contain child=two", got[1]) + } +} diff --git a/blog/logger.go b/blog/logger.go new file mode 100644 index 00000000000..de262925178 --- /dev/null +++ b/blog/logger.go @@ -0,0 +1,129 @@ +package blog + +// This file defines the core interface of the blog package: blog.Logger, and +// its constructor, blog.New. This type has methods for emitting logs at each +// level, and for emitting audit logs at the info and error levels. It is +// expected that users of this package will create a single top-level logger, +// store it on a persistent object (such as a gRPC or HTTP handler struct), +// attach relevant attributes to a context.Context which flows through methods +// on that struct, and pass the context to the stored logger at each call site. + +import ( + "context" + "errors" + "fmt" + "log/slog" + "log/syslog" + "os" + + "github.com/letsencrypt/boulder/core" +) + +// Logger is a wrapper around slog.Logger. It exposes methods whose signatures +// require that a context be provided (from which slog Attrs will be extracted), +// and that any additional attributes be presented as slog.Attrs (not as +// comma-separated keys and values). It does not provide affordances for +// deriving a child logger with additional attrs attached; calling code should +// attach such persistent attributes to its context object instead. +type Logger interface { + Error(context.Context, string, error, ...slog.Attr) + Warn(context.Context, string, ...slog.Attr) + Info(context.Context, string, ...slog.Attr) + Debug(context.Context, string, ...slog.Attr) + AuditError(context.Context, string, error, ...slog.Attr) + AuditInfo(context.Context, string, ...slog.Attr) +} + +// logger implements the Logger interface. +type logger struct { + inner *slog.Logger +} + +// New returns a Logger per the config. The logger extracts slog.Attrs from the +// context, prepends the [AUDIT] tag to calls to its Audit* methods, prepends a +// checksum to all messages, and then writes the resulting log messages to +// stdout and/or syslog as configured. +// +// Cannot error if only the stdout logger is enabled (has a non-negative level). +func New(conf Config) (*logger, error) { + var stdoutHandler slog.Handler + if conf.StdoutLevel > 0 { + writer := newChecksumWriter(os.Stdout) + opts := &slog.HandlerOptions{Level: configToSlogLevel(conf.StdoutLevel)} + stdoutHandler = &contextHandler{inner: newAuditHandler(writer, opts)} + } + + var syslogHandler slog.Handler + if conf.SyslogLevel == 0 { + conf.SyslogLevel = 6 + } + if conf.SyslogLevel > 0 { + syslogger, err := syslog.Dial("", "", syslog.LOG_INFO, core.Command()) + if err != nil { + return nil, fmt.Errorf("failed to connect to syslog: %w", err) + } + + writer := newChecksumWriter(syslogger) + opts := &slog.HandlerOptions{Level: configToSlogLevel(conf.SyslogLevel)} + syslogHandler = &contextHandler{inner: newAuditHandler(writer, opts)} + } + + var l *slog.Logger + switch { + case stdoutHandler != nil && syslogHandler != nil: + l = slog.New(slog.NewMultiHandler(stdoutHandler, syslogHandler)) + case stdoutHandler != nil: + l = slog.New(stdoutHandler) + case syslogHandler != nil: + l = slog.New(syslogHandler) + default: + return nil, errors.New("either StdoutLevel or SyslogLevel must be positive") + } + + l = l.With(universalAttrs()...) + + return &logger{inner: l}, nil +} + +// Error logs the given message, error, and other key-value pairs at error +// level. The error will be included in the attrs under the key "error". +func (l *logger) Error(ctx context.Context, msg string, err error, attrs ...slog.Attr) { + // We attach these attrs to the context, rather than passing them directly to + // LogAttrs, to ensure that they come last in the log line. See + // contextHandler.Handle() for more information. + ctx = ContextWith(ctx, append(attrs, Error(err))...) + l.inner.LogAttrs(ctx, slog.LevelError, msg) +} + +// Warn logs the given message and other key-value pairs at warning level. +func (l *logger) Warn(ctx context.Context, msg string, attrs ...slog.Attr) { + ctx = ContextWith(ctx, attrs...) + l.inner.LogAttrs(ctx, slog.LevelWarn, msg) +} + +// Info logs the given message and other key-value pairs at info level. +func (l *logger) Info(ctx context.Context, msg string, attrs ...slog.Attr) { + ctx = ContextWith(ctx, attrs...) + l.inner.LogAttrs(ctx, slog.LevelInfo, msg) +} + +// Debug logs the given message and other key-value pairs at debug level. +func (l *logger) Debug(ctx context.Context, msg string, attrs ...slog.Attr) { + ctx = ContextWith(ctx, attrs...) + l.inner.LogAttrs(ctx, slog.LevelDebug, msg) +} + +// AuditError logs the given message, error, and other key-value pairs at error +// level and with the audit tag. The error will be included in the attrs under +// the key "error". +func (l *logger) AuditError(ctx context.Context, msg string, err error, attrs ...slog.Attr) { + ctx = ContextWith(ctx, append(attrs, auditAttr, Error(err))...) + l.inner.LogAttrs(ctx, slog.LevelError, msg) +} + +// AuditInfo logs the given message and other key-value pairs at info level and +// with the audit tag. +func (l *logger) AuditInfo(ctx context.Context, msg string, attrs ...slog.Attr) { + ctx = ContextWith(ctx, append(attrs, auditAttr)...) + l.inner.LogAttrs(ctx, slog.LevelInfo, msg) +} diff --git a/blog/logger_test.go b/blog/logger_test.go new file mode 100644 index 00000000000..2785f542c7d --- /dev/null +++ b/blog/logger_test.go @@ -0,0 +1,190 @@ +package blog + +import ( + "context" + "errors" + "log/slog" + "strings" + "testing" +) + +func TestNew(t *testing.T) { + t.Parallel() + + // Both levels suppressed should return an error. + _, err := New(Config{StdoutLevel: -1, SyslogLevel: -1}) + if err == nil { + t.Errorf("New with both levels suppressed should error, got nil") + } + + // An stdout-only logger should be constructable. + l, err := New(Config{StdoutLevel: 6, SyslogLevel: -1}) + if err != nil { + t.Fatalf("New with stdout enabled should succeed, got: %s", err) + } + if l == nil { + t.Errorf("New should return a non-nil logger") + } +} + +func TestLoggerMethods(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + logFn func(l Logger) + wantMsg string + wantLvl string + wantErr string + wantAttr string + audit bool + }{ + { + name: "Info", + logFn: func(l Logger) { l.Info(context.Background(), "hi there") }, + wantMsg: `msg="hi there"`, + wantLvl: "level=INFO", + }, + { + name: "Debug", + logFn: func(l Logger) { l.Debug(context.Background(), "debug me") }, + wantMsg: `msg="debug me"`, + wantLvl: "level=DEBUG", + }, + { + name: "Warn", + logFn: func(l Logger) { l.Warn(context.Background(), "careful now") }, + wantMsg: `msg="careful now"`, + wantLvl: "level=WARN", + }, + { + name: "Error", + logFn: func(l Logger) { l.Error(context.Background(), "oh no", errors.New("boom")) }, + wantMsg: `msg="oh no"`, + wantLvl: "level=ERROR", + wantErr: `error=boom`, + }, + { + name: "AuditInfo", + logFn: func(l Logger) { l.AuditInfo(context.Background(), "important thing") }, + wantMsg: `msg="important thing"`, + wantLvl: "level=INFO", + audit: true, + }, + { + name: "AuditError", + logFn: func(l Logger) { l.AuditError(context.Background(), "audit err", errors.New("bad")) }, + wantMsg: `msg="audit err"`, + wantLvl: "level=ERROR", + wantErr: `error=bad`, + audit: true, + }, + { + name: "Info with attrs", + logFn: func(l Logger) { + l.Info(context.Background(), "with attrs", slog.String("foo", "bar"), slog.Int("n", 7)) + }, + wantMsg: `msg="with attrs"`, + wantLvl: "level=INFO", + wantAttr: `foo=bar n=7`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + l := NewMock() + tc.logFn(l) + + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + line := got[0] + + if !strings.Contains(line, tc.wantLvl) { + t.Errorf("log line %q does not contain %q", line, tc.wantLvl) + } + if !strings.Contains(line, tc.wantMsg) { + t.Errorf("log line %q does not contain %q", line, tc.wantMsg) + } + if tc.wantErr != "" && !strings.Contains(line, tc.wantErr) { + t.Errorf("log line %q does not contain %q", line, tc.wantErr) + } + if tc.wantAttr != "" && !strings.Contains(line, tc.wantAttr) { + t.Errorf("log line %q does not contain %q", line, tc.wantAttr) + } + if tc.audit && !strings.Contains(line, "[AUDIT]") { + t.Errorf("expected audit log line %q to contain [AUDIT]", line) + } + if !tc.audit && strings.Contains(line, "[AUDIT]") { + t.Errorf("non-audit log line %q should not contain [AUDIT]", line) + } + }) + } +} + +func TestLoggerIncludesContextAttrs(t *testing.T) { + t.Parallel() + + l := NewMock() + ctx := ContextWith(context.Background(), slog.String("request", "abc123")) + l.Info(ctx, "served", slog.Int("code", 200)) + + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + for _, want := range []string{"request=abc123", "code=200", "msg=served"} { + if !strings.Contains(got[0], want) { + t.Errorf("log line %q does not contain %q", got[0], want) + } + } +} + +func TestAuditAttrNotEmitted(t *testing.T) { + t.Parallel() + + // The audit=true attr is an internal marker and should not appear in the + // resulting output, even though it causes the [AUDIT] prefix to be added. + l := NewMock() + l.AuditInfo(context.Background(), "hello") + + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + if !strings.Contains(got[0], "[AUDIT]") { + t.Errorf("audit log line %q should contain [AUDIT]", got[0]) + } + if strings.Contains(got[0], "audit=true") { + t.Errorf("log line %q should not contain audit=true marker", got[0]) + } +} + +func TestLoggerChecksum(t *testing.T) { + t.Parallel() + + // Every log line emitted by a blog.Logger is prefixed with a space-separated + // base64-encoded CRC32 of the remaining line contents. + l := NewMock() + l.Info(context.Background(), "hello") + + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + + parts := strings.SplitN(got[0], " ", 2) + if len(parts) != 2 { + t.Fatalf("expected log line to have a space-separated checksum prefix, got %q", got[0]) + } + // The trailing newline is a line terminator, not part of the line, so + // strip it before computing the expected checksum. + body := strings.TrimSuffix(parts[1], "\n") + want := LogLineChecksum(body) + if parts[0] != want { + t.Errorf("checksum prefix %q does not match LogLineChecksum of remainder %q", parts[0], want) + } +} diff --git a/blog/mock.go b/blog/mock.go new file mode 100644 index 00000000000..14af2a9c0c5 --- /dev/null +++ b/blog/mock.go @@ -0,0 +1,93 @@ +package blog + +// This file provides an alternate constructor whose return value satisfies the +// blog.Logger interface, but which stores all logged lines in memory. Tests +// can also cast the logger to a blog.Mock, and then gain access to a suite of +// methods useful for making test assertions about the contents of those logs. + +import ( + "fmt" + "log/slog" + "regexp" + "slices" + "strings" + "sync" +) + +// inmemWriter implements the io.Writer interface, but saves all written bytes +// to an in-memory slice of strings for later inspection. +type inmemWriter struct { + sync.RWMutex + out []string +} + +func (iw *inmemWriter) Write(p []byte) (int, error) { + iw.Lock() + defer iw.Unlock() + iw.out = append(iw.out, string(p)) + return len(p), nil +} + +// Mock implements the blog.Logger interface by virtue of embedding a +// blog.logger which writes to an in-memory datastore. It also exports methods +// allowing callers to inspect all log lines which have been written to it. +type Mock struct { + *logger + iw *inmemWriter +} + +// NewMock returns an object which implements the blog.Logger interface, but +// which also exposes methods allowing callers to inspect all log lines written +// to it. It always uses the text (i.e. not json) format, and always logs at +// level 7 (debug). +func NewMock() *Mock { + w := &inmemWriter{} + l := slog.New(&contextHandler{inner: newAuditHandler( + newChecksumWriter(w), + &slog.HandlerOptions{Level: configToSlogLevel(7)}, + )}) + return &Mock{&logger{inner: l}, w} +} + +// GetAll returns all messages logged since instantiation or the last call to +// Clear(). +func (ml *Mock) GetAll() []string { + ml.iw.RLock() + defer ml.iw.RUnlock() + return slices.Clone(ml.iw.out) +} + +// GetAllMatching returns all messages logged since instantiation or the last +// Clear() whose text matches the given regexp. The regexp is +// accepted as a string and compiled on the fly, because convenience +// is more important than performance. +func (ml *Mock) GetAllMatching(reString string) []string { + ml.iw.RLock() + defer ml.iw.RUnlock() + + var matches []string + re := regexp.MustCompile(reString) + for _, logMsg := range ml.iw.out { + if re.MatchString(logMsg) { + matches = append(matches, logMsg) + } + } + return matches +} + +// ExpectMatch returns an error if no log lines matching the given regex have +// been logged since instantiation or the last Clear(). +func (ml *Mock) ExpectMatch(reString string) error { + results := ml.GetAllMatching(reString) + if len(results) == 0 { + return fmt.Errorf("expected log line %q, got %q", reString, strings.Join(ml.GetAll(), "\n")) + } + return nil +} + +// Clear resets the log buffer. +func (ml *Mock) Clear() { + ml.iw.Lock() + defer ml.iw.Unlock() + ml.iw.out = nil +} diff --git a/blog/mock_test.go b/blog/mock_test.go new file mode 100644 index 00000000000..842d4418ffc --- /dev/null +++ b/blog/mock_test.go @@ -0,0 +1,103 @@ +package blog + +import ( + "strings" + "testing" +) + +func TestMockGetAll(t *testing.T) { + t.Parallel() + + l := NewMock() + got := l.GetAll() + if len(got) != 0 { + t.Errorf("fresh mock has %d log lines, want 0", len(got)) + } + + l.Info(t.Context(), "first") + l.Info(t.Context(), "second") + l.Info(t.Context(), "third") + + got = l.GetAll() + if len(got) != 3 { + t.Fatalf("got %d log lines, want 3: %v", len(got), got) + } + for i, want := range []string{"first", "second", "third"} { + if !strings.Contains(got[i], want) { + t.Errorf("log line %d %q does not contain %q", i, got[i], want) + } + } +} + +func TestMockGetAllMatching(t *testing.T) { + t.Parallel() + + l := NewMock() + l.Info(t.Context(), "apple pie") + l.Info(t.Context(), "apple tart") + l.Info(t.Context(), "banana bread") + + apples := l.GetAllMatching("apple") + if len(apples) != 2 { + t.Errorf("GetAllMatching(%q) returned %d lines, want 2", "apple", len(apples)) + } + + bananas := l.GetAllMatching("banana") + if len(bananas) != 1 { + t.Errorf("GetAllMatching(%q) returned %d lines, want 1", "banana", len(bananas)) + } + + cherries := l.GetAllMatching("cherry") + if len(cherries) != 0 { + t.Errorf("GetAllMatching(%q) returned %d lines, want 0", "cherry", len(cherries)) + } + + // Regex metacharacters should work. + bread := l.GetAllMatching("bread|tart") + if len(bread) != 2 { + t.Errorf("GetAllMatching(%q) returned %d lines, want 2", "bread|tart", len(bread)) + } +} + +func TestMockExpectMatch(t *testing.T) { + t.Parallel() + + l := NewMock() + l.Info(t.Context(), "hello world") + + err := l.ExpectMatch("hello") + if err != nil { + t.Errorf("ExpectMatch(%q) returned unexpected error: %s", "hello", err) + } + + err = l.ExpectMatch("goodbye") + if err == nil { + t.Errorf("ExpectMatch(%q) should have returned an error, got nil", "goodbye") + } +} + +func TestMockClear(t *testing.T) { + t.Parallel() + + l := NewMock() + l.Info(t.Context(), "before") + got := l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1", len(got)) + } + + l.Clear() + got = l.GetAll() + if len(got) != 0 { + t.Errorf("after Clear, got %d log lines, want 0", len(got)) + } + + l.Info(t.Context(), "after") + got = l.GetAll() + if len(got) != 1 { + t.Fatalf("got %d log lines, want 1: %v", len(got), got) + } + if !strings.Contains(got[0], "after") { + t.Errorf("log line %q does not contain %q", got[0], "after") + } +} diff --git a/blog/prod_vars.go b/blog/prod_vars.go new file mode 100644 index 00000000000..8a85f03f0ae --- /dev/null +++ b/blog/prod_vars.go @@ -0,0 +1,34 @@ +//go:build !integration + +package blog + +// This file is used by production code and unit tests. + +import ( + "io" + "log/slog" + "testing" +) + +// stdlibHandler constructs the underlying handler provided by the go +// standard library appropriate to the current environment. +// +// In unit tests, we use the TextHandler for ease of assertion readability. +// In actual production code, we use the JSONHandler. +func stdlibHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler { + if testing.Testing() { + return slog.NewTextHandler(w, opts) + } else { + return slog.NewJSONHandler(w, opts) + } +} + +// universalAttrs returns the set of slog.Attrs which should be included in all +// log lines. It returns []any instead of []slog.Attr because slog doesn't have +// a Logger.WithAttr() method. +// +// Because our production log collector adds dc/host/prog/pid tags itself, we +// don't add anything here. +func universalAttrs() []any { + return nil +} diff --git a/blog/test_vars.go b/blog/test_vars.go new file mode 100644 index 00000000000..e02a38e61b1 --- /dev/null +++ b/blog/test_vars.go @@ -0,0 +1,31 @@ +//go:build integration + +package blog + +// This file is used by the integration tests. + +import ( + "io" + "log/slog" + + "github.com/letsencrypt/boulder/core" +) + +// stdlibHandler constructs the underlying handler provided by the go +// standard library appropriate to the current environment. +// +// In integration tests, we always use the TextHandler. +func stdlibHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler { + return slog.NewTextHandler(w, opts) +} + +// universalAttrs returns the set of slog.Attrs which should be included in all +// log lines. It returns []any instead of []slog.Attr because slog doesn't have +// a Logger.WithAttr() method. +// +// Because our test environment does not plumb the log output through a system +// which tags lines with their source, do that ourselves for the sake +// of test output readability. +func universalAttrs() []any { + return []any{slog.String("prog", core.Command())} +} diff --git a/blog/validator/tail_logger.go b/blog/validator/tail_logger.go new file mode 100644 index 00000000000..6d1554b5903 --- /dev/null +++ b/blog/validator/tail_logger.go @@ -0,0 +1,54 @@ +package validator + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/letsencrypt/boulder/blog" +) + +// tailLogger is an adapter to the nxadm/tail module's logging interface. It is +// defined here instead of in //blog/adapters.go because it is only used locally, +// not set as a package-level default. +type tailLogger struct { + blog.Logger +} + +func (tl tailLogger) Fatal(v ...any) { + tl.Logger.Error(context.Background(), "tail fatal", errors.New(fmt.Sprint(v...))) + os.Exit(1) +} +func (tl tailLogger) Fatalf(format string, v ...any) { + tl.Logger.Error(context.Background(), "tail fatal", fmt.Errorf(format, v...)) + os.Exit(1) +} +func (tl tailLogger) Fatalln(v ...any) { + tl.Logger.Error(context.Background(), "tail fatal", errors.New(fmt.Sprint(v...))) + os.Exit(1) +} +func (tl tailLogger) Panic(v ...any) { + msg := fmt.Sprint(v...) + tl.Logger.Error(context.Background(), "tail panic", errors.New(msg)) + panic(msg) +} +func (tl tailLogger) Panicf(format string, v ...any) { + err := fmt.Errorf(format, v...) + tl.Logger.Error(context.Background(), "tail panic", err) + panic(err) +} +func (tl tailLogger) Panicln(v ...any) { + msg := fmt.Sprint(v...) + tl.Logger.Error(context.Background(), "tail panic", errors.New(msg)) + panic(msg) +} +func (tl tailLogger) Print(v ...any) { + tl.Logger.Info(context.Background(), fmt.Sprint(v...)) +} +func (tl tailLogger) Printf(format string, v ...any) { + tl.Logger.Info(context.Background(), fmt.Sprintf(format, v...)) +} +func (tl tailLogger) Println(v ...any) { + tl.Logger.Info(context.Background(), fmt.Sprint(v...)) +} diff --git a/log/validator/validator.go b/blog/validator/validator.go similarity index 87% rename from log/validator/validator.go rename to blog/validator/validator.go index 5f309e5ae16..3e560c2e77f 100644 --- a/log/validator/validator.go +++ b/blog/validator/validator.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -15,7 +16,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" ) var errInvalidChecksum = errors.New("invalid checksum length") @@ -34,11 +35,11 @@ type Validator struct { monitorCancel context.CancelFunc lineCounter *prometheus.CounterVec - log log.Logger + log blog.Logger } // New Validator monitoring paths, which is a list of file globs. -func New(patterns []string, logger log.Logger, stats prometheus.Registerer) *Validator { +func New(patterns []string, logger blog.Logger, stats prometheus.Registerer) *Validator { lineCounter := promauto.With(stats).NewCounterVec(prometheus.CounterOpts{ Name: "log_lines", Help: "A counter of log lines processed, with status", @@ -60,13 +61,13 @@ func New(patterns []string, logger log.Logger, stats prometheus.Registerer) *Val } // pollPaths expands v.patterns and calls v.tailValidateFile on each resulting file -func (v *Validator) pollPaths() { +func (v *Validator) pollPaths(ctx context.Context) { v.mu.Lock() defer v.mu.Unlock() for _, pattern := range v.patterns { paths, err := filepath.Glob(pattern) if err != nil { - v.log.Errf("expanding file glob: %s", err) + v.log.Error(ctx, "expanding file glob", err) } for _, path := range paths { @@ -84,10 +85,10 @@ func (v *Validator) pollPaths() { }) if err != nil { // TailFile shouldn't error when MustExist is false - v.log.Errf("unexpected error from TailFile: %v", err) + v.log.Error(ctx, "unexpected error from TailFile", err, slog.String("file", path)) } - go v.tailValidate(path, t.Lines) + go v.tailValidate(ctx, path, t.Lines) v.tailers[path] = t } @@ -97,7 +98,7 @@ func (v *Validator) pollPaths() { // Monitor calls v.pollPaths every minute until its context is cancelled func (v *Validator) monitor(ctx context.Context) { for { - v.pollPaths() + v.pollPaths(ctx) // Wait a minute, unless cancelled timer := time.NewTimer(time.Minute) @@ -109,7 +110,7 @@ func (v *Validator) monitor(ctx context.Context) { } } -func (v *Validator) tailValidate(filename string, lines chan *tail.Line) { +func (v *Validator) tailValidate(ctx context.Context, filename string, lines chan *tail.Line) { // Emit no more than 1 error line per second. This prevents consuming large // amounts of disk space in case there is problem that causes all log lines to // be invalid. @@ -118,7 +119,7 @@ func (v *Validator) tailValidate(filename string, lines chan *tail.Line) { for line := range lines { if line.Err != nil { - v.log.Errf("error while tailing %s: %s", filename, line.Err) + v.log.Error(ctx, "error while tailing", line.Err, slog.String("file", filename)) continue } err := lineValid(line.Text) @@ -130,7 +131,11 @@ func (v *Validator) tailValidate(filename string, lines chan *tail.Line) { } select { case <-outputLimiter.C: - v.log.Errf("%s: %s %q", filename, err, line.Text) + v.log.Error(ctx, "invalid log line", err, + slog.String("file", filename), + slog.Int("line", line.Num), + slog.String("text", line.Text), + ) default: } } else { @@ -204,7 +209,7 @@ func lineValid(text string) error { return nil } // Check the extracted checksum against the computed checksum - computedChecksum := log.LogLineChecksum(line) + computedChecksum := blog.LogLineChecksum(line) if checksum != computedChecksum { return fmt.Errorf("%s invalid checksum (expected %q, got %q)", errorPrefix, computedChecksum, checksum) } diff --git a/log/validator/validator_test.go b/blog/validator/validator_test.go similarity index 100% rename from log/validator/validator_test.go rename to blog/validator/validator_test.go diff --git a/ca/ca.go b/ca/ca.go index 5ce5c07d01f..f01568a7bf0 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -12,6 +12,7 @@ import ( "encoding/hex" "errors" "fmt" + "log/slog" "math/big" mrand "math/rand/v2" "slices" @@ -29,6 +30,7 @@ import ( cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" csrlib "github.com/letsencrypt/boulder/csr" @@ -37,7 +39,6 @@ import ( "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" "github.com/letsencrypt/boulder/linter" - blog "github.com/letsencrypt/boulder/log" rapb "github.com/letsencrypt/boulder/ra/proto" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -49,28 +50,6 @@ const ( certType = certificateType("certificate") ) -// issuanceEvent is logged before and after issuance of precertificates and certificates. -// The `omitempty` fields are not always present. -// CSR, Precertificate, and Certificate are hex-encoded DER bytes to make it easier to -// ad-hoc search for sequences or OIDs in logs. Other data, like public key within CSR, -// is logged as base64 because it doesn't have interesting DER structure. -type issuanceEvent struct { - Requester int64 - OrderID int64 - Profile string - Issuer string - IssuanceRequest *issuance.IssuanceRequest - CSR string `json:",omitempty"` - Result issuanceEventResult -} - -// issuanceEventResult exists just to lend some extra structure to the -// issuanceEvent struct above. -type issuanceEventResult struct { - Precertificate string `json:",omitempty"` - Certificate string `json:",omitempty"` -} - // caMetrics holds various metrics which are shared between caImpl and crlImpl. type caMetrics struct { signatureCount *prometheus.CounterVec @@ -220,6 +199,12 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c return nil, berrors.InternalServerError("Incomplete issue certificate request") } + ctx = blog.ContextWith(ctx, + blog.Acct(req.RegistrationID), + blog.Order(req.OrderID), + slog.String("profile", req.CertProfileName), + ) + if ca.sctClient == nil { return nil, errors.New("IssueCertificate called with a nil SCT service") } @@ -269,6 +254,11 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c serialBigInt := ca.generateSerialNumber() serialHex := core.SerialToString(serialBigInt) + ctx = blog.ContextWith(ctx, + slog.String("issuer", issuer.Name()), + blog.Serial(serialHex), + ) + // Step 2: Persist the serial and minimal metadata, to ensure that we never // duplicate a serial. _, err = ca.sa.AddSerial(ctx, &sapb.AddSerialRequest{ @@ -306,7 +296,7 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c lintPrecertDER, issuanceToken, err := issuer.Prepare(profile, precertReq) if err != nil { - ca.log.AuditErr("Preparing precert failed", err, map[string]any{"serial": serialHex}) + ca.log.AuditError(ctx, "Preparing precert failed", err) if errors.Is(err, linter.ErrLinting) { ca.metrics.lintErrorCount.Inc() } @@ -328,31 +318,23 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c return nil, fmt.Errorf("persisting linting precert to database: %w", err) } - ca.log.AuditInfo("Signing precert", issuanceEvent{ - Requester: req.RegistrationID, - OrderID: req.OrderID, - Profile: req.CertProfileName, - Issuer: issuer.Name(), - IssuanceRequest: precertReq, - CSR: hex.EncodeToString(csr.Raw), - }) + ca.log.AuditInfo(ctx, "Signing precert", + slog.Any("issuanceRequest", precertReq), + slog.String("csr", hex.EncodeToString(csr.Raw)), + ) precertDER, err := issuer.Issue(issuanceToken) if err != nil { ca.metrics.noteSignError(err) - ca.log.AuditErr("Signing precert failed", err, map[string]any{"serial": serialHex}) + ca.log.AuditError(ctx, "Signing precert failed", err) return nil, fmt.Errorf("failed to sign precertificate: %w", err) } ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc() - ca.log.AuditInfo("Signing precert success", issuanceEvent{ - Requester: req.RegistrationID, - OrderID: req.OrderID, - Profile: req.CertProfileName, - Issuer: issuer.Name(), - IssuanceRequest: precertReq, - Result: issuanceEventResult{Precertificate: hex.EncodeToString(precertDER)}, - }) + ca.log.AuditInfo(ctx, "Signing precert success", + slog.Any("issuanceRequest", precertReq), + slog.Group("result", slog.String("precertificate", hex.EncodeToString(precertDER))), + ) err = tbsCertIsDeterministic(lintPrecertDER, precertDER) if err != nil { @@ -402,35 +384,27 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c lintCertDER, issuanceToken, err := issuer.Prepare(profile, certReq) if err != nil { - ca.log.AuditErr("Preparing cert failed", err, map[string]any{"serial": serialHex}) + ca.log.AuditError(ctx, "Preparing cert failed", err) return nil, fmt.Errorf("failed to prepare certificate signing: %w", err) } - ca.log.AuditInfo("Signing cert", issuanceEvent{ - Requester: req.RegistrationID, - OrderID: req.OrderID, - Profile: req.CertProfileName, - Issuer: issuer.Name(), - IssuanceRequest: certReq, - }) + ca.log.AuditInfo(ctx, "Signing cert", + slog.Any("issuanceRequest", certReq), + ) certDER, err := issuer.Issue(issuanceToken) if err != nil { ca.metrics.noteSignError(err) - ca.log.AuditErr("Signing cert failed", err, map[string]any{"serial": serialHex}) + ca.log.AuditError(ctx, "Signing cert failed", err) return nil, fmt.Errorf("failed to sign certificate: %w", err) } ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc() ca.metrics.certificates.With(prometheus.Labels{"profile": req.CertProfileName}).Inc() - ca.log.AuditInfo("Signing cert success", issuanceEvent{ - Requester: req.RegistrationID, - OrderID: req.OrderID, - Profile: req.CertProfileName, - Issuer: issuer.Name(), - IssuanceRequest: certReq, - Result: issuanceEventResult{Certificate: hex.EncodeToString(certDER)}, - }) + ca.log.AuditInfo(ctx, "Signing cert success", + slog.Any("issuanceRequest", certReq), + slog.Group("result", slog.String("certificate", hex.EncodeToString(certDER))), + ) err = tbsCertIsDeterministic(lintCertDER, certDER) if err != nil { @@ -443,7 +417,7 @@ func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, req *c Issued: timestamppb.New(ca.clk.Now()), }) if err != nil { - ca.log.AuditErr("Storing cert failed", err, map[string]any{"serial": serialHex}) + ca.log.AuditError(ctx, "Storing cert failed", err) return nil, fmt.Errorf("persisting cert to database: %w", err) } diff --git a/ca/ca_test.go b/ca/ca_test.go index 4f853c51676..0c5b94ca7dc 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -26,6 +26,7 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" @@ -35,7 +36,6 @@ import ( "github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/must" "github.com/letsencrypt/boulder/policy" rapb "github.com/letsencrypt/boulder/ra/proto" @@ -125,7 +125,7 @@ func newCAArgs(t *testing.T) *caArgs { features.Reset() fc := clock.NewFake() - fc.Add(1 * time.Hour) + fc.Set(time.Date(2020, 01, 01, 12, 00, 00, 0, time.UTC)) pa, err := policy.New(map[identifier.IdentifierType]bool{"dns": true}, nil, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") @@ -135,7 +135,11 @@ func newCAArgs(t *testing.T) *caArgs { legacy, err := issuance.NewProfile(issuance.ProfileConfig{ MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90}, MaxValidityBackdate: config.Duration{Duration: time.Hour}, - IgnoredLints: []string{"w_subject_common_name_included"}, + IgnoredLints: []string{ + "e_sub_cert_aia_does_not_contain_ocsp_url", + "w_ct_sct_policy_count_unsatisfied", + "n_subject_common_name_included", + }, }) test.AssertNotError(t, err, "Loading test profile") modern, err := issuance.NewProfile(issuance.ProfileConfig{ @@ -145,7 +149,11 @@ func newCAArgs(t *testing.T) *caArgs { OmitSKID: true, MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 6}, MaxValidityBackdate: config.Duration{Duration: time.Hour}, - IgnoredLints: []string{"w_ext_subject_key_identifier_missing_sub_cert"}, + IgnoredLints: []string{ + "e_sub_cert_aia_does_not_contain_ocsp_url", + "w_ct_sct_policy_count_unsatisfied", + "w_ext_subject_key_identifier_missing_sub_cert", + }, }) test.AssertNotError(t, err, "Loading test profile") profiles := map[string]*issuance.Profile{ diff --git a/ca/crl.go b/ca/crl.go index 22cc5deb1d4..62af4c4d2d0 100644 --- a/ca/crl.go +++ b/ca/crl.go @@ -6,19 +6,19 @@ import ( "errors" "fmt" "io" + "log/slog" "strings" - "time" "google.golang.org/grpc" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" bcrl "github.com/letsencrypt/boulder/crl" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" ) type crlImpl struct { @@ -115,17 +115,16 @@ func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLR return errors.New("no crl metadata received") } - // Compute a unique ID for this issuer-number-shard combo, to tie together all - // the audit log lines related to its issuance. - logID := blog.LogLineChecksum(fmt.Sprintf("%d", issuer.NameID()) + req.Number.String() + fmt.Sprintf("%d", req.Shard)) - ci.log.AuditInfo("Signing CRL", map[string]any{ - "logID": logID, - "issuer": issuer.Cert.Subject.CommonName, - "number": req.Number.String(), - "shard": req.Shard, - "thisUpdate": req.ThisUpdate.Format(time.RFC3339), - "numEntries": len(rcs), - }) + ctx := blog.ContextWith(stream.Context(), + slog.String("issuer", issuer.Name()), + slog.Int64("shard", req.Shard), + slog.String("number", req.Number.String()), + ) + + ci.log.AuditInfo(ctx, "Signing CRL", + slog.Time("thisUpdate", req.ThisUpdate), + slog.Int("numEntries", len(rcs)), + ) if len(rcs) > 0 { builder := strings.Builder{} @@ -133,17 +132,15 @@ func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLR fmt.Fprintf(&builder, "\"%x:%d\",", rcs[i].SerialNumber.Bytes(), rcs[i].ReasonCode) if builder.Len() >= ci.maxLogLen { - ci.log.AuditInfo("Signing CRL entries", map[string]string{ - "logID": logID, - "entries": fmt.Sprintf("[%s]", strings.TrimSuffix(builder.String(), ",")), - }) + ci.log.AuditInfo(ctx, "Signing CRL entries", + slog.String("entries", fmt.Sprintf("[%s]", strings.TrimSuffix(builder.String(), ","))), + ) builder = strings.Builder{} } } - ci.log.AuditInfo("Signing CRL entries", map[string]any{ - "logID": logID, - "entries": fmt.Sprintf("[%s]", strings.TrimSuffix(builder.String(), ",")), - }) + ci.log.AuditInfo(ctx, "Signing CRL entries", + slog.String("entries", fmt.Sprintf("[%s]", strings.TrimSuffix(builder.String(), ","))), + ) } req.Entries = rcs @@ -156,11 +153,10 @@ func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLR ci.metrics.signatureCount.With(prometheus.Labels{"purpose": "crl", "issuer": issuer.Name()}).Inc() hash := sha256.Sum256(crlBytes) - ci.log.AuditInfo("Signing CRL success", map[string]any{ - "logID": logID, - "size": len(crlBytes), - "hash": fmt.Sprintf("%x", hash), - }) + ci.log.AuditInfo(ctx, "Signing CRL success", + slog.Int("size", len(crlBytes)), + slog.String("sha256", fmt.Sprintf("%x", hash)), + ) for i := 0; i < len(crlBytes); i += 1000 { j := min(i+1000, len(crlBytes)) @@ -171,7 +167,7 @@ func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLR return err } if i%1000 == 0 { - ci.log.Debugf("Wrote %d bytes to output stream", i*1000) + ci.log.Debug(ctx, "Output byte stream checkpoint", slog.Int("bytes", i*1000)) } } diff --git a/ca/crl_test.go b/ca/crl_test.go index b6c78ebfe29..92185149d7a 100644 --- a/ca/crl_test.go +++ b/ca/crl_test.go @@ -1,6 +1,7 @@ package ca import ( + "context" "crypto/x509" "fmt" "io" @@ -19,6 +20,7 @@ import ( type mockGenerateCRLBidiStream struct { grpc.ServerStream + ctx context.Context input <-chan *capb.GenerateCRLRequest output chan<- *capb.GenerateCRLResponse } @@ -36,6 +38,10 @@ func (s mockGenerateCRLBidiStream) Send(entry *capb.GenerateCRLResponse) error { return nil } +func (s mockGenerateCRLBidiStream) Context() context.Context { + return s.ctx +} + func TestGenerateCRL(t *testing.T) { t.Parallel() cargs := newCAArgs(t) @@ -55,7 +61,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when no metadata is sent. ins := make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() close(ins) err = <-errs @@ -65,7 +71,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when incomplete metadata is sent. ins = make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() ins <- &capb.GenerateCRLRequest{ Payload: &capb.GenerateCRLRequest_Metadata{ @@ -80,7 +86,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when unrecognized metadata is sent. ins = make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() now := cargs.clk.Now() ins <- &capb.GenerateCRLRequest{ @@ -100,7 +106,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when two metadata are sent. ins = make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() ins <- &capb.GenerateCRLRequest{ Payload: &capb.GenerateCRLRequest_Metadata{ @@ -129,7 +135,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when an entry has a bad serial. ins = make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() ins <- &capb.GenerateCRLRequest{ Payload: &capb.GenerateCRLRequest_Entry{ @@ -148,7 +154,7 @@ func TestGenerateCRL(t *testing.T) { // Test that we get an error when an entry has a bad revocation time. ins = make(chan *capb.GenerateCRLRequest) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: nil}) }() ins <- &capb.GenerateCRLRequest{ @@ -169,7 +175,7 @@ func TestGenerateCRL(t *testing.T) { ins = make(chan *capb.GenerateCRLRequest) outs := make(chan *capb.GenerateCRLResponse) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: outs}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: outs}) close(outs) }() crlBytes := make([]byte, 0) @@ -206,7 +212,7 @@ func TestGenerateCRL(t *testing.T) { ins = make(chan *capb.GenerateCRLRequest) outs = make(chan *capb.GenerateCRLResponse) go func() { - errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: outs}) + errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{ctx: t.Context(), input: ins, output: outs}) close(outs) }() crlBytes = make([]byte, 0) diff --git a/cmd/admin/admin.go b/cmd/admin/admin.go index 4ced496191d..44c061462aa 100644 --- a/cmd/admin/admin.go +++ b/cmd/admin/admin.go @@ -9,10 +9,10 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" - blog "github.com/letsencrypt/boulder/log" rapb "github.com/letsencrypt/boulder/ra/proto" sapb "github.com/letsencrypt/boulder/sa/proto" ) diff --git a/cmd/admin/cert.go b/cmd/admin/cert.go index 607a5c5e438..eab0237a48f 100644 --- a/cmd/admin/cert.go +++ b/cmd/admin/cert.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io" + "log/slog" "os" "os/user" "strings" @@ -14,6 +15,7 @@ import ( "sync/atomic" "unicode" + "github.com/letsencrypt/boulder/blog" core "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" rapb "github.com/letsencrypt/boulder/ra/proto" @@ -137,7 +139,7 @@ func (s *subcommandRevokeCert) Run(ctx context.Context, a *admin) error { return errors.New("no serials to revoke found") } - a.log.Infof("Found %d certificates to revoke", len(serials)) + a.log.Info(ctx, "Found certificates to revoke", slog.Int("count", len(serials))) if s.malformed { return s.revokeMalformed(ctx, a, serials, reasonCode) @@ -202,6 +204,7 @@ func (a *admin) serialsFromFile(_ context.Context, filePath string) ([]string, e if err != nil { return nil, fmt.Errorf("opening serials file: %w", err) } + defer file.Close() var serials []string scanner := bufio.NewScanner(file) @@ -212,6 +215,10 @@ func (a *admin) serialsFromFile(_ context.Context, filePath string) ([]string, e } serials = append(serials, serial) } + err = scanner.Err() + if err != nil { + return nil, fmt.Errorf("error while reading serials file: %w", err) + } return serials, nil } @@ -332,9 +339,9 @@ func (a *admin) revokeSerials(ctx context.Context, serials []string, reason revo if err != nil { errCount.Add(1) if errors.Is(err, berrors.AlreadyRevoked) { - a.log.Warningf("not revoking %q: already revoked", serial) + a.log.Warn(ctx, "cert already revoked", blog.Serial(serial)) } else { - a.log.Errf("failed to revoke %q: %s", serial, err) + a.log.Error(ctx, "failed to revoke", err, blog.Serial(serial)) } } } diff --git a/cmd/admin/cert_test.go b/cmd/admin/cert_test.go index 7a42898703d..deeb80cd8c9 100644 --- a/cmd/admin/cert_test.go +++ b/cmd/admin/cert_test.go @@ -21,10 +21,10 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/mocks" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/revocation" @@ -234,7 +234,7 @@ func TestRevokeSerials(t *testing.T) { t.Logf("error: %s", err) t.Logf("logs: %s", strings.Join(log.GetAll(), "")) test.AssertError(t, err, "already-revoked should result in error") - test.AssertEquals(t, len(log.GetAllMatching("not revoking")), 1) + test.AssertEquals(t, len(log.GetAllMatching("cert already revoked")), 1) test.AssertEquals(t, len(mra.revocationRequests), 3) assertRequestsContain(mra.revocationRequests, 0, false) diff --git a/cmd/admin/dryrun.go b/cmd/admin/dryrun.go index 9aec729e045..29a406bd466 100644 --- a/cmd/admin/dryrun.go +++ b/cmd/admin/dryrun.go @@ -2,12 +2,14 @@ package main import ( "context" + "encoding/hex" + "log/slog" "google.golang.org/grpc" - "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/types/known/emptypb" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/identifier" rapb "github.com/letsencrypt/boulder/ra/proto" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -18,12 +20,15 @@ type dryRunRAC struct { var _ adminRAClient = (*dryRunRAC)(nil) -func (d dryRunRAC) AdministrativelyRevokeCertificate(_ context.Context, req *rapb.AdministrativelyRevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - b, err := prototext.Marshal(req) - if err != nil { - return nil, err - } - d.log.Infof("dry-run: %#v", string(b)) +func (d dryRunRAC) AdministrativelyRevokeCertificate(ctx context.Context, req *rapb.AdministrativelyRevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + d.log.Info(ctx, "dry-run: ra.AdministrativelyRevokeCertificate", + blog.Serial(req.Serial), + slog.Int64("code", req.Code), + slog.String("adminName", req.AdminName), + slog.Bool("skipBlockKey", req.SkipBlockKey), + slog.Bool("malformed", req.Malformed), + slog.Int64("crlShard", req.CrlShard), + ) return &emptypb.Empty{}, nil } @@ -33,33 +38,58 @@ type dryRunSAC struct { var _ adminSAClient = (*dryRunSAC)(nil) -func (d dryRunSAC) AddBlockedKey(_ context.Context, req *sapb.AddBlockedKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - d.log.Infof("dry-run: Block SPKI hash %x by %s %s", req.KeyHash, req.Comment, req.Source) +func (d dryRunSAC) AddBlockedKey(ctx context.Context, req *sapb.AddBlockedKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + d.log.Info(ctx, "dry-run: sa.AddBlockedKey", + slog.String("keyHash", hex.EncodeToString(req.KeyHash)), + slog.Time("added", req.Added.AsTime()), + slog.String("source", req.Source), + slog.String("comment", req.Comment), + slog.Int64("revokedBy", req.RevokedBy), + ) return &emptypb.Empty{}, nil } -func (d dryRunSAC) AddRateLimitOverride(_ context.Context, req *sapb.AddRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.AddRateLimitOverrideResponse, error) { - d.log.Infof("dry-run: Add override for %q (%s)", req.Override.BucketKey, req.Override.Comment) +func (d dryRunSAC) AddRateLimitOverride(ctx context.Context, req *sapb.AddRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.AddRateLimitOverrideResponse, error) { + d.log.Info(ctx, "dry-run: sa.AddRateLimitOverride", + slog.Int64("limit", req.Override.LimitEnum), + slog.String("bucketKey", req.Override.BucketKey), + slog.String("comment", req.Override.Comment), + slog.Duration("period", req.Override.Period.AsDuration()), + slog.Int64("count", req.Override.Count), + slog.Int64("burst", req.Override.Burst), + slog.Bool("force", req.Force), + ) return &sapb.AddRateLimitOverrideResponse{Inserted: true, Enabled: true}, nil } -func (d dryRunSAC) DisableRateLimitOverride(_ context.Context, req *sapb.DisableRateLimitOverrideRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - d.log.Infof("dry-run: Disable override for %q", req.BucketKey) +func (d dryRunSAC) DisableRateLimitOverride(ctx context.Context, req *sapb.DisableRateLimitOverrideRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + d.log.Info(ctx, "dry-run: sa.DisableRateLimitOverride", + slog.Int64("limit", req.LimitEnum), + slog.String("bucketKey", req.BucketKey), + ) return &emptypb.Empty{}, nil } -func (d dryRunSAC) EnableRateLimitOverride(_ context.Context, req *sapb.EnableRateLimitOverrideRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - d.log.Infof("dry-run: Enable override for %q", req.BucketKey) +func (d dryRunSAC) EnableRateLimitOverride(ctx context.Context, req *sapb.EnableRateLimitOverrideRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + d.log.Info(ctx, "dry-run: sa.EnableRateLimitOverride", + slog.Int64("limit", req.LimitEnum), + slog.String("bucketKey", req.BucketKey), + ) return &emptypb.Empty{}, nil } -func (d dryRunSAC) PauseIdentifiers(_ context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { - d.log.Infof("dry-run: Pause identifiers %#v for account %d", req.Identifiers, req.RegistrationID) +func (d dryRunSAC) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { + d.log.Info(ctx, "dry-run: sa.PauseIdentifiers", + blog.Acct(req.RegistrationID), + blog.Idents(identifier.FromProtoSlice(req.Identifiers)...), + ) return &sapb.PauseIdentifiersResponse{Paused: int64(len(req.Identifiers))}, nil } -func (d dryRunSAC) UnpauseAccount(_ context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) { - d.log.Infof("dry-run: Unpause account %d", req.Id) +func (d dryRunSAC) UnpauseAccount(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) { + d.log.Info(ctx, "dry-run: sa.UnpauseAccount", + blog.Acct(req.Id), + ) return &sapb.Count{Count: 1}, nil } @@ -69,13 +99,23 @@ type dryRunSAAdmin struct { var _ saAdminClient = (*dryRunSAAdmin)(nil) -func (d dryRunSAAdmin) CreateIncident(_ context.Context, req *sapb.CreateIncidentRequest, _ ...grpc.CallOption) (*sapb.Incident, error) { - d.log.Infof("dry-run: Create incident %q (url=%q, renewBy=%s)", req.SerialTable, req.Url, req.RenewBy.AsTime()) +func (d dryRunSAAdmin) CreateIncident(ctx context.Context, req *sapb.CreateIncidentRequest, _ ...grpc.CallOption) (*sapb.Incident, error) { + d.log.Info(ctx, "dry-run: saa.CreateIncident", + slog.String("incident", req.SerialTable), + slog.String("url", req.Url), + slog.Time("renewBy", req.RenewBy.AsTime()), + ) return &sapb.Incident{SerialTable: req.SerialTable, Url: req.Url, RenewBy: req.RenewBy, Enabled: false}, nil } -func (d dryRunSAAdmin) UpdateIncident(_ context.Context, req *sapb.UpdateIncidentRequest, _ ...grpc.CallOption) (*sapb.Incident, error) { - d.log.Infof("dry-run: Update incident %q url=%q renewBy=%v enabled=%v", req.SerialTable, req.Url, req.RenewBy, req.GetEnabled()) +func (d dryRunSAAdmin) UpdateIncident(ctx context.Context, req *sapb.UpdateIncidentRequest, _ ...grpc.CallOption) (*sapb.Incident, error) { + d.log.Info(ctx, "dry-run: saa.UpdateIncident", + slog.String("incident", req.SerialTable), + slog.String("url", req.Url), + slog.Time("renewBy", req.RenewBy.AsTime()), + slog.Bool("enabled", req.GetEnabled()), + ) + out := &sapb.Incident{SerialTable: req.SerialTable, Url: req.Url, RenewBy: req.RenewBy} if req.Enabled != nil { out.Enabled = *req.Enabled @@ -83,12 +123,13 @@ func (d dryRunSAAdmin) UpdateIncident(_ context.Context, req *sapb.UpdateInciden return out, nil } -func (d dryRunSAAdmin) AddSerialsToIncident(_ context.Context, _ ...grpc.CallOption) (grpc.ClientStreamingClient[sapb.AddSerialsToIncidentRequest, emptypb.Empty], error) { - return &dryRunAddSerialsStream{log: d.log}, nil +func (d dryRunSAAdmin) AddSerialsToIncident(ctx context.Context, _ ...grpc.CallOption) (grpc.ClientStreamingClient[sapb.AddSerialsToIncidentRequest, emptypb.Empty], error) { + return &dryRunAddSerialsStream{ctx: ctx, log: d.log}, nil } type dryRunAddSerialsStream struct { grpc.ClientStream + ctx context.Context log blog.Logger incident string count int @@ -105,6 +146,9 @@ func (d *dryRunAddSerialsStream) Send(req *sapb.AddSerialsToIncidentRequest) err } func (d *dryRunAddSerialsStream) CloseAndRecv() (*emptypb.Empty, error) { - d.log.Infof("dry-run: Add %d serials to incident %q", d.count, d.incident) + d.log.Info(d.ctx, "dry-run: saa.AddSerialsToIncident", + slog.String("incident", d.incident), + slog.Int("count", d.count), + ) return &emptypb.Empty{}, nil } diff --git a/cmd/admin/incident.go b/cmd/admin/incident.go index 38be8005db8..03ac2950e19 100644 --- a/cmd/admin/incident.go +++ b/cmd/admin/incident.go @@ -6,6 +6,7 @@ import ( "errors" "flag" "fmt" + "log/slog" "os" "strconv" "strings" @@ -47,7 +48,7 @@ func (s *subcommandCreateIncident) Run(ctx context.Context, a *admin) error { return fmt.Errorf("parsing -renew-by as RFC3339: %w", err) } - inc, err := a.saac.CreateIncident(ctx, &sapb.CreateIncidentRequest{ + _, err = a.saac.CreateIncident(ctx, &sapb.CreateIncidentRequest{ SerialTable: s.incident, Url: s.url, RenewBy: timestamppb.New(renewBy), @@ -55,8 +56,6 @@ func (s *subcommandCreateIncident) Run(ctx context.Context, a *admin) error { if err != nil { return fmt.Errorf("creating incident: %w", err) } - a.log.Infof("Created incident %q url=%q renewBy=%s enabled=%t", - inc.SerialTable, inc.Url, inc.RenewBy.AsTime(), inc.Enabled) return nil } @@ -131,17 +130,6 @@ func (s *subcommandUpdateIncident) Run(ctx context.Context, a *admin) error { if err != nil { return fmt.Errorf("updating incident %q: %w", s.incident, err) } - var changes []string - if req.Url != "" { - changes = append(changes, fmt.Sprintf("url=%q", req.Url)) - } - if req.RenewBy != nil { - changes = append(changes, fmt.Sprintf("renewBy=%s", req.RenewBy.AsTime())) - } - if req.Enabled != nil { - changes = append(changes, fmt.Sprintf("enabled=%t", *req.Enabled)) - } - a.log.AuditInfo(fmt.Sprintf("Updated incident %q: %s", s.incident, strings.Join(changes, " ")), nil) return nil } @@ -182,9 +170,6 @@ func (s *subcommandLoadIncidentSerials) Run(ctx context.Context, a *admin) error } defer file.Close() - a.log.Infof("Loading serials from %q into incident %q with parallelism=%d batch-size=%d.", - s.serialsFile, s.incident, s.parallelism, s.batchSize) - var totalSent atomic.Uint64 work := make(chan []string, s.parallelism) g, gctx := errgroup.WithContext(ctx) @@ -268,7 +253,7 @@ func (s *subcommandLoadIncidentSerials) Run(ctx context.Context, a *admin) error n := totalSent.Add(uint64(len(chunk))) prev := n - uint64(len(chunk)) if prev/100000 != n/100000 { - a.log.Infof("Sent %d serials total", n) + a.log.Info(ctx, "Loading serials in progress", slog.Uint64("count", n)) } } @@ -286,6 +271,5 @@ func (s *subcommandLoadIncidentSerials) Run(ctx context.Context, a *admin) error return fmt.Errorf("loading serials: %w", err) } - a.log.Infof("Done. Sent %d serials from %q into incident %q.", totalSent.Load(), s.serialsFile, s.incident) return nil } diff --git a/cmd/admin/incident_test.go b/cmd/admin/incident_test.go index a62480775c4..36fba9cb8d4 100644 --- a/cmd/admin/incident_test.go +++ b/cmd/admin/incident_test.go @@ -12,7 +12,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" ) diff --git a/cmd/admin/key.go b/cmd/admin/key.go index e15b589bfda..d32a7dc5d4e 100644 --- a/cmd/admin/key.go +++ b/cmd/admin/key.go @@ -10,6 +10,7 @@ import ( "flag" "fmt" "io" + "log/slog" "os" "os/user" "sync" @@ -132,6 +133,7 @@ func (a *admin) spkiHashesFromFile(filePath string) ([][]byte, error) { if err != nil { return nil, fmt.Errorf("opening spki hashes file: %w", err) } + defer file.Close() var spkiHashes [][]byte scanner := bufio.NewScanner(file) @@ -151,6 +153,10 @@ func (a *admin) spkiHashesFromFile(filePath string) ([][]byte, error) { spkiHashes = append(spkiHashes, spkiHash) } + err = scanner.Err() + if err != nil { + return nil, fmt.Errorf("error while reading spki hashes file: %w", err) + } return spkiHashes, nil } @@ -180,8 +186,6 @@ func (a *admin) spkiHashFromCSRPEM(filename string, checkSignature bool, expecte return nil, fmt.Errorf("no PEM data found in %q", filename) } - a.log.Debugf("Parsing key to block from CSR PEM: %x", data) - csr, err := x509.ParseCertificateRequest(data.Bytes) if err != nil { return nil, fmt.Errorf("parsing CSR %q: %w", filename, err) @@ -222,9 +226,9 @@ func (a *admin) blockSPKIHashes(ctx context.Context, spkiHashes [][]byte, commen if err != nil { errCount.Add(1) if errors.Is(err, berrors.AlreadyRevoked) { - a.log.Warningf("not blocking %x: already blocked", spkiHash) + a.log.Warn(ctx, "key already blocked", slog.String("spkiHash", hex.EncodeToString(spkiHash))) } else { - a.log.Errf("failed to block %x: %s", spkiHash, err) + a.log.Error(ctx, "failed to block key", err, slog.String("spkiHash", hex.EncodeToString(spkiHash))) } } } @@ -270,7 +274,7 @@ func (a *admin) blockSPKIHash(ctx context.Context, spkiHash []byte, u *user.User count++ } - a.log.Infof("Found %d unexpired certificates matching the provided key", count) + a.log.Info(ctx, "Found unexpired certificates matching the provided key", slog.Int("count", count)) _, err = a.sac.AddBlockedKey(ctx, &sapb.AddBlockedKeyRequest{ KeyHash: spkiHash[:], diff --git a/cmd/admin/key_test.go b/cmd/admin/key_test.go index 6a41b687c02..0d0732a0192 100644 --- a/cmd/admin/key_test.go +++ b/cmd/admin/key_test.go @@ -23,8 +23,8 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/mocks" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" @@ -179,7 +179,7 @@ func TestBlockSPKIHash(t *testing.T) { log.Clear() err = a.blockSPKIHash(context.Background(), keyHash[:], u, "hello world") test.AssertNotError(t, err, "") - test.AssertEquals(t, len(log.GetAllMatching("Found 0 unexpired certificates")), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Found unexpired certificates matching the provided key.* count=0`)), 1) test.AssertEquals(t, len(msa.blockRequests), 1) test.AssertByteEquals(t, msa.blockRequests[0].KeyHash, keyHash[:]) test.AssertContains(t, msa.blockRequests[0].Comment, "hello world") @@ -190,7 +190,7 @@ func TestBlockSPKIHash(t *testing.T) { a.sac = dryRunSAC{log: log} err = a.blockSPKIHash(context.Background(), keyHash[:], u, "") test.AssertNotError(t, err, "") - test.AssertEquals(t, len(log.GetAllMatching("Found 0 unexpired certificates")), 1) - test.AssertEquals(t, len(log.GetAllMatching("dry-run: Block SPKI hash "+hex.EncodeToString(keyHash[:]))), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Found unexpired certificates matching the provided key.* count=0`)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`dry-run: sa.AddBlockedKey.* keyHash=`+hex.EncodeToString(keyHash[:]))), 1) test.AssertEquals(t, len(msa.blockRequests), 0) } diff --git a/cmd/admin/main.go b/cmd/admin/main.go index cfdc4573b5e..6bb42abffcf 100644 --- a/cmd/admin/main.go +++ b/cmd/admin/main.go @@ -14,9 +14,11 @@ import ( "context" "flag" "fmt" + "log/slog" "os" "strings" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/features" ) @@ -32,7 +34,7 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } @@ -133,20 +135,21 @@ func main() { a, err := newAdmin(*configFile, *dryRun) cmd.FailOnError(err, "creating admin object") + ctx := context.Background() + // Finally, run the selected subcommand. if *dryRun { - a.log.Infof("admin tool executing a dry-run with the following arguments: %q", strings.Join(os.Args, " ")) + a.log.Info(ctx, "admin tool executing a dry-run", slog.String("cmd", strings.Join(os.Args, " "))) } else { - a.log.AuditInfo("admin tool beginning execution", map[string]any{"cmd": strings.Join(os.Args, " ")}) + a.log.AuditInfo(ctx, "admin tool beginning execution", slog.String("cmd", strings.Join(os.Args, " "))) } - err = subcommand.Run(context.Background(), a) + err = subcommand.Run(ctx, a) cmd.FailOnError(err, "executing subcommand") if *dryRun { - a.log.Infof("admin tool has successfully completed executing a dry-run with the following arguments: %q", strings.Join(os.Args, " ")) - a.log.Info("Dry run complete. Pass -dry-run=false to mutate the database.") + a.log.Info(ctx, "admin tool completed a dry-run; pass -dry-run=false to mutate the database", slog.String("cmd", strings.Join(os.Args, " "))) } else { - a.log.AuditInfo("admin tool completed successfully", map[string]any{"cmd": strings.Join(os.Args, " ")}) + a.log.AuditInfo(ctx, "admin tool completed successfully", slog.String("cmd", strings.Join(os.Args, " "))) } } diff --git a/cmd/admin/overrides_add.go b/cmd/admin/overrides_add.go index f6ccec2c33a..f41fdb53cb7 100644 --- a/cmd/admin/overrides_add.go +++ b/cmd/admin/overrides_add.go @@ -5,16 +5,18 @@ import ( "errors" "flag" "fmt" + "log/slog" "net/netip" "strings" "time" + "google.golang.org/protobuf/types/known/durationpb" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/policy" rl "github.com/letsencrypt/boulder/ratelimits" sapb "github.com/letsencrypt/boulder/sa/proto" - "google.golang.org/protobuf/types/known/durationpb" ) type subcommandAddOverride struct { @@ -159,9 +161,15 @@ func (c *subcommandAddOverride) Run(ctx context.Context, a *admin) error { } if resp.Inserted { - a.log.Infof("Added new override for limit %s key %q, status=[%s]\n", name, bucketKey, status) + a.log.Info(ctx, "Added new override", + slog.String("limit", name.String()), + slog.String("bucketKey", bucketKey), + slog.String("status", status)) } else { - a.log.Infof("Updated existing override for limit %s key %q, status=[%s]\n", name, bucketKey, status) + a.log.Info(ctx, "Updated existing override", + slog.String("limit", name.String()), + slog.String("bucketKey", bucketKey), + slog.String("status", status)) } return nil } diff --git a/cmd/admin/overrides_dump.go b/cmd/admin/overrides_dump.go index 41947493fd6..bb98ad5a59d 100644 --- a/cmd/admin/overrides_dump.go +++ b/cmd/admin/overrides_dump.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "io" + "log/slog" "google.golang.org/protobuf/types/known/emptypb" @@ -62,6 +63,9 @@ func (c *subcommandDumpEnabledOverrides) Run(ctx context.Context, a *admin) erro return fmt.Errorf("dumping overrides: %w", err) } - a.log.Infof("Wrote %d overrides to %q\n", len(overrides), c.file) + a.log.Info(ctx, "Wrote overrides to file", + slog.Int("count", len(overrides)), + slog.String("file", c.file), + ) return nil } diff --git a/cmd/admin/overrides_import.go b/cmd/admin/overrides_import.go index ab265eacd4d..1e75b7713d6 100644 --- a/cmd/admin/overrides_import.go +++ b/cmd/admin/overrides_import.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "log/slog" "sync" "google.golang.org/protobuf/types/known/durationpb" @@ -74,18 +75,27 @@ func (c *subcommandImportOverrides) Run(ctx context.Context, a *admin) error { for range overrideCount { result := <-results if result.err != nil { - a.log.Errf("failed to add override: key=%q limit=%d: %s", result.ov.BucketKey, result.ov.LimitEnum, result.err) + a.log.Error(ctx, "failed to add override", result.err, + slog.Int64("limit", result.ov.LimitEnum), + slog.String("bucketKey", result.ov.BucketKey), + ) errorCount++ continue } if result.resp != nil && result.resp.Existing != nil { - a.log.Errf( - "override for limit %s bucketKey %q is lower than existing override (count=%d burst=%d period=%s), use --force to override", - ratelimits.Name(int(result.ov.LimitEnum)), - result.ov.BucketKey, - result.resp.Existing.Count, - result.resp.Existing.Burst, - result.resp.Existing.Period.AsDuration(), + a.log.Error(ctx, "refused to update override", errors.New("new override is lower than existing override"), + slog.Int64("limit", result.ov.LimitEnum), + slog.String("bucketKey", result.ov.BucketKey), + slog.Group("old", + slog.Duration("period", result.resp.Existing.Period.AsDuration()), + slog.Int64("count", result.resp.Existing.Count), + slog.Int64("burst", result.resp.Existing.Burst), + ), + slog.Group("new", + slog.Duration("period", result.ov.Period.AsDuration()), + slog.Int64("count", result.ov.Count), + slog.Int64("burst", result.ov.Burst), + ), ) errorCount++ } @@ -97,6 +107,6 @@ func (c *subcommandImportOverrides) Run(ctx context.Context, a *admin) error { if errorCount > 0 { return fmt.Errorf("%d out of %d overrides failed to be added, see log message(s) for more details", errorCount, overrideCount) } - a.log.Infof("Successfully added %d overrides", overrideCount) + a.log.Info(ctx, "Successfully added overrides", slog.Int("count", overrideCount)) return nil } diff --git a/cmd/admin/overrides_toggle.go b/cmd/admin/overrides_toggle.go index 99e713b2907..17ef7e6f5ed 100644 --- a/cmd/admin/overrides_toggle.go +++ b/cmd/admin/overrides_toggle.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "log/slog" "net/netip" "strings" @@ -86,7 +87,10 @@ func (c *subcommandToggleOverride) Run(ctx context.Context, a *admin) error { if err != nil { return fmt.Errorf("enabling override for limit %s key %q: %s", name, bucketKey, err) } - a.log.Infof("Enabled override for limit %s key %q\n", name, bucketKey) + a.log.Info(ctx, "Enabled override", + slog.String("limit", name.String()), + slog.String("bucketKey", bucketKey), + ) return nil } @@ -97,6 +101,9 @@ func (c *subcommandToggleOverride) Run(ctx context.Context, a *admin) error { if err != nil { return fmt.Errorf("disabling override for limit %s key %q: %s", name, bucketKey, err) } - a.log.Infof("Disabled override for limit %s key %q\n", name, bucketKey) + a.log.Info(ctx, "Disabled override", + slog.String("limit", name.String()), + slog.String("bucketKey", bucketKey), + ) return nil } diff --git a/cmd/admin/pause_identifier.go b/cmd/admin/pause_identifier.go index da268d7245b..95483c2d0d5 100644 --- a/cmd/admin/pause_identifier.go +++ b/cmd/admin/pause_identifier.go @@ -7,11 +7,13 @@ import ( "flag" "fmt" "io" + "log/slog" "os" "strconv" "sync" "sync/atomic" + "github.com/letsencrypt/boulder/blog" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/identifier" sapb "github.com/letsencrypt/boulder/sa/proto" @@ -39,7 +41,7 @@ func (p *subcommandPauseIdentifier) Run(ctx context.Context, a *admin) error { return errors.New("the -batch-file flag is required") } - idents, err := a.readPausedAccountFile(p.batchFile) + idents, err := a.readPausedAccountFile(ctx, p.batchFile) if err != nil { return err } @@ -85,7 +87,10 @@ func (a *admin) pauseIdentifiers(ctx context.Context, entries []pauseCSVData, pa }) if err != nil { errCount.Add(1) - a.log.Errf("error pausing identifier(s) %q for account %d: %v", data.idents, data.accountID, err) + a.log.Error(ctx, "failed to pause identifiers", err, + blog.Acct(data.accountID), + blog.Idents(identifier.FromProtoSlice(data.idents)...), + ) } else { respChan <- response } @@ -127,7 +132,7 @@ type pauseCSVData struct { // `pauseCSVData` objects and returns it or an error. It will skip malformed // lines and continue processing until either the end of file marker is detected // or other read error. -func (a *admin) readPausedAccountFile(filePath string) ([]pauseCSVData, error) { +func (a *admin) readPausedAccountFile(ctx context.Context, filePath string) ([]pauseCSVData, error) { fp, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("opening paused account data file: %w", err) @@ -157,25 +162,31 @@ func (a *admin) readPausedAccountFile(filePath string) ([]pauseCSVData, error) { // We should have strictly 3 fields, note that just commas is considered // a valid CSV line. if len(record) != 3 { - a.log.Infof("skipping: malformed line %d, should contain exactly 3 fields\n", lineCounter) + a.log.Info(ctx, "skipping malformed line: should contain exactly 3 fields") continue } recordID := record[0] accountID, err := strconv.ParseInt(recordID, 10, 64) if err != nil || accountID == 0 { - a.log.Infof("skipping: malformed accountID entry on line %d\n", lineCounter) + a.log.Info(ctx, "skipping malformed accountID entry") continue } // Ensure that an identifier type is present, otherwise skip the line. if len(record[1]) == 0 { - a.log.Infof("skipping: malformed identifierType entry on line %d\n", lineCounter) + a.log.Info(ctx, "skipping malformed identifierType entry", + slog.String("file", filePath), + slog.Int("line", lineCounter), + ) continue } if len(record[2]) == 0 { - a.log.Infof("skipping: malformed identifierValue entry on line %d\n", lineCounter) + a.log.Info(ctx, "skipping malformed identifierValue entry", + slog.String("file", filePath), + slog.Int("line", lineCounter), + ) continue } @@ -186,7 +197,10 @@ func (a *admin) readPausedAccountFile(filePath string) ([]pauseCSVData, error) { } parsedRecords = append(parsedRecords, parsedRecord) } - a.log.Infof("detected %d valid record(s) from input file\n", len(parsedRecords)) + a.log.Debug(ctx, "Loaded input from file", + slog.String("file", filePath), + slog.Int("count", len(parsedRecords)), + ) return parsedRecords, nil } diff --git a/cmd/admin/pause_identifier_test.go b/cmd/admin/pause_identifier_test.go index 937cf179107..8057896ec8d 100644 --- a/cmd/admin/pause_identifier_test.go +++ b/cmd/admin/pause_identifier_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" "google.golang.org/grpc" @@ -72,7 +72,7 @@ func TestReadingPauseCSV(t *testing.T) { err := os.WriteFile(csvFile, []byte(strings.Join(testCase.data, "\n")), os.ModePerm) test.AssertNotError(t, err, "could not write temporary file") - parsedData, err := a.readPausedAccountFile(csvFile) + parsedData, err := a.readPausedAccountFile(t.Context(), csvFile) test.AssertNotError(t, err, "no error expected, but received one") test.AssertEquals(t, len(parsedData), testCase.expectedRecords) }) diff --git a/cmd/admin/unpause_account.go b/cmd/admin/unpause_account.go index 7f38c20b02e..69aa6eb3109 100644 --- a/cmd/admin/unpause_account.go +++ b/cmd/admin/unpause_account.go @@ -6,12 +6,14 @@ import ( "errors" "flag" "fmt" + "log/slog" "os" "slices" "strconv" "sync" "sync/atomic" + "github.com/letsencrypt/boulder/blog" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/unpause" ) @@ -53,7 +55,7 @@ func (u *subcommandUnpauseAccount) Run(ctx context.Context, a *admin) error { case "-account": regIDs = []int64{u.accountID} case "-batch-file": - regIDs, err = a.readUnpauseAccountFile(u.batchFile) + regIDs, err = a.readUnpauseAccountFile(ctx, u.batchFile) default: return errors.New("no recognized input method flag set (this shouldn't happen)") } @@ -97,7 +99,7 @@ func (a *admin) unpauseAccounts(ctx context.Context, accountIDs []int64, paralle response, err := a.sac.UnpauseAccount(ctx, &sapb.RegistrationID{Id: accountID}) if err != nil { errCount.Add(1) - a.log.Errf("error unpausing accountID %d: %v", accountID, err) + a.log.Error(ctx, "error unpausing account", err, blog.Acct(accountID)) break } totalCount += response.Count @@ -138,7 +140,7 @@ func (a *admin) unpauseAccounts(ctx context.Context, accountIDs []int64, paralle // readUnpauseAccountFile parses the contents of a file containing one account // ID per into a slice of int64s. It will skip malformed records and continue // processing until the end of file marker. -func (a *admin) readUnpauseAccountFile(filePath string) ([]int64, error) { +func (a *admin) readUnpauseAccountFile(ctx context.Context, filePath string) ([]int64, error) { fp, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("opening paused account data file: %w", err) @@ -152,7 +154,10 @@ func (a *admin) readUnpauseAccountFile(filePath string) ([]int64, error) { lineCounter++ regID, err := strconv.ParseInt(scanner.Text(), 10, 64) if err != nil { - a.log.Infof("skipping: malformed account ID entry on line %d\n", lineCounter) + a.log.Info(ctx, "skipping malformed account ID entry", + slog.String("file", filePath), + slog.Int("line", lineCounter), + ) continue } unpauseAccounts = append(unpauseAccounts, regID) diff --git a/cmd/admin/unpause_account_test.go b/cmd/admin/unpause_account_test.go index f39b168fcbf..e29f7c06d6f 100644 --- a/cmd/admin/unpause_account_test.go +++ b/cmd/admin/unpause_account_test.go @@ -8,10 +8,11 @@ import ( "strings" "testing" - blog "github.com/letsencrypt/boulder/log" + "google.golang.org/grpc" + + "github.com/letsencrypt/boulder/blog" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" - "google.golang.org/grpc" ) func TestReadingUnpauseAccountsFile(t *testing.T) { @@ -53,7 +54,7 @@ func TestReadingUnpauseAccountsFile(t *testing.T) { err := os.WriteFile(file, []byte(strings.Join(testCase.data, "\n")), os.ModePerm) test.AssertNotError(t, err, "could not write temporary file") - regIDs, err := a.readUnpauseAccountFile(file) + regIDs, err := a.readUnpauseAccountFile(t.Context(), file) test.AssertNotError(t, err, "no error expected, but received one") test.AssertEquals(t, len(regIDs), testCase.expectedRegIDs) }) diff --git a/cmd/bad-key-revoker/main.go b/cmd/bad-key-revoker/main.go index a073b738c8d..e068bbd27e6 100644 --- a/cmd/bad-key-revoker/main.go +++ b/cmd/bad-key-revoker/main.go @@ -3,9 +3,11 @@ package notmain import ( "context" "database/sql" + "encoding/hex" "errors" "flag" "fmt" + "log/slog" "os" "time" @@ -15,12 +17,12 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/db" bgrpc "github.com/letsencrypt/boulder/grpc" - blog "github.com/letsencrypt/boulder/log" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/sa" @@ -197,12 +199,12 @@ func (bkr *badKeyRevoker) revokeCerts(ctx context.Context, certs []unrevokedCert // invoke exits early and returns true if there is no work to be done. // Otherwise, it processes a single key in the blockedKeys table and returns false. func (bkr *badKeyRevoker) invoke(ctx context.Context) (work bool, err error) { - logEvent := make(map[string]any) + var logAttrs []slog.Attr defer func() { if err != nil { - bkr.logger.AuditErr("Error while processing bad key", err, logEvent) + bkr.logger.AuditError(ctx, "Error while processing bad key", err, logAttrs...) } else { - bkr.logger.AuditInfo("Processed bad key", logEvent) + bkr.logger.AuditInfo(ctx, "Processed bad key", logAttrs...) } }() @@ -211,14 +213,14 @@ func (bkr *badKeyRevoker) invoke(ctx context.Context) (work bool, err error) { if err != nil { return false, err } - logEvent["keysToProcess"] = uncheckedCount + logAttrs = append(logAttrs, slog.Int("keysToProcess", uncheckedCount)) // Set the gauge to the number of rows to be processed (max: // blockedKeysGaugeLimit). bkr.keysToProcess.Set(float64(uncheckedCount)) if uncheckedCount >= blockedKeysGaugeLimit { - logEvent["keysToProcessOverflow"] = true + logAttrs = append(logAttrs, slog.Bool("keysToProcessOverflow", true)) } // select a row to process @@ -229,15 +231,17 @@ func (bkr *badKeyRevoker) invoke(ctx context.Context) (work bool, err error) { } return false, err } - logEvent["keyHash"] = fmt.Sprintf("%x", unchecked.KeyHash) - logEvent["revokedBy"] = unchecked.RevokedBy + logAttrs = append(logAttrs, + slog.String("keyHash", hex.EncodeToString(unchecked.KeyHash)), + slog.Int64("revokedBy", unchecked.RevokedBy), + ) // select all unrevoked, unexpired serials associated with the blocked key hash unrevokedCerts, err := bkr.findUnrevoked(ctx, unchecked) if err != nil { return false, err } - logEvent["certsToProcess"] = len(unrevokedCerts) + logAttrs = append(logAttrs, slog.Int("certsToProcess", len(unrevokedCerts))) if len(unrevokedCerts) == 0 { err = bkr.markRowChecked(ctx, unchecked) @@ -251,7 +255,7 @@ func (bkr *badKeyRevoker) invoke(ctx context.Context) (work bool, err error) { for _, cert := range unrevokedCerts { serials = append(serials, cert.Serial) } - logEvent["serials"] = serials + logAttrs = append(logAttrs, slog.Any("serials", serials)) // revoke each certificate err = bkr.revokeCerts(ctx, unrevokedCerts) @@ -303,7 +307,7 @@ type Config struct { MaxExpectedReplicationLag config.Duration `validate:"-"` } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } @@ -418,7 +422,7 @@ func (bkr *badKeyRevoker) backoff() { bkr.backoffIntervalMax, bkr.backoffFactor, ) - bkr.logger.Infof("backoff trying again in %.2f seconds", backoffDur.Seconds()) + bkr.logger.Info(context.Background(), "backing off", slog.Duration("retryAfter", backoffDur)) bkr.clk.Sleep(backoffDur) } diff --git a/cmd/bad-key-revoker/main_test.go b/cmd/bad-key-revoker/main_test.go index 19f11fdbc49..86ad9ba7aed 100644 --- a/cmd/bad-key-revoker/main_test.go +++ b/cmd/bad-key-revoker/main_test.go @@ -15,9 +15,9 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/db" - blog "github.com/letsencrypt/boulder/log" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/sa" "github.com/letsencrypt/boulder/test" @@ -454,7 +454,7 @@ func TestBackoffPolicy(t *testing.T) { // Backoff once. Check to make sure the backoff is logged. bkr.backoff() - resultLog := mocklog.GetAllMatching("INFO: backoff trying again in") + resultLog := mocklog.GetAllMatching(`level=INFO msg="backing off" retryAfter=`) if len(resultLog) == 0 { t.Fatalf("no backoff loglines found") } diff --git a/cmd/boulder-ca/main.go b/cmd/boulder-ca/main.go index bafd240a3a8..1995f513a41 100644 --- a/cmd/boulder-ca/main.go +++ b/cmd/boulder-ca/main.go @@ -9,6 +9,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/ca" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" @@ -107,7 +108,7 @@ type Config struct { PA cmd.PAConfig - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } @@ -194,7 +195,7 @@ func main() { } issuer, err := issuance.LoadIssuer(issuerConfig, clk) - cmd.FailOnError(err, "Loading issuer") + cmd.FailOnError(err, fmt.Sprintf("Loading issuer %q", issuerConfig.Location.CertFile)) issuers = append(issuers, issuer) } diff --git a/cmd/boulder-mtca/main.go b/cmd/boulder-mtca/main.go index dd358d2dd73..589d6747c17 100644 --- a/cmd/boulder-mtca/main.go +++ b/cmd/boulder-mtca/main.go @@ -1,3 +1,5 @@ +//go:build go1.27 + package notmain import ( @@ -7,6 +9,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/issuance" @@ -28,7 +31,7 @@ type Config struct { Issuer issuance.IssuerConfig } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/boulder-mtpublisher/main.go b/cmd/boulder-mtpublisher/main.go new file mode 100644 index 00000000000..921f88ab0c2 --- /dev/null +++ b/cmd/boulder-mtpublisher/main.go @@ -0,0 +1,75 @@ +package notmain + +import ( + "context" + "flag" + "os" + + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/config" + "github.com/letsencrypt/boulder/mtpublisher" + "github.com/letsencrypt/boulder/sa" +) + +type Config struct { + MTPublisher struct { + DB cmd.DBConfig + + DebugAddr string `validate:"omitempty,hostname_port"` + + // PollInterval is how often the stub scans for checkpoints that still + // lack a mirror cosignature. + PollInterval config.Duration `validate:"required"` + + // MTCLogID is the log this MTPublisher operates on (e.g. + // "44947.4.1.0.44"). Used as a guard on the `mtcLogID` column of the + // connected checkpoints table. + MTCLogID string `validate:"required"` + + // MirrorID identifies the cosigner this publisher writes alongside each + // cosignature (e.g. "32473.9"). + MirrorID string `validate:"required"` + } + Syslog blog.Config + OpenTelemetry cmd.OpenTelemetryConfig +} + +func main() { + debugAddr := flag.String("debug-addr", "", "Debug server address override") + configFile := flag.String("config", "", "File path to the configuration file for this service") + flag.Parse() + if *configFile == "" { + flag.Usage() + os.Exit(1) + } + + var c Config + err := cmd.ReadConfigFile(*configFile, &c) + cmd.FailOnError(err, "Reading JSON config file into config structure") + + if *debugAddr != "" { + c.MTPublisher.DebugAddr = *debugAddr + } + + scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.MTPublisher.DebugAddr) + defer oTelShutdown(context.Background()) + cmd.LogStartup(logger) + clk := clock.New() + + dbMap, err := sa.InitWrappedDb(c.MTPublisher.DB, scope, logger) + cmd.FailOnError(err, "While initializing dbMap") + + publisher, err := mtpublisher.New(dbMap, c.MTPublisher.PollInterval.Duration, c.MTPublisher.MTCLogID, c.MTPublisher.MirrorID, clk, logger) + cmd.FailOnError(err, "Failed to create MTPublisher stub") + + ctx, cancel := context.WithCancel(context.Background()) + go cmd.CatchSignals(cancel) + publisher.Start(ctx) +} + +func init() { + cmd.RegisterCommand("boulder-mtpublisher", main, &cmd.ConfigValidator{Config: &Config{}}) +} diff --git a/cmd/boulder-publisher/main.go b/cmd/boulder-publisher/main.go index dceeb6074e6..4ef265b7da3 100644 --- a/cmd/boulder-publisher/main.go +++ b/cmd/boulder-publisher/main.go @@ -10,6 +10,7 @@ import ( ct "github.com/google/certificate-transparency-go" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" @@ -35,7 +36,7 @@ type Config struct { Chains [][]string `validate:"min=1,dive,min=2,dive,required"` } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index cea9d75568d..e4409c8ffbf 100644 --- a/cmd/boulder-ra/main.go +++ b/cmd/boulder-ra/main.go @@ -3,11 +3,13 @@ package notmain import ( "context" "flag" + "fmt" "os" "time" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" @@ -20,6 +22,7 @@ import ( "github.com/letsencrypt/boulder/goodkey/sagoodkey" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/issuance" + mtcapb "github.com/letsencrypt/boulder/mtca/proto" "github.com/letsencrypt/boulder/policy" pubpb "github.com/letsencrypt/boulder/publisher/proto" "github.com/letsencrypt/boulder/ra" @@ -41,6 +44,8 @@ type Config struct { MaxContactsPerRegistration int + ProfileToMTCA map[string]*cmd.GRPCClientConfig + SAService *cmd.GRPCClientConfig VAService *cmd.GRPCClientConfig CAService *cmd.GRPCClientConfig @@ -79,8 +84,7 @@ type Config struct { // ValidationProfiles is a map of validation profiles to their // respective issuance allow lists. If a profile is not included in this - // mapping, it cannot be used by any account. If this field is left - // empty, all profiles are open to all accounts. + // mapping, it cannot be used by any account. ValidationProfiles map[string]*ra.ValidationProfileConfig `validate:"required"` // DefaultProfileName sets the profile to use if one wasn't provided by the @@ -121,7 +125,7 @@ type Config struct { PA cmd.PAConfig - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } @@ -182,6 +186,14 @@ func main() { vac := vapb.NewVAClient(vaConn) caaClient := vapb.NewCAAClient(vaConn) + profileToMTCA := make(map[string]mtcapb.MTCAClient) + for profile, clientConfig := range c.RA.ProfileToMTCA { + mtcaConn, err := bgrpc.ClientSetup(clientConfig, tlsConfig, scope, clk) + cmd.FailOnError(err, fmt.Sprintf("Unable to create MTCA client for profile %s", profile)) + mtcaClient := mtcapb.NewMTCAClient(mtcaConn) + profileToMTCA[profile] = mtcaClient + } + caConn, err := bgrpc.ClientSetup(c.RA.CAService, tlsConfig, scope, clk) cmd.FailOnError(err, "Unable to create CA client") cac := capb.NewCertificateAuthorityClient(caConn) @@ -228,6 +240,11 @@ func main() { cmd.Fail("At least one profile must be configured") } + for name, profile := range c.RA.ValidationProfiles { + if profile.MTC && c.RA.ProfileToMTCA[name] == nil { + cmd.Fail(fmt.Sprintf("profile %q is configured for MTC but has no MTCA backend", name)) + } + } validationProfiles, err := ra.NewValidationProfiles(c.RA.DefaultProfileName, c.RA.ValidationProfiles) cmd.FailOnError(err, "Failed to load validation profiles") @@ -279,6 +296,7 @@ func main() { c.RA.FinalizeTimeout.Duration, ctp, issuerCerts, + profileToMTCA, ) defer rai.Drain() diff --git a/cmd/boulder-sa/main.go b/cmd/boulder-sa/main.go index 196d03a7c67..df51702c453 100644 --- a/cmd/boulder-sa/main.go +++ b/cmd/boulder-sa/main.go @@ -7,6 +7,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/db" @@ -33,7 +34,7 @@ type Config struct { LagFactor config.Duration `validate:"-"` } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/boulder-va/main.go b/cmd/boulder-va/main.go index d5b48097f68..9e6593a226c 100644 --- a/cmd/boulder-va/main.go +++ b/cmd/boulder-va/main.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/features" @@ -81,7 +82,7 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/boulder-wfe2/main.go b/cmd/boulder-wfe2/main.go index 86948e8fcec..fbf77012fe7 100644 --- a/cmd/boulder-wfe2/main.go +++ b/cmd/boulder-wfe2/main.go @@ -6,14 +6,17 @@ import ( "encoding/pem" "flag" "fmt" + "log/slog" "net/http" "os" "time" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" + berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey/sagoodkey" @@ -27,6 +30,7 @@ import ( bredis "github.com/letsencrypt/boulder/redis" sapb "github.com/letsencrypt/boulder/sa/proto" emailpb "github.com/letsencrypt/boulder/salesforce/email/proto" + "github.com/letsencrypt/boulder/strictyaml" "github.com/letsencrypt/boulder/unpause" "github.com/letsencrypt/boulder/web" "github.com/letsencrypt/boulder/wfe2" @@ -147,6 +151,10 @@ type Config struct { // contacts than this are rejected. Default: 10. MaxContactsPerRegistration int `validate:"omitempty,min=1"` + // MaxCumulativeIdentifierLength is the maximum allowed total bytes of + // all identifier values in a new-order request. Default (0) means no limit. + MaxCumulativeIdentifierLength int `validate:"omitempty,min=1"` + AccountCache *CacheConfig Limiter struct { @@ -210,9 +218,14 @@ type Config struct { // repeats. We don't want to issue certs for names that look like they // result from this process. BlockedOnDemandLabels []string `validate:"omitempty"` + + // BlockedAccountsFile is the path to a YAML file listing regIDs which are + // blocked from requesting issuance of new certificates. If empty, no + // accounts are blocked. + BlockedAccountsFile string `validate:"omitempty"` } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig // OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests @@ -224,6 +237,56 @@ type CacheConfig struct { TTL config.Duration } +type blockedAccountsPolicy struct { + BlockedAccountIDs []int64 `yaml:"BlockedAccountIDs"` + Message string `yaml:"Message"` +} + +// accountBlocker implements wfe2.AccountBlocker. +type accountBlocker struct { + message string + blocked map[int64]bool +} + +var _ wfe2.AccountBlocker = new(accountBlocker) + +func (b *accountBlocker) CheckAccountID(id int64) error { + if b.blocked[id] { + return berrors.UnauthorizedError("%s", b.message) + } + return nil +} + +// loadBlockedAccountsFile parses the YAML file at the given path and returns +// the set of blocked account IDs it contains. +// TODO: Update the schema to handle multiple lists each with their own +// message string. +func loadBlockedAccountsFile(f string) (*accountBlocker, error) { + configBytes, err := os.ReadFile(f) + if err != nil { + return nil, err + } + + var policy blockedAccountsPolicy + err = strictyaml.Unmarshal(configBytes, &policy) + if err != nil { + return nil, err + } + + blocked := make(map[int64]bool, 0) + for _, id := range policy.BlockedAccountIDs { + if id <= 0 { + return nil, fmt.Errorf("malformed BlockedAccountIDs entry, not a positive integer: %d", id) + } + blocked[id] = true + } + + return &accountBlocker{ + message: policy.Message, + blocked: blocked, + }, nil +} + // loadChain takes a list of filenames containing pem-formatted certificates, // and returns a chain representing all of those certificates in order. It // ensures that the resulting chain is valid. The final file is expected to be @@ -380,6 +443,12 @@ func main() { overridesRefresherShutdown = txnBuilder.NewRefresher(30 * time.Minute) } + var acctBlocker wfe2.AccountBlocker + if c.WFE.BlockedAccountsFile != "" { + acctBlocker, err = loadBlockedAccountsFile(c.WFE.BlockedAccountsFile) + cmd.FailOnError(err, "Couldn't load blocked accounts file") + } + var accountGetter wfe2.AccountGetter if c.WFE.AccountCache != nil { accountGetter = wfe2.NewAccountCache(sac, @@ -400,6 +469,7 @@ func main() { c.WFE.Timeout.Duration, c.WFE.StaleTimeout.Duration, c.WFE.MaxContactsPerRegistration, + c.WFE.MaxCumulativeIdentifierLength, rac, sac, eec, @@ -414,6 +484,7 @@ func main() { c.WFE.Unpause.JWTLifetime.Duration, c.WFE.Unpause.URL, c.WFE.BlockedOnDemandLabels, + acctBlocker, c.WFE.DirectoryCAAIdentity, ) cmd.FailOnError(err, "Unable to create WFE") @@ -428,11 +499,11 @@ func main() { cmd.Fail("HTTP listen address is not configured") } - logger.Infof("Server running, listening on %s....", c.WFE.ListenAddress) handler := wfe.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...) srv := web.NewServer(c.WFE.ListenAddress, handler, logger) go func() { + logger.Info(context.Background(), "HTTP server listening", slog.String("addr", c.WFE.ListenAddress)) err := srv.ListenAndServe() if err != nil && err != http.ErrServerClosed { cmd.FailOnError(err, "Running HTTP server") @@ -442,7 +513,7 @@ func main() { tlsSrv := web.NewServer(c.WFE.TLSListenAddress, handler, logger) if tlsSrv.Addr != "" { go func() { - logger.Infof("TLS server listening on %s", tlsSrv.Addr) + logger.Info(context.Background(), "TLS server listening", slog.String("addr", tlsSrv.Addr)) err := tlsSrv.ListenAndServeTLS(c.WFE.ServerCertificatePath, c.WFE.ServerKeyPath) if err != nil && err != http.ErrServerClosed { cmd.FailOnError(err, "Running TLS server") diff --git a/cmd/boulder/main.go b/cmd/boulder/main.go index f202fae8de5..7d2f4ef77e9 100644 --- a/cmd/boulder/main.go +++ b/cmd/boulder/main.go @@ -7,7 +7,6 @@ import ( _ "github.com/letsencrypt/boulder/cmd/bad-key-revoker" _ "github.com/letsencrypt/boulder/cmd/boulder-ca" - _ "github.com/letsencrypt/boulder/cmd/boulder-mtca" _ "github.com/letsencrypt/boulder/cmd/boulder-observer" _ "github.com/letsencrypt/boulder/cmd/boulder-publisher" _ "github.com/letsencrypt/boulder/cmd/boulder-ra" diff --git a/cmd/boulder/main_next.go b/cmd/boulder/main_next.go new file mode 100644 index 00000000000..56466d9ebe5 --- /dev/null +++ b/cmd/boulder/main_next.go @@ -0,0 +1,8 @@ +//go:build go1.27 + +package main + +import ( + _ "github.com/letsencrypt/boulder/cmd/boulder-mtca" + _ "github.com/letsencrypt/boulder/cmd/boulder-mtpublisher" +) diff --git a/cmd/boulder/main_test.go b/cmd/boulder/main_test.go index d27e56e8837..20f65cfdda8 100644 --- a/cmd/boulder/main_test.go +++ b/cmd/boulder/main_test.go @@ -37,6 +37,8 @@ func TestConfigValidation(t *testing.T) { fileNames = []string{"observer.yml"} case "boulder-publisher": fileNames = []string{"publisher.json"} + case "boulder-mtpublisher": + fileNames = []string{"mtpublisher.json"} case "boulder-ra": fileNames = []string{"ra.json"} case "boulder-sa": diff --git a/cmd/ceremony/file.go b/cmd/ceremony/file.go index 752d7b7465e..fb27a99933f 100644 --- a/cmd/ceremony/file.go +++ b/cmd/ceremony/file.go @@ -9,6 +9,7 @@ func writeFile(filename string, bytes []byte) error { if err != nil { return err } + defer f.Close() _, err = f.Write(bytes) return err } diff --git a/cmd/cert-checker/main.go b/cmd/cert-checker/main.go index 8590c5f0a36..bc8765b5dbd 100644 --- a/cmd/cert-checker/main.go +++ b/cmd/cert-checker/main.go @@ -4,13 +4,15 @@ import ( "bytes" "context" "crypto/x509" - "encoding/json" "flag" "fmt" + "log/slog" + "net" "net/netip" "os" "regexp" "slices" + "strings" "sync" "sync/atomic" "time" @@ -22,6 +24,7 @@ import ( "github.com/zmap/zlint/v3" "github.com/zmap/zlint/v3/lint" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" @@ -32,12 +35,46 @@ import ( "github.com/letsencrypt/boulder/goodkey/sagoodkey" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/linter" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/precert" "github.com/letsencrypt/boulder/sa" ) +type certCheckerMetrics struct { + checkerLatency prometheus.Histogram + checkerTimestamp prometheus.Gauge + checkerGoodCount prometheus.Gauge + checkerBadCount prometheus.Gauge +} + +func newCertCheckerMetrics(stats prometheus.Registerer) *certCheckerMetrics { + checkerLatency := promauto.With(stats).NewHistogram(prometheus.HistogramOpts{ + Name: "cert_checker_latency", + Help: "Histogram of latencies a cert-checker worker takes to complete a batch", + }) + + checkerTimestamp := promauto.With(stats).NewGauge(prometheus.GaugeOpts{ + Name: "cert_checker_last_run_timestamp", + Help: "Timestamp of cert-checker's last run", + }) + + checkerGoodCount := promauto.With(stats).NewGauge(prometheus.GaugeOpts{ + Name: "cert_checker_good_count", + Help: "Cert-checker count of good certificates", + }) + + checkerBadCount := promauto.With(stats).NewGauge(prometheus.GaugeOpts{ + Name: "cert_checker_bad_count", + Help: "Cert-checker count of bad certificates", + }) + return &certCheckerMetrics{ + checkerLatency: checkerLatency, + checkerTimestamp: checkerTimestamp, + checkerGoodCount: checkerGoodCount, + checkerBadCount: checkerBadCount, + } +} + // For defense-in-depth in addition to using the PA & its identPolicy to check // domain names we also perform a check against the regex's from the // forbiddenDomains array @@ -62,25 +99,9 @@ var batchSize = 1000 type report struct { begin time.Time end time.Time - GoodCerts int64 `json:"good-certs"` - BadCerts int64 `json:"bad-certs"` - DbErrs int64 `json:"db-errs"` - Entries map[string]reportEntry `json:"entries"` -} - -func (r *report) dump() error { - content, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err - } - fmt.Fprintln(os.Stdout, string(content)) - return nil -} - -type reportEntry struct { - Valid bool `json:"valid"` - SANs []string `json:"sans"` - Problems []string `json:"problems,omitempty"` + GoodCerts int64 `json:"good-certs"` + BadCerts int64 `json:"bad-certs"` + DbErrs int64 `json:"db-errs"` } // certDB is an interface collecting the borp.DbMap functions that the various @@ -102,7 +123,6 @@ type certChecker struct { getPrecert precertGetter certs chan *corepb.Certificate clock clock.Clock - rMu *sync.Mutex issuedReport report checkPeriod time.Duration acceptableValidityDurations map[time.Duration]bool @@ -132,9 +152,7 @@ func newChecker(saDbMap certDB, dbMap: saDbMap, getPrecert: precertGetter, certs: make(chan *corepb.Certificate, batchSize), - rMu: new(sync.Mutex), clock: clk, - issuedReport: report{Entries: make(map[string]reportEntry)}, checkPeriod: period, acceptableValidityDurations: avd, lints: lints, @@ -170,11 +188,11 @@ func (c *certChecker) findStartingID(ctx context.Context, begin, end time.Time) }, ) if err != nil { - c.logger.AuditErr("finding starting certificate", err, map[string]any{ - "begin": queryBegin.Format(time.RFC3339), - "end": queryEnd.Format(time.RFC3339), - "attempt": retries + 1, - }) + c.logger.AuditError(ctx, "finding starting certificate", err, + slog.Time("begin", queryBegin), + slog.Time("end", queryEnd), + slog.Int("attempt", retries+1), + ) retries++ time.Sleep(core.RetryBackoff(retries, time.Second, time.Minute, 2)) continue @@ -236,12 +254,12 @@ func (c *certChecker) getCerts(ctx context.Context) error { }, ) if err != nil { - c.logger.AuditErr("selecting certificates", err, map[string]any{ - "begin": c.issuedReport.begin.Format(time.RFC3339), - "end": c.issuedReport.end.Format(time.RFC3339), - "batchStartID": batchStartID, - "attempt": retries + 1, - }) + c.logger.AuditError(ctx, "selecting certificates", err, + slog.Time("begin", c.issuedReport.begin), + slog.Time("end", c.issuedReport.end), + slog.Int64("batchStartID", batchStartID), + slog.Int("attempt", retries+1), + ) retries++ time.Sleep(core.RetryBackoff(retries, time.Second, time.Minute, 2)) continue @@ -265,26 +283,21 @@ func (c *certChecker) getCerts(ctx context.Context) error { return nil } -func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool) { +func (c *certChecker) processCerts(ctx context.Context) { for cert := range c.certs { sans, problems := c.checkCert(ctx, cert) - valid := len(problems) == 0 - c.rMu.Lock() - if !badResultsOnly || (badResultsOnly && !valid) { - c.issuedReport.Entries[cert.Serial] = reportEntry{ - Valid: valid, - SANs: sans, - Problems: problems, - } - } - c.rMu.Unlock() - if !valid { + if len(problems) != 0 { atomic.AddInt64(&c.issuedReport.BadCerts, 1) + c.logger.Error(ctx, "certificate error found", + fmt.Errorf("detected %d problems", len(problems)), + blog.Serial(cert.Serial), + blog.Idents(sans...), + slog.String("probs", strings.Join(problems, "; ")), + ) } else { atomic.AddInt64(&c.issuedReport.GoodCerts, 1) } } - wg.Done() } // Extensions that we allow in certificates @@ -343,7 +356,8 @@ func (c *certChecker) checkValidations(ctx context.Context, cert *corepb.Certifi } // checkCert returns a list of Subject Alternative Names in the certificate and a list of problems with the certificate. -func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ([]string, []string) { +func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) (identifier.ACMEIdentifiers, []string) { + ctx = blog.ContextWith(ctx, blog.Serial(cert.Serial)) var problems []string // Check that the digests match. @@ -359,12 +373,6 @@ func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ( return nil, problems } - // Now that it's parsed, we can extract the SANs. - sans := slices.Clone(parsedCert.DNSNames) - for _, ip := range parsedCert.IPAddresses { - sans = append(sans, ip.String()) - } - // Run zlint checks. results := zlint.LintCertificateEx(parsedCert, c.lints) for name, res := range results.Results { @@ -430,7 +438,7 @@ func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ( } // Check that the CommonName is included in the SANs. - if !slices.Contains(sans, parsedCert.Subject.CommonName) { + if !slices.Contains(parsedCert.DNSNames, parsedCert.Subject.CommonName) { problems = append(problems, fmt.Sprintf("Certificate Common Name does not appear in Subject Alternative Names: %q !< %v", parsedCert.Subject.CommonName, parsedCert.DNSNames)) } @@ -506,7 +514,7 @@ func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ( if err != nil { // Log and continue, since we want the problems slice to only contains // problems with the cert itself. - c.logger.Errf("fetching linting precertificate for %s: %s", cert.Serial, err) + c.logger.Error(ctx, "fetching linting precertificate", err) atomic.AddInt64(&c.issuedReport.DbErrs, 1) } else { err = precert.Correspond(precertDER, cert.Der) @@ -522,16 +530,12 @@ func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ( if features.Get().CertCheckerRequiresValidations { problems = append(problems, err.Error()) } else { - var identValues []string - for _, ident := range idents { - identValues = append(identValues, ident.Value) - } - c.logger.Warningf("Certificate %s %s: %s", cert.Serial, identValues, err) + c.logger.Warn(ctx, "Certificate validation check failed", blog.Idents(idents...), blog.Error(err)) } } } - return sans, problems + return identifier.FromCert(p), problems } type Config struct { @@ -540,8 +544,19 @@ type Config struct { cmd.HostnamePolicyConfig Workers int `validate:"required,min=1"` - // Deprecated: this is ignored, and cert checker always checks both expired and unexpired. - UnexpiredOnly bool + // LookupDNSAuthority can only be specified with PushgatewayService. It's a single + // : of the DNS server to be used for resolution + // of pushgateway backends. If the address contains a hostname it will be resolved + // using system DNS. If the address contains a port, the client will use it + // directly, otherwise port 53 is used. + LookupDNSAuthority string `validate:"excluded_without=PushgatewayService,required_with=PushgatewayService,omitempty,ip|hostname|hostname_port"` + // PushgatewayService entry contains a service and domain name that will be used + // to construct a SRV DNS query to lookup pushgateway backends. For example: if + // the resource record is 'foo.service.consul', then the 'Service' is 'foo' + // and the 'Domain' is 'service.consul'. The expected dNSName to be + // authenticated in the server certificate would be 'foo.service.consul'. + PushgatewayService *cmd.ServiceDomain `validate:"required_with=LookupDNSAuthority"` + // Deprecated: cert-checker only logs bad results anyway. BadResultsOnly bool CheckPeriod config.Duration @@ -574,7 +589,48 @@ type Config struct { Features features.Config } PA cmd.PAConfig - Syslog cmd.SyslogConfig + Syslog blog.Config +} + +// getPushgatewayURL resolves svc via SRV+A lookups against dnsAuthority and +// returns an http:// URL whose host is an IP address. Both lookups go through +// dnsAuthority (typically Consul DNS) because the system resolver can't answer +// queries for the .consul domain. The SRV target is then flattened to an IP +// because the returned URL is consumed by net/http via cmd.PushMetrics, which +// resolves hostnames using the system resolver. Scheme is fixed to http: +// pushgateway is assumed to be on an internal network +func getPushgatewayURL(ctx context.Context, dnsAuthority string, svc cmd.ServiceDomain) (string, error) { + host, port, err := net.SplitHostPort(dnsAuthority) + if err != nil { + // Assume only hostname or IPv4 address was specified. + host = dnsAuthority + port = "53" + } + r := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, net.JoinHostPort(host, port)) + }, + } + _, targets, err := r.LookupSRV(ctx, svc.Service, "tcp", svc.Domain) + if err != nil { + return "", fmt.Errorf("SRV lookup of _%s._tcp.%s failed: %w", svc.Service, svc.Domain, err) + } + if len(targets) == 0 { + return "", fmt.Errorf("SRV lookup of _%s._tcp.%s returned 0 results", svc.Service, svc.Domain) + } + // Flatten the SRV target to an IP using the same Consul authority; net/http + // (used downstream) would otherwise try to resolve names like + // *.addr.dc1.consul via the system resolver and fail. + target := strings.TrimSuffix(targets[0].Target, ".") + addrs, err := r.LookupHost(ctx, target) + if err != nil { + return "", fmt.Errorf("A/AAAA lookup of %q failed: %w", target, err) + } + if len(addrs) == 0 { + return "", fmt.Errorf("A/AAAA lookup of %q returned 0 results", target) + } + return fmt.Sprintf("http://%s", net.JoinHostPort(addrs[0], fmt.Sprint(targets[0].Port))), nil } func main() { @@ -594,6 +650,9 @@ func main() { logger := cmd.NewLogger(config.Syslog) cmd.LogStartup(logger) + reg := prometheus.NewRegistry() + metrics := newCertCheckerMetrics(reg) + acceptableValidityDurations := make(map[time.Duration]bool) if len(config.CertChecker.AcceptableValidityDurations) > 0 { for _, entry := range config.CertChecker.AcceptableValidityDurations { @@ -616,11 +675,6 @@ func main() { saDbMap, err := sa.InitWrappedDb(config.CertChecker.DB, prometheus.DefaultRegisterer, logger) cmd.FailOnError(err, "While initializing dbMap") - checkerLatency := promauto.NewHistogram(prometheus.HistogramOpts{ - Name: "cert_checker_latency", - Help: "Histogram of latencies a cert-checker worker takes to complete a batch", - }) - pa, err := policy.New(config.PA.Identifiers, config.PA.Challenges, logger) cmd.FailOnError(err, "Failed to create PA") @@ -652,34 +706,54 @@ func main() { ) fmt.Fprintf(os.Stderr, "# Getting certificates issued in the last %s\n", config.CertChecker.CheckPeriod) + // No timeout, since cert-checker can take several hours to run. + ctx := context.Background() + // Since we grab certificates in batches we don't want this to block, when it // is finished it will close the certificate channel which allows the range // loops in checker.processCerts to break go func() { - err := checker.getCerts(context.TODO()) + err := checker.getCerts(ctx) cmd.FailOnError(err, "Batch retrieval of certificates failed") }() fmt.Fprintf(os.Stderr, "# Processing certificates using %d workers\n", config.CertChecker.Workers) wg := new(sync.WaitGroup) for range config.CertChecker.Workers { - wg.Add(1) - go func() { + wg.Go(func() { s := checker.clock.Now() - checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly) - checkerLatency.Observe(checker.clock.Since(s).Seconds()) - }() + checker.processCerts(context.Background()) + metrics.checkerLatency.Observe(checker.clock.Since(s).Seconds()) + }) } wg.Wait() - fmt.Fprintf( - os.Stderr, - "# Finished processing certificates, report length: %d, good: %d, bad: %d\n", - len(checker.issuedReport.Entries), - checker.issuedReport.GoodCerts, - checker.issuedReport.BadCerts, + logger.Info(ctx, "Finished processing certificates", + slog.Time("begin", checker.issuedReport.begin), + slog.Time("end", checker.issuedReport.end), + slog.Int64("goodCerts", checker.issuedReport.GoodCerts), + slog.Int64("badCerts", checker.issuedReport.BadCerts), + slog.Int64("dbErrs", checker.issuedReport.DbErrs), ) - err = checker.issuedReport.dump() - cmd.FailOnError(err, "Failed to dump results: %s\n") + + metrics.checkerTimestamp.SetToCurrentTime() + metrics.checkerGoodCount.Set(float64(checker.issuedReport.GoodCerts)) + metrics.checkerBadCount.Set(float64(checker.issuedReport.BadCerts)) + + if config.CertChecker.PushgatewayService != nil { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + pushgatewayURL, err := getPushgatewayURL(ctx, config.CertChecker.LookupDNSAuthority, *config.CertChecker.PushgatewayService) + if err != nil { + logger.Error(ctx, "failed to get pushgateway URL", err) + } else { + err = cmd.PushMetrics("cert-checker", pushgatewayURL, reg, logger) + if err != nil { + logger.Error(ctx, "failed to push metrics to pushgateway", err, slog.String("url", pushgatewayURL)) + } else { + logger.Debug(ctx, "pushed metrics to pushgateway", slog.String("url", pushgatewayURL)) + } + } + } if checker.issuedReport.BadCerts > 0 { os.Exit(1) diff --git a/cmd/cert-checker/main_test.go b/cmd/cert-checker/main_test.go index 9ffcf1beafa..156bef5bbaf 100644 --- a/cmd/cert-checker/main_test.go +++ b/cmd/cert-checker/main_test.go @@ -14,16 +14,21 @@ import ( "log" "math/big" mrand "math/rand/v2" + "net" + "net/netip" + "net/url" "os" "slices" + "strconv" "strings" - "sync" "testing" "time" "github.com/jmhodges/clock" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/ctpolicy/loglist" @@ -31,7 +36,6 @@ import ( "github.com/letsencrypt/boulder/goodkey/sagoodkey" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/linter" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/sa" @@ -169,8 +173,12 @@ func TestCheckCertReturnsSANs(t *testing.T) { } names, problems := checker.checkCert(context.Background(), cert) - if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com", "127.0.0.1"}) { - t.Errorf("didn't get expected DNS names. other problems: %s", strings.Join(problems, "\n")) + if !slices.Equal(names, identifier.ACMEIdentifiers{ + identifier.NewDNS("al--so--wr--ong.com"), + identifier.NewDNS("quite_invalid.com"), + identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + }) { + t.Errorf("didn't get expected DNS names (got = %#v). other problems: %s", names, strings.Join(problems, "\n")) } } @@ -336,7 +344,8 @@ func TestGetAndProcessCerts(t *testing.T) { fc := clock.NewFake() fc.Set(fc.Now().Add(time.Hour)) - checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock()) + mocklog := blog.NewMock() + checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, mocklog) sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, 0, fc, blog.NewMock(), metrics.NoopRegisterer) test.AssertNotError(t, err, "Couldn't create SA to insert certificates") saCleanUp := test.ResetBoulderTestDatabase(t) @@ -375,11 +384,9 @@ func TestGetAndProcessCerts(t *testing.T) { err = checker.getCerts(context.Background()) test.AssertNotError(t, err, "Failed to retrieve certificates") test.AssertEquals(t, len(checker.certs), 5) - wg := new(sync.WaitGroup) - wg.Add(1) - checker.processCerts(context.Background(), wg, false) + checker.processCerts(t.Context()) test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5)) - test.AssertEquals(t, len(checker.issuedReport.Entries), 5) + test.AssertEquals(t, len(mocklog.GetAllMatching("certificate error found")), 5) } // mismatchedCountDB is a certDB implementation for `getCerts` that returns one @@ -507,30 +514,6 @@ func TestGetCertsLate(t *testing.T) { } } -func TestSaveReport(t *testing.T) { - r := report{ - begin: time.Time{}, - end: time.Time{}, - GoodCerts: 2, - BadCerts: 1, - Entries: map[string]reportEntry{ - "020000000000004b475da49b91da5c17": { - Valid: true, - }, - "020000000000004d1613e581432cba7e": { - Valid: true, - }, - "020000000000004e402bc21035c6634a": { - Valid: false, - Problems: []string{"None really..."}, - }, - }, - } - - err := r.dump() - test.AssertNotError(t, err, "Failed to dump results") -} - func TestIsForbiddenDomain(t *testing.T) { // Note: These testcases are not an exhaustive representation of domains // Boulder won't issue for, but are instead testing the defense-in-depth @@ -617,6 +600,7 @@ func TestIgnoredLint(t *testing.T) { template.DNSNames = []string{"zombo.com"} template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} + template.CRLDistributionPoints = []string{"http://crl.example.org"} template.IsCA = false subjectCertDer, err := x509.CreateCertificate(rand.Reader, template, issuerCert, testKey.Public(), testKey) @@ -698,3 +682,58 @@ func TestPrecertCorrespond(t *testing.T) { } t.Fatalf("expected precert correspondence problem, but got: %v", problems) } + +func TestGetPushgatewayURL(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + gotURL, err := getPushgatewayURL(t.Context(), "consul.service.consul:53", + cmd.ServiceDomain{Service: "redisratelimits", Domain: "service.consul"}) + if err != nil { + t.Fatalf("getPushgatewayURL(consul.service.consul:53) = %s, but want success", err) + } + + parsed, err := url.Parse(gotURL) + if err != nil { + t.Fatalf("returned URL should be parseable: %s", err) + } + if parsed.Scheme != "http" { + t.Errorf("expected http scheme but got %s", parsed.Scheme) + } + + host, port, err := net.SplitHostPort(parsed.Host) + if err != nil { + t.Errorf("URL host should contain a port: %s", err) + } + if net.ParseIP(host) == nil { + t.Errorf("host should be an IP (LookupHost flatten step)") + } + + portNum, err := strconv.Atoi(port) + if err != nil { + t.Errorf("port should be numeric: %s", err) + } + if portNum < 0 || portNum > 65536 { + t.Errorf("port should be in a valid range but got %d", portNum) + } + }) + t.Run("DNS authority no port specified", func(t *testing.T) { + _, err := getPushgatewayURL(t.Context(), "consul.service.consul", + cmd.ServiceDomain{Service: "redisratelimits", Domain: "service.consul"}) + if err != nil { + t.Fatalf("getPushgatewayURL(consul.service.consul:53) = %s, but want success", err) + } + }) + t.Run("SRV not found", func(t *testing.T) { + _, err := getPushgatewayURL(t.Context(), "consul.service.consul:53", + cmd.ServiceDomain{Service: "doesnotexist", Domain: "service.consul"}) + if err == nil { + t.Errorf("getPushgatewayURL for 'doesnotexist' service should have failed") + } + }) + t.Run("DNS authority unreachable", func(t *testing.T) { + _, err := getPushgatewayURL(t.Context(), "doesnotexist.invalid:53", + cmd.ServiceDomain{Service: "redisratelimits", Domain: "service.consul"}) + if err == nil { + t.Fatalf("getPushgatewayURL(doesnotexist.invalid:53) should have failed") + } + }) +} diff --git a/cmd/config.go b/cmd/config.go index 853d2b61ffd..49ae2d99105 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -223,20 +223,6 @@ func (t *TLSConfig) Load(scope prometheus.Registerer) (*tls.Config, error) { }, nil } -// SyslogConfig defines the config for syslogging. -// 3 means "error", 4 means "warning", 6 is "info" and 7 is "debug". -// Configuring a given level causes all messages at that level and below to -// be logged. -type SyslogConfig struct { - // When absent or zero, this causes no logs to be emitted on stdout/stderr. - // Errors and warnings will be emitted on stderr if the configured level - // allows. - StdoutLevel int `validate:"min=-1,max=7"` - // When absent or zero, this defaults to logging all messages of level 6 - // or below. To disable syslog logging entirely, set this to -1. - SyslogLevel int `validate:"min=-1,max=7"` -} - // ServiceDomain contains the service and domain name the gRPC or bdns provider // will use to construct a SRV DNS query to lookup backends. type ServiceDomain struct { diff --git a/cmd/crl-checker/main.go b/cmd/crl-checker/main.go index 69f62352b12..c3398caec2c 100644 --- a/cmd/crl-checker/main.go +++ b/cmd/crl-checker/main.go @@ -1,17 +1,21 @@ package notmain import ( + "context" "crypto/x509" "encoding/json" + "errors" "flag" "fmt" "io" + "log/slog" "net/http" "net/url" "os" "strings" "time" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/crl/checker" @@ -49,8 +53,9 @@ func main() { save := flag.Bool("save", false, "save CRLs to files named after the URL") flag.Parse() - logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: -1}) + logger := cmd.NewLogger(blog.Config{StdoutLevel: 6, SyslogLevel: -1}) cmd.LogStartup(logger) + ctx := context.Background() urlFileContents, err := os.ReadFile(*urlFile) cmd.FailOnError(err, "Reading CRL URLs file") @@ -68,7 +73,7 @@ func main() { issuer, err = core.LoadCert(*issuerFile) cmd.FailOnError(err, "Loading issuer certificate") } else { - logger.Warning("CRL signature validation disabled") + logger.Warn(ctx, "CRL signature validation disabled") } ageLimit, err := time.ParseDuration(*ageLimitStr) @@ -79,23 +84,25 @@ func main() { totalBytes := 0 oldestTimestamp := time.Time{} for _, u := range urls { + ctx := blog.ContextWith(ctx, slog.String("url", u)) + crl, err := downloadShard(u) if err != nil { errCount += 1 - logger.Errf("fetching CRL %q failed: %s", u, err) + logger.Error(ctx, "fetching CRL failed", err) continue } if *save { parsedURL, err := url.Parse(u) if err != nil { - logger.Errf("parsing url: %s", err) + logger.Error(ctx, "parsing url", err) continue } filename := fmt.Sprintf("%s%s", parsedURL.Host, strings.ReplaceAll(parsedURL.Path, "/", "_")) err = os.WriteFile(filename, crl.Raw, 0660) if err != nil { - logger.Errf("writing file: %s", err) + logger.Error(ctx, "writing file", err) continue } } @@ -105,14 +112,14 @@ func main() { zcrl, err := x509.ParseRevocationList(crl.Raw) if err != nil { errCount += 1 - logger.Errf("parsing CRL %q failed: %s", u, err) + logger.Error(ctx, "parsing CRL failed", err) continue } err = checker.Validate(zcrl, issuer, ageLimit) if err != nil { errCount += 1 - logger.Errf("checking CRL %q failed: %s", u, err) + logger.Error(ctx, "checking CRL failed", err) continue } @@ -124,7 +131,7 @@ func main() { serial := core.SerialToString(c.SerialNumber) if _, seen := seenSerials[serial]; seen { errCount += 1 - logger.Errf("serial seen in multiple shards: %s", serial) + logger.Error(ctx, "serial seen in multiple shards", errors.New("duplicate serial"), blog.Serial(serial)) continue } seenSerials[serial] = struct{}{} @@ -141,12 +148,12 @@ func main() { cmd.Fail(fmt.Sprintf("Encountered %d errors", errCount)) } - logger.AuditInfo("CRL checking complete", map[string]string{ - "numCRLs": fmt.Sprintf("%d", len(urls)), - "numSerials": fmt.Sprintf("%d", len(seenSerials)), - "numBytes": fmt.Sprintf("%d", totalBytes), - "oldestCRL": oldestTimestamp.Format(time.RFC3339), - }) + logger.AuditInfo(ctx, "CRL checking complete", + slog.Int("numCRLs", len(urls)), + slog.Int("numSerials", len(seenSerials)), + slog.Int("numBytes", totalBytes), + slog.Time("oldestCRL", oldestTimestamp), + ) } func init() { diff --git a/cmd/crl-storer/main.go b/cmd/crl-storer/main.go index acc15684be4..47b5b0f6838 100644 --- a/cmd/crl-storer/main.go +++ b/cmd/crl-storer/main.go @@ -3,6 +3,7 @@ package notmain import ( "context" "flag" + "fmt" "net/http" "os" @@ -12,13 +13,13 @@ import ( awsl "github.com/aws/smithy-go/logging" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/crl/storer" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" ) type Config struct { @@ -50,11 +51,13 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } // awsLogger implements the github.com/aws/smithy-go/logging.Logger interface. +// It is defined here, instead of in //blog/adapters.go, because it is only +// used locally rather than set as a package-level default. type awsLogger struct { blog.Logger } @@ -62,9 +65,9 @@ type awsLogger struct { func (log awsLogger) Logf(c awsl.Classification, format string, v ...any) { switch c { case awsl.Debug: - log.Debugf(format, v...) + log.Debug(context.Background(), fmt.Sprintf(format, v...)) case awsl.Warn: - log.Warningf(format, v...) + log.Warn(context.Background(), fmt.Sprintf(format, v...)) } } diff --git a/cmd/crl-updater/main.go b/cmd/crl-updater/main.go index dda581c6ba8..590c3c0c9ca 100644 --- a/cmd/crl-updater/main.go +++ b/cmd/crl-updater/main.go @@ -9,6 +9,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" @@ -118,7 +119,7 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/email-exporter/main.go b/cmd/email-exporter/main.go index 164ae12d24c..f6e34c05932 100644 --- a/cmd/email-exporter/main.go +++ b/cmd/email-exporter/main.go @@ -7,6 +7,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/salesforce" @@ -58,7 +59,7 @@ type Config struct { // of memory. If left unset, no caching is performed. EmailCacheSize int `validate:"omitempty,min=1"` } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/log-validator/main.go b/cmd/log-validator/main.go index a292ddc371e..a60e1bbce90 100644 --- a/cmd/log-validator/main.go +++ b/cmd/log-validator/main.go @@ -4,14 +4,15 @@ import ( "context" "flag" + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/blog/validator" "github.com/letsencrypt/boulder/cmd" - "github.com/letsencrypt/boulder/log/validator" ) type Config struct { Files []string `validate:"min=1,dive,required"` DebugAddr string `validate:"omitempty,hostname_port"` - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/memorymonitor.go b/cmd/memorymonitor.go index 27654e29594..e60fa8d579f 100644 --- a/cmd/memorymonitor.go +++ b/cmd/memorymonitor.go @@ -1,7 +1,9 @@ package cmd import ( + "context" "fmt" + "log/slog" "math" "os" "runtime" @@ -9,7 +11,7 @@ import ( "runtime/pprof" "time" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" ) // memoryCheckPeriod indicates how frequently we'll check for high-memory-usage conditions. @@ -30,6 +32,7 @@ func MemoryMonitor() { } memLimitU64 := uint64(memLimit) //nolint:gosec // G115: memLimit is not negative + var logger blog.Logger var memStats runtime.MemStats ticker := time.NewTicker(memoryCheckPeriod) for { @@ -38,14 +41,20 @@ func MemoryMonitor() { runtime.ReadMemStats(&memStats) if memStats.Sys-memStats.HeapReleased > memLimitU64 { - logger := blog.Get() + if logger == nil && backupLogger.log != nil { + logger = backupLogger.log + } else if logger == nil { + // We're so early in the process that a real logger hasn't been built yet. + // Create one with a sane default config that cannot error during creation. + logger, _ = blog.New(blog.Config{StdoutLevel: 6, SyslogLevel: -1}) + } err := writeProfile(logger, "heap") if err != nil { - logger.Errf("writing heap profile: %s", err) + logger.Error(context.Background(), "Writing heap profile", err) } err = writeProfile(logger, "goroutine") if err != nil { - logger.Errf("writing goroutine profile: %s", err) + logger.Error(context.Background(), "Writing goroutine profile", err) } time.Sleep(profileDumpPeriod) @@ -60,7 +69,7 @@ func writeProfile(logger blog.Logger, typ string) error { return fmt.Errorf("creating profile file: %s", err) } defer profileFile.Close() - logger.Infof("Writing %s profile to %s", typ, profileFile.Name()) + logger.Info(context.Background(), "Writing profile", slog.String("type", typ), slog.String("file", profileFile.Name())) err = pprof.Lookup(typ).WriteTo(profileFile, 0) if err != nil { return fmt.Errorf("writing profile: %s", err) diff --git a/cmd/nonce-service/main.go b/cmd/nonce-service/main.go index cae29d5cb58..d91b720d80d 100644 --- a/cmd/nonce-service/main.go +++ b/cmd/nonce-service/main.go @@ -10,6 +10,7 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/nonce" @@ -30,7 +31,7 @@ type Config struct { // boulder-wfe and nonce-service instances. NonceHMACKey cmd.HMACKeyConfig `validate:"required"` - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } } @@ -97,8 +98,6 @@ func main() { &noncepb.NonceService_ServiceDesc, ns).Build(tlsConfig, scope, clock.New()) cmd.FailOnError(err, "Unable to setup nonce service gRPC server") - logger.Infof("Nonce server listening on %s with prefix %q", c.NonceService.GRPC.Address, noncePrefix) - cmd.FailOnError(start(), "Nonce service gRPC server failed") } diff --git a/cmd/remoteva/main.go b/cmd/remoteva/main.go index 8fd9da293ac..96b3862be63 100644 --- a/cmd/remoteva/main.go +++ b/cmd/remoteva/main.go @@ -10,6 +10,7 @@ import ( "github.com/jmhodges/clock" "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" @@ -62,7 +63,7 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig } diff --git a/cmd/reversed-hostname-checker/main.go b/cmd/reversed-hostname-checker/main.go index e3869ffe288..28690b30178 100644 --- a/cmd/reversed-hostname-checker/main.go +++ b/cmd/reversed-hostname-checker/main.go @@ -12,6 +12,7 @@ import ( "net/netip" "os" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/policy" @@ -39,7 +40,7 @@ func main() { } scanner := bufio.NewScanner(input) - logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7}) + logger := cmd.NewLogger(blog.Config{StdoutLevel: 7}) cmd.LogStartup(logger) pa, err := policy.New(nil, nil, logger) if err != nil { diff --git a/cmd/sfe/main.go b/cmd/sfe/main.go index 6abda25e771..b350d8358d2 100644 --- a/cmd/sfe/main.go +++ b/cmd/sfe/main.go @@ -3,12 +3,14 @@ package notmain import ( "context" "flag" + "log/slog" "net/http" "os" "sync" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/features" @@ -104,7 +106,7 @@ type Config struct { Features features.Config } - Syslog cmd.SyslogConfig + Syslog blog.Config OpenTelemetry cmd.OpenTelemetryConfig // OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests @@ -209,7 +211,7 @@ func main() { overridesImporterWG.Go(func() { importer.Start(ctx) }) - logger.Infof("Overrides importer started with mode=%s interval=%s", mode, c.SFE.OverridesImporter.Interval.Duration) + logger.Info(context.Background(), "Overrides importer started", slog.String("mode", string(mode)), slog.Duration("interval", c.SFE.OverridesImporter.Interval.Duration)) } } @@ -243,11 +245,11 @@ func main() { ) cmd.FailOnError(err, "Unable to create SFE") - logger.Infof("Server running, listening on %s....", c.SFE.ListenAddress) handler := sfei.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...) srv := web.NewServer(c.SFE.ListenAddress, handler, logger) go func() { + logger.Info(context.Background(), "Server listening", slog.String("addr", c.SFE.ListenAddress)) err := srv.ListenAndServe() if err != nil && err != http.ErrServerClosed { cmd.FailOnError(err, "Running HTTP server") diff --git a/cmd/shell.go b/cmd/shell.go index 2b3509f5aaa..0cfa6fff2df 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -9,7 +9,7 @@ import ( "fmt" "io" "log" - "log/syslog" + "log/slog" "net/http" "net/http/pprof" "os" @@ -17,6 +17,7 @@ import ( "runtime" "runtime/debug" "strings" + "sync" "syscall" "time" @@ -26,6 +27,7 @@ import ( "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/client_golang/prometheus/push" "github.com/redis/go-redis/v9" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" @@ -35,9 +37,9 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.30.0" "google.golang.org/grpc/grpclog" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/strictyaml" "github.com/letsencrypt/validator/v10" ) @@ -60,7 +62,7 @@ type mysqlLogger struct { } func (m mysqlLogger) Print(v ...any) { - m.Errf("[mysql] %s", fmt.Sprint(v...)) + m.Error(context.Background(), "mysql error", errors.New(fmt.Sprint(v...))) } // grpcLogger implements the grpclog.LoggerV2 interface. @@ -85,21 +87,21 @@ func (log grpcLogger) Fatalln(args ...any) { // Pass through all Error level logs. func (log grpcLogger) Error(args ...any) { - log.Logger.Errf("%s", fmt.Sprint(args...)) + log.Logger.Error(context.Background(), "grpc error", errors.New(fmt.Sprint(args...))) } func (log grpcLogger) Errorf(format string, args ...any) { - log.Logger.Errf(format, args...) + log.Logger.Error(context.Background(), "grpc error", fmt.Errorf(format, args...)) } func (log grpcLogger) Errorln(args ...any) { - log.Logger.Errf("%s", fmt.Sprintln(args...)) + log.Logger.Error(context.Background(), "grpc error", errors.New(fmt.Sprintln(args...))) } // Pass through most Warnings, but filter out a few noisy ones. func (log grpcLogger) Warning(args ...any) { - log.Logger.Warning(fmt.Sprint(args...)) + log.Logger.Warn(context.Background(), fmt.Sprint(args...)) } func (log grpcLogger) Warningf(format string, args ...any) { - log.Logger.Warningf(format, args...) + log.Logger.Warn(context.Background(), fmt.Sprintf(format, args...)) } func (log grpcLogger) Warningln(args ...any) { msg := fmt.Sprintln(args...) @@ -112,7 +114,7 @@ func (log grpcLogger) Warningln(args ...any) { return } // Since we've already formatted the message, just pass through to .Warning() - log.Logger.Warning(msg) + log.Logger.Warn(context.Background(), msg) } // Don't log any INFO-level gRPC stuff. In practice this is all noise, like @@ -137,7 +139,7 @@ type promLogger struct { } func (log promLogger) Println(args ...any) { - log.Errf("%s", fmt.Sprint(args...)) + log.Error(context.Background(), "Prometheus error", errors.New(fmt.Sprint(args...))) } type redisLogger struct { @@ -145,7 +147,7 @@ type redisLogger struct { } func (rl redisLogger) Printf(ctx context.Context, format string, v ...any) { - rl.Infof(format, v...) + rl.Info(ctx, fmt.Sprintf(format, v...)) } // logWriter implements the io.Writer interface. @@ -155,7 +157,7 @@ type logWriter struct { func (lw logWriter) Write(p []byte) (n int, err error) { // Lines received by logWriter will always have a trailing newline. - lw.Logger.Info(strings.Trim(string(p), "\n")) + lw.Logger.Info(context.Background(), strings.TrimSuffix(string(p), "\n")) return } @@ -165,10 +167,28 @@ type logOutput struct { } func (l logOutput) Output(calldepth int, logline string) error { - l.Logger.Info(logline) + l.Logger.Info(context.Background(), logline) return nil } +// singletonLogger can only be initialized once, then never overwritten. +type singletonLogger struct { + once sync.Once + log blog.Logger +} + +func (s *singletonLogger) init(l blog.Logger) { + s.once.Do(func() { + s.log = l + }) +} + +// backupLogger is used only by AuditPanic, which is deferred before we've had a +// chance to build a real logger. If NewLogger is called, it initializes this +// backup to be the same as the real logger it returns. Otherwise, AuditPanic +// will construct its own logger with a known-good config. +var backupLogger singletonLogger + // StatsAndLogging sets up an AuditLogger, Prometheus Registerer, and // OpenTelemetry tracing. It returns the Registerer and AuditLogger, along // with a graceful shutdown function to be deferred. @@ -181,7 +201,7 @@ func (l logOutput) Output(calldepth int, logline string) error { // is called, because gRPC's SetLogger doesn't use any locking. // // This function does not return an error, and will panic on problems. -func StatsAndLogging(logConf SyslogConfig, otConf OpenTelemetryConfig, addr string) (prometheus.Registerer, blog.Logger, func(context.Context)) { +func StatsAndLogging(logConf blog.Config, otConf OpenTelemetryConfig, addr string) (prometheus.Registerer, blog.Logger, func(context.Context)) { logger := NewLogger(logConf) shutdown := NewOpenTelemetry(otConf, logger) @@ -190,30 +210,15 @@ func StatsAndLogging(logConf SyslogConfig, otConf OpenTelemetryConfig, addr stri } // NewLogger creates a logger object with the provided settings, sets it as -// the global logger, and returns it. +// the backup logger, and returns it. // // It also sets the logging systems for various packages we use to go through // the created logger, and sets up a periodic log event for the current timestamp. -func NewLogger(logConf SyslogConfig) blog.Logger { - var logger blog.Logger - if logConf.SyslogLevel >= 0 { - syslogger, err := syslog.Dial( - "", - "", - syslog.LOG_INFO, // default, not actually used - core.Command()) - FailOnError(err, "Could not connect to Syslog") - syslogLevel := int(syslog.LOG_INFO) - if logConf.SyslogLevel != 0 { - syslogLevel = logConf.SyslogLevel - } - logger, err = blog.New(syslogger, logConf.StdoutLevel, syslogLevel) - FailOnError(err, "Could not connect to Syslog") - } else { - logger = blog.StdoutLogger(logConf.StdoutLevel) - } +func NewLogger(logConf blog.Config) blog.Logger { + logger, err := blog.New(logConf) + FailOnError(err, "While constructing logger") - _ = blog.Set(logger) + backupLogger.init(logger) _ = mysql.SetLogger(mysqlLogger{logger}) grpclog.SetLoggerV2(grpcLogger{logger}) log.SetOutput(logWriter{logger}) @@ -224,7 +229,7 @@ func NewLogger(logConf SyslogConfig) blog.Logger { go func() { for { time.Sleep(time.Hour) - logger.Info(fmt.Sprintf("time=%s", time.Now().Format(time.RFC3339Nano))) + logger.Info(context.Background(), "heartbeat", slog.Time("now", time.Now())) } }() return logger @@ -264,7 +269,7 @@ func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer { registry := prometheus.NewRegistry() if addr == "" { - logger.Info("No debug listen address specified") + logger.Debug(context.Background(), "No debug listen address specified") return registry } @@ -295,7 +300,7 @@ func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer { ErrorLog: promLogger{logger}, })) - logger.Infof("Debug server listening on %s", addr) + logger.Debug(context.Background(), "Debug server listening", slog.String("addr", addr)) server := http.Server{ Addr: addr, @@ -313,7 +318,9 @@ func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer { // It returns a graceful shutdown function to be deferred. func NewOpenTelemetry(config OpenTelemetryConfig, logger blog.Logger) func(ctx context.Context) { otel.SetLogger(stdr.New(logOutput{logger})) - otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { logger.Errf("OpenTelemetry error: %v", err) })) + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { + logger.Error(context.Background(), "OpenTelemetry error", err) + })) resources := resource.NewWithAttributes( semconv.SchemaURL, @@ -348,7 +355,7 @@ func NewOpenTelemetry(config OpenTelemetryConfig, logger blog.Logger) func(ctx c return func(ctx context.Context) { err := tracerProvider.Shutdown(ctx) if err != nil { - logger.Errf("Error while shutting down OpenTelemetry: %v", err) + logger.Error(ctx, "Failed to shut down OpenTelemetry", err) } } } @@ -356,27 +363,28 @@ func NewOpenTelemetry(config OpenTelemetryConfig, logger blog.Logger) func(ctx c // AuditPanic catches and logs panics, then exits with exit code 1. // This method should be called in a defer statement as early as possible. func AuditPanic() { + logger := backupLogger.log + if logger == nil { + // We're so early in the process that a real logger hasn't been built yet. + // Create one with a sane default config that cannot error during creation. + logger, _ = blog.New(blog.Config{StdoutLevel: 6, SyslogLevel: -1}) + } + err := recover() // No panic, no problem if err == nil { - blog.Get().AuditInfo("Process exiting normally", info()) + logger.AuditInfo(context.Background(), "Process exiting normally", info()...) return } - // Get the global logger if it's initialized, or create a default one if not. - // We could wind up creating a default logger if we panic so early in a process' - // lifetime that we haven't yet parsed the config and created a logger. - log := blog.Get() + // For the special type `failure`, audit log the message and exit quietly fail, ok := err.(failure) if ok { - log.AuditErr(fail.msg, nil, nil) + logger.AuditError(context.Background(), "Command failed", errors.New(fail.msg)) } else { // For all other values (which might not be an error) passed to `panic`, log // them and a stack trace - log.AuditErr("Panic", nil, map[string]any{ - "panic": fmt.Sprintf("%#v", err), - "stack": string(debug.Stack()), - }) + logger.AuditError(context.Background(), "Panic", fmt.Errorf("%#v", err), slog.String("stack", string(debug.Stack()))) } // Because this function is deferred as early as possible, there's no further defers to run after this one // So it is safe to os.Exit to set the exit code and exit without losing any defers we haven't executed. @@ -529,27 +537,19 @@ func ValidateYAMLConfig(cv *ConfigValidator, in io.Reader) error { return nil } -type buildInfo struct { - Command string - BuildID string - BuildTime string - GoVersion string - BuildHost string -} - // info produces build information about this binary -func info() buildInfo { - return buildInfo{ - Command: core.Command(), - BuildID: core.GetBuildID(), - BuildTime: core.GetBuildTime(), - GoVersion: runtime.Version(), - BuildHost: core.GetBuildHost(), +func info() []slog.Attr { + return []slog.Attr{ + slog.String("buildHost", core.GetBuildHost()), + slog.String("buildTime", core.GetBuildTime()), + slog.String("buildID", core.GetBuildID()), + slog.String("goVersion", runtime.Version()), + slog.String("command", core.Command()), } } func LogStartup(logger blog.Logger) { - logger.AuditInfo("Process starting", info()) + logger.AuditInfo(context.Background(), "Process starting", info()...) } // CatchSignals blocks until a SIGTERM, SIGINT, or SIGHUP is received, then @@ -575,3 +575,17 @@ func WaitForSignal() { signal.Notify(sigChan, syscall.SIGHUP) <-sigChan } + +// PushMetrics pushes the provided Prometheus metrics to the provided +// Pushgateway URL with the provided job name. +func PushMetrics(jobname, pushgatewayURL string, gatherer prometheus.Gatherer, logger blog.Logger) error { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + return push.New(pushgatewayURL, jobname). + Client(&http.Client{Timeout: 10 * time.Second}). + Gatherer(gatherer). + Grouping("instance", hostname). + Push() +} diff --git a/cmd/shell_test.go b/cmd/shell_test.go index 16cc8c114a3..6f17118c76f 100644 --- a/cmd/shell_test.go +++ b/cmd/shell_test.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "log" "os" "os/exec" "runtime" @@ -13,9 +12,9 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) @@ -70,54 +69,6 @@ func TestPAConfigUnmarshal(t *testing.T) { test.AssertNotError(t, pc4.CheckIdentifiers(), "Disallowed empty identifiers map") } -func TestMysqlLogger(t *testing.T) { - log := blog.UseMock() - mLog := mysqlLogger{log} - - testCases := []struct { - args []any - expected string - }{ - { - []any{nil}, - `ERR: [mysql] `, - }, - { - []any{""}, - `ERR: [mysql] `, - }, - { - []any{"Sup ", 12345, " Sup sup"}, - `ERR: [mysql] Sup 12345 Sup sup`, - }, - } - - for _, tc := range testCases { - // mysqlLogger proxies blog.AuditLogger to provide a Print() method - mLog.Print(tc.args...) - logged := log.GetAll() - // Calling Print should produce the expected output - test.AssertEquals(t, len(logged), 1) - test.AssertEquals(t, logged[0], tc.expected) - log.Clear() - } -} - -func TestCaptureStdlibLog(t *testing.T) { - logger := blog.UseMock() - oldDest := log.Writer() - defer func() { - log.SetOutput(oldDest) - }() - log.SetOutput(logWriter{logger}) - log.Print("thisisatest") - results := logger.GetAllMatching("thisisatest") - if len(results) != 1 { - t.Fatalf("Expected logger to receive 'thisisatest', got: %s", - strings.Join(logger.GetAllMatching(".*"), "\n")) - } -} - func TestLogStartup(t *testing.T) { core.BuildID = "TestBuildID" core.BuildTime = "RightNow!" @@ -126,8 +77,13 @@ func TestLogStartup(t *testing.T) { log := blog.NewMock() LogStartup(log) logged := strings.Join(log.GetAll(), "\n") - expected := fmt.Sprintf(`INFO: [AUDIT] Process starting JSON={"Command":"cmd.test","BuildID":"TestBuildID","BuildTime":"RightNow!","GoVersion":"%s","BuildHost":"Localhost"}`, runtime.Version()) - test.AssertEquals(t, logged, expected) + test.AssertContains(t, logged, `level=INFO`) + test.AssertContains(t, logged, `[AUDIT]`) + test.AssertContains(t, logged, `msg="Process starting"`) + test.AssertContains(t, logged, `buildID=TestBuildID`) + test.AssertContains(t, logged, `buildTime=RightNow!`) + test.AssertContains(t, logged, `buildHost=Localhost`) + test.AssertContains(t, logged, fmt.Sprintf(`goVersion=%s`, runtime.Version())) } func TestReadConfigFile(t *testing.T) { @@ -144,29 +100,6 @@ func TestReadConfigFile(t *testing.T) { test.AssertEquals(t, c.GRPC.Timeout.Duration, 1*time.Second) } -func TestLogWriter(t *testing.T) { - mock := blog.UseMock() - lw := logWriter{mock} - _, _ = lw.Write([]byte("hi\n")) - lines := mock.GetAllMatching(".*") - test.AssertEquals(t, len(lines), 1) - test.AssertEquals(t, lines[0], "INFO: hi") -} - -func TestGRPCLoggerWarningFilter(t *testing.T) { - m := blog.NewMock() - l := grpcLogger{m} - l.Warningln("asdf", "qwer") - lines := m.GetAllMatching(".*") - test.AssertEquals(t, len(lines), 1) - - m = blog.NewMock() - l = grpcLogger{m} - l.Warningln("Server.processUnaryRPC failed to write status: connection error: desc = \"transport is closing\"") - lines = m.GetAllMatching(".*") - test.AssertEquals(t, len(lines), 0) -} - func Test_newVersionCollector(t *testing.T) { // 'buildTime' core.BuildTime = core.Unspecified @@ -282,7 +215,10 @@ func TestFailExit(t *testing.T) { cmd.Env = append(os.Environ(), "TIME_TO_DIE=1") output, err := cmd.CombinedOutput() test.AssertError(t, err, "running a failing program") - test.AssertContains(t, string(output), "[AUDIT] tears in the rain") + test.AssertContains(t, string(output), `[AUDIT]`) + test.AssertContains(t, string(output), `level=ERROR`) + test.AssertContains(t, string(output), `msg="Command failed"`) + test.AssertContains(t, string(output), `error="tears in the rain"`) // "goroutine" usually shows up in stack traces, so we check it // to make sure we didn't print a stack trace. test.AssertNotContains(t, string(output), "goroutine") diff --git a/core/objects.go b/core/objects.go index dee0e27ecda..a39ecf05e5e 100644 --- a/core/objects.go +++ b/core/objects.go @@ -269,7 +269,7 @@ func (ch Challenge) StringID() string { type Authorization struct { // An identifier for this authorization, unique across // authorizations and certificates within this instance. - ID string `json:"-"` + ID int64 `json:"-"` // The identifier for which authorization is being given Identifier identifier.ACMEIdentifier `json:"identifier"` diff --git a/core/proto/core.pb.go b/core/proto/core.pb.go index be9431bbad5..c8acf30f91a 100644 --- a/core/proto/core.pb.go +++ b/core/proto/core.pb.go @@ -614,7 +614,8 @@ func (x *Registration) GetStatus() string { type Authorization struct { state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // TODO(#8722): reserve + IdInt int64 `protobuf:"varint,12,opt,name=idInt,proto3" json:"idInt,omitempty"` // TODO(#8722): rename RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"` Identifier *Identifier `protobuf:"bytes,11,opt,name=identifier,proto3" json:"identifier,omitempty"` Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` @@ -662,6 +663,13 @@ func (x *Authorization) GetId() string { return "" } +func (x *Authorization) GetIdInt() int64 { + if x != nil { + return x.IdInt + } + return 0 +} + func (x *Authorization) GetRegistrationID() int64 { if x != nil { return x.RegistrationID @@ -1010,72 +1018,73 @@ var file_core_proto_rawDesc = string([]byte{ 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x06, 0x10, - 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xc8, 0x02, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, + 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xde, 0x02, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x12, 0x2f, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x18, - 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, - 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, - 0x65, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, - 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x02, - 0x10, 0x03, 0x22, 0x93, 0x04, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, - 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, - 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x64, 0x49, + 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x69, 0x64, 0x49, 0x6e, 0x74, 0x12, + 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, + 0x65, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, + 0x10, 0x09, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x93, 0x04, 0x0a, 0x05, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, + 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, + 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, + 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, + 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x7a, + 0x0a, 0x08, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, - 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0a, - 0x10, 0x0b, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x7a, 0x0a, 0x08, 0x43, 0x52, 0x4c, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, - 0x08, 0x03, 0x10, 0x04, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, - 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/core/proto/core.proto b/core/proto/core.proto index c9def4ec74c..66d576f9bde 100644 --- a/core/proto/core.proto +++ b/core/proto/core.proto @@ -93,9 +93,10 @@ message Registration { } message Authorization { - // Next unused field number: 12 + // Next unused field number: 13 reserved 5, 7, 8; - string id = 1; + string id = 1; // TODO(#8722): reserve + int64 idInt = 12; // TODO(#8722): rename int64 registrationID = 3; // Fields specified by RFC 8555, Section 7.1.4 reserved 2; // Previously dnsName diff --git a/crl/storer/storer.go b/crl/storer/storer.go index 23e8bbbeace..6455d77bdf9 100644 --- a/crl/storer/storer.go +++ b/crl/storer/storer.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "log/slog" "math/big" "slices" "time" @@ -22,11 +23,11 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/crl" "github.com/letsencrypt/boulder/crl/idp" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" ) // simpleS3 matches the subset of the s3.Client interface which we use, to allow @@ -139,6 +140,12 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR return errors.New("got no metadata message") } + ctx := blog.ContextWith(stream.Context(), + slog.String("issuer", issuer.Subject.CommonName), + slog.Int64("shard", shardIdx), + slog.String("number", crlNumber.String()), + ) + crlId := crl.Id(issuer.NameID(), int(shardIdx), crlNumber) crl, err := x509.ParseRevocationList(crlBytes) @@ -160,6 +167,7 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR // additional safety check against clock skew and potential races, if multiple // crl-updaters are working on the same shard at the same time. We only run // these checks if we found a CRL, so we don't block uploading brand new CRLs. + var prevEtag *string filename := fmt.Sprintf("%d/%d.crl", issuer.NameID(), shardIdx) prevObj, err := cs.s3Client.GetObject(stream.Context(), &s3.GetObjectInput{ Bucket: &cs.s3Bucket, @@ -170,7 +178,7 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR if !ok || smithyErr.HTTPStatusCode() != 404 { return fmt.Errorf("getting previous CRL for %s: %w", crlId, err) } - cs.log.Infof("No previous CRL found for %s, proceeding", crlId) + cs.log.Info(ctx, "Proceeding because no previous CRL found") } else { defer prevObj.Body.Close() prevBytes, err := io.ReadAll(prevObj.Body) @@ -207,6 +215,10 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR if !uriMatch { return fmt.Errorf("IDP does not match previous: %v !∩ %v", idpURIs, prevURIs) } + + // This ensures that the CRL object hasn't been replaced since we downloaded + // it above. Prevents races against another storer. + prevEtag = prevObj.ETag } // Finally actually upload the new CRL. @@ -225,6 +237,7 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR Metadata: map[string]string{"crlNumber": crlNumber.String()}, Expires: &expires, CacheControl: &cacheControl, + IfMatch: prevEtag, }) latency := cs.clk.Now().Sub(start) @@ -232,18 +245,18 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR if err != nil { cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "failed").Inc() - cs.log.AuditErr("CRL upload failed", err, map[string]any{"id": crlId}) + cs.log.AuditError(ctx, "CRL upload failed", err) return fmt.Errorf("uploading to S3: %w", err) } cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "success").Inc() - cs.log.AuditInfo("CRL uploaded", map[string]any{ - "id": crlId, - "issuerCN": issuer.Subject.CommonName, - "thisUpdate": crl.ThisUpdate.Format(time.RFC3339), - "nextUpdate": crl.NextUpdate.Format(time.RFC3339), - "numEntries": len(crl.RevokedCertificateEntries), - }) + cs.log.AuditInfo(ctx, "CRL uploaded", + slog.Time("thisUpdate", crl.ThisUpdate), + slog.Time("nextUpdate", crl.NextUpdate), + slog.Int("numEntries", len(crl.RevokedCertificateEntries)), + slog.Int("size", len(crlBytes)), + slog.String("sha256", fmt.Sprintf("%x", checksum)), + ) return stream.SendAndClose(&emptypb.Empty{}) } diff --git a/crl/storer/storer_test.go b/crl/storer/storer_test.go index 22654b9ebcc..6285370b52b 100644 --- a/crl/storer/storer_test.go +++ b/crl/storer/storer_test.go @@ -21,10 +21,10 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/crl/idp" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) diff --git a/crl/updater/batch.go b/crl/updater/batch.go index 03f1d3aec85..82f78561e91 100644 --- a/crl/updater/batch.go +++ b/crl/updater/batch.go @@ -3,8 +3,11 @@ package updater import ( "context" "errors" + "log/slog" + "math/big" "sync" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/crl" "github.com/letsencrypt/boulder/issuance" ) @@ -14,10 +17,11 @@ import ( func (cu *crlUpdater) RunOnce(ctx context.Context) error { var wg sync.WaitGroup atTime := cu.clk.Now() + var crlNumber *big.Int = crl.Number(atTime) type workItem struct { - issuerNameID issuance.NameID - shardIdx int + issuer *issuance.Certificate + shardIdx int } var anyErr bool @@ -34,11 +38,18 @@ func (cu *crlUpdater) RunOnce(ctx context.Context) error { if !ok { return } - err := cu.updateShardWithRetry(ctx, atTime, work.issuerNameID, work.shardIdx) + + // Attach log attributes for use here and inside updateShardWithRetry. + ctx := blog.ContextWith(ctx, + slog.String("issuer", work.issuer.Subject.CommonName), + slog.Int("issuerNameID", int(work.issuer.NameID())), + slog.Int("shard", work.shardIdx), + slog.String("number", crlNumber.String()), + ) + + err := cu.updateShardWithRetry(ctx, atTime, work.issuer.NameID(), work.shardIdx) if err != nil { - cu.log.AuditErr("Generating CRL failed", err, map[string]any{ - "id": crl.Id(work.issuerNameID, work.shardIdx, crl.Number(atTime)), - }) + cu.log.AuditError(ctx, "Generating CRL failed", err) once.Do(func() { anyErr = true }) } } @@ -59,7 +70,7 @@ func (cu *crlUpdater) RunOnce(ctx context.Context) error { close(inputs) wg.Wait() return ctx.Err() - case inputs <- workItem{issuerNameID: issuer.NameID(), shardIdx: i + 1}: + case inputs <- workItem{issuer: issuer, shardIdx: i + 1}: } } } diff --git a/crl/updater/batch_test.go b/crl/updater/batch_test.go index e49d9863a2e..02f1508df6f 100644 --- a/crl/updater/batch_test.go +++ b/crl/updater/batch_test.go @@ -8,8 +8,8 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) diff --git a/crl/updater/continuous.go b/crl/updater/continuous.go index 6415ad6c9a8..3ae8382f559 100644 --- a/crl/updater/continuous.go +++ b/crl/updater/continuous.go @@ -2,10 +2,13 @@ package updater import ( "context" + "log/slog" + "math/big" "math/rand/v2" "sync" "time" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/crl" "github.com/letsencrypt/boulder/issuance" ) @@ -16,7 +19,7 @@ import ( func (cu *crlUpdater) Run(ctx context.Context) error { var wg sync.WaitGroup - shardWorker := func(issuerNameID issuance.NameID, shardIdx int) { + shardWorker := func(ctx context.Context, issuer *issuance.Certificate, shardIdx int) { defer wg.Done() // Wait for a random number of nanoseconds less than the updatePeriod, so @@ -41,13 +44,21 @@ func (cu *crlUpdater) Run(ctx context.Context) error { } atTime := cu.clk.Now() - err := cu.updateShardWithRetry(ctx, atTime, issuerNameID, shardIdx) + var crlNumber *big.Int = crl.Number(atTime) + + // Attach log attributes for use here and inside updateShardWithRetry. + ctx := blog.ContextWith(ctx, + slog.String("issuer", issuer.Subject.CommonName), + slog.Int("issuerNameID", int(issuer.NameID())), + slog.Int("shard", shardIdx), + slog.String("number", crlNumber.String()), + ) + + err := cu.updateShardWithRetry(ctx, atTime, issuer.NameID(), shardIdx) if err != nil { // We only log, rather than return, so that the long-lived process can // continue and try again at the next tick. - cu.log.AuditErr("Generating CRL failed", err, map[string]any{ - "id": crl.Id(issuerNameID, shardIdx, crl.Number(atTime)), - }) + cu.log.AuditError(ctx, "Generating CRL failed", err) } select { @@ -63,7 +74,7 @@ func (cu *crlUpdater) Run(ctx context.Context) error { for _, issuer := range cu.issuers { for i := 1; i <= cu.numShards; i++ { wg.Add(1) - go shardWorker(issuer.NameID(), i) + go shardWorker(ctx, issuer, i) } } diff --git a/crl/updater/updater.go b/crl/updater/updater.go index ede6c19cb40..df2e445f1d9 100644 --- a/crl/updater/updater.go +++ b/crl/updater/updater.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "fmt" "io" + "log/slog" "strconv" "time" @@ -13,13 +14,12 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core/proto" - "github.com/letsencrypt/boulder/crl" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -157,16 +157,13 @@ func (cu *crlUpdater) updateShardWithRetry(ctx context.Context, atTime time.Time return fmt.Errorf("leasing shard: %w", err) } - crlID := crl.Id(issuerNameID, shardIdx, crl.Number(atTime)) - for i := range cu.maxAttempts { // core.RetryBackoff always returns 0 when its first argument is zero. sleepTime := core.RetryBackoff(i, time.Second, time.Minute, 2) if i != 0 { - cu.log.AuditErr("Generating CRL failed", err, map[string]any{ - "id": crlID, - "retryAfter": int(sleepTime.Seconds()), - }) + cu.log.AuditError(ctx, "Generating CRL failed", err, + slog.Duration("retryAfter", sleepTime), + ) } cu.clk.Sleep(sleepTime) @@ -203,8 +200,6 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN ctx, cancel := context.WithCancel(ctx) defer cancel() - crlID := crl.Id(issuerNameID, shardIdx, crl.Number(atTime)) - start := cu.clk.Now() defer func() { // This func closes over the named return value `err`, so can reference it. @@ -216,7 +211,7 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN cu.updatedCounter.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, result).Inc() }() - cu.log.Infof("Generating CRL shard: id=[%s]", crlID) + cu.log.Info(ctx, "Generating CRL shard") // Query for unexpired certificates, with padding to ensure that revoked certificates show // up in at least one CRL, even if they expire between revocation and CRL generation. @@ -244,7 +239,9 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN crlEntries = append(crlEntries, entry) } - cu.log.Infof("Queried SA for CRL shard: id=[%s] shardIdx=[%d] numEntries=[%d]", crlID, shardIdx, len(crlEntries)) + cu.log.Info(ctx, "Queried SA for CRL shard", + slog.Int("numEntries", len(crlEntries)), + ) // Send the full list of CRL Entries to the CA. caStream, err := cu.ca.GenerateCRL(ctx) @@ -336,9 +333,10 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN return fmt.Errorf("closing CRLStorer upload stream: %w", err) } - cu.log.Infof( - "Generated CRL shard: id=[%s] size=[%d] hash=[%x]", - crlID, crlLen, crlHash.Sum(nil)) + cu.log.Info(ctx, "Generated CRL shard", + slog.Int("size", crlLen), + slog.String("sha256", fmt.Sprintf("%x", crlHash.Sum(nil))), + ) cu.sizeBytesGauge.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, strconv.Itoa(shardIdx)).Set(float64(crlLen)) cu.sizeEntriesGauge.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, strconv.Itoa(shardIdx)).Set(float64(len(crlEntries))) diff --git a/crl/updater/updater_test.go b/crl/updater/updater_test.go index 86c73c2ca94..cf7400743ac 100644 --- a/crl/updater/updater_test.go +++ b/crl/updater/updater_test.go @@ -16,11 +16,11 @@ import ( "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" corepb "github.com/letsencrypt/boulder/core/proto" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" diff --git a/ctpolicy/ctpolicy.go b/ctpolicy/ctpolicy.go index f818ab62b4d..88a2ea65b9a 100644 --- a/ctpolicy/ctpolicy.go +++ b/ctpolicy/ctpolicy.go @@ -4,16 +4,17 @@ import ( "context" "encoding/base64" "fmt" + "log/slog" "strings" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/ctpolicy/loglist" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" pubpb "github.com/letsencrypt/boulder/publisher/proto" ) @@ -247,7 +248,10 @@ func (ctp *CTPolicy) submitAllBestEffort(ctx context.Context, blob core.CertDER, }, ) if err != nil { - ctp.log.Warningf("ct submission of cert to log %q failed: %s", log.Url, err) + ctp.log.Warn(ctx, "Submission of cert to CT log failed", + slog.String("log", log.Url), + blog.Error(err), + ) } }(log) } diff --git a/ctpolicy/ctpolicy_test.go b/ctpolicy/ctpolicy_test.go index b2a2dd8c316..f54e19925b0 100644 --- a/ctpolicy/ctpolicy_test.go +++ b/ctpolicy/ctpolicy_test.go @@ -13,10 +13,10 @@ import ( "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/ctpolicy/loglist" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" pubpb "github.com/letsencrypt/boulder/publisher/proto" "github.com/letsencrypt/boulder/test" @@ -98,7 +98,6 @@ func TestGetSCTs(t *testing.T) { t.Run(tc.name, func(t *testing.T) { synctest.Test(t, func(t *testing.T) { mockLog := blog.NewMock() - defer mockLog.Close() ctp := New(tc.mock, tc.logs, nil, nil, time.Second, mockLog, metrics.NoopRegisterer) ret, err := ctp.GetSCTs(tc.ctx, []byte{0}, time.Time{}) if tc.result != nil { @@ -131,7 +130,6 @@ func TestGetSCTsNoStaggerOnError(t *testing.T) { // We shouldn't wait for the stagger (1h, here) because all the submissions should fail synctest.Test(t, func(t *testing.T) { mockLog := blog.NewMock() - defer mockLog.Close() ctp := New(&mockFailPub{}, loglist.List{ {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, {Name: "LogA2", Operator: "OperA", Url: "UrlA2", Key: []byte("KeyA2")}, diff --git a/docker-compose.yml b/docker-compose.yml index 9d7816d4983..87a1b37ccba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,7 +142,7 @@ services: command: "consul agent -dev -config-format=hcl -config-file=/test/consul/config.hcl" bjaeger: - image: jaegertracing/all-in-one:1.50 + image: jaegertracing/all-in-one:1.76.0 networks: - bouldernet @@ -199,8 +199,8 @@ services: environment: # By specifying KEYSPACES vttestserver will create the corresponding # databases on startup. - KEYSPACES: boulder_sa,incidents_sa,boulder_sa_next,incidents_sa_next - NUM_SHARDS: 1,1,1,1 + KEYSPACES: boulder_sa,incidents_sa,boulder_sa_next,incidents_sa_next,mtcmeta_44947_4_1_0_44 + NUM_SHARDS: 1,1,1,1,1 healthcheck: # Make sure the service is up and the tables are created. Use `serials` because it happens # to be last in the SQL initialization files, so if it exists the other tables do too. @@ -212,7 +212,7 @@ services: "-e", "SELECT 1 FROM serials"] interval: 2s timeout: 30s - retries: 3 + retries: 10 start_period: 10s start_interval: 2s networks: diff --git a/docs/logging.md b/docs/logging.md index 9fc6405d0de..8be6728ee4b 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -1,11 +1,62 @@ # Logging -Boulder can log to stdout/stderr, syslog, or both. Boulder components -generally have a `syslog` portion of their JSON config that indicates the -maximum level of log that should be sent to a given destination. For instance, -in `test/config/wfe2.json`: +## Input -``` +We use the `blog` package, maintained here in this repo, as the mediator for +all log output from Boulder. See that package for documentation of its API. + +We have several best practices for how we use that package, above and beyond +what its API enforces. Expect these to evolve over time as we improve and +learn: + +1. Only use static strings as log messages. Never use `fmt.Sprintf` to create + the string that becomes the log message. Anything you would format into that + message, instead attach to the log line as an attribute. + +2. By default, avoid deferred logging statements. We do use these to great + effect in certain places where there are many possible error returns and we + MUST log no matter what, but it's not a pattern we want to perpetuate. It + generally leads to difficult-to-enforce constraints around variable scoping + and redeclaration. + +3. Only attach attributes to the context if a) you truly want *every* subsequent + log line to contain them, and b) there are multiple possible subsequent log + lines which would otherwise duplicate those attributes. Keep in mind that we + do not, and do not intend to ever, transmit log attributes across the gRPC + boundary. + +4. In service of the above, err on the side of only attaching `blog.Attrs` + (i.e., slog.Attrs which are so widely-used that we gave them helper functions + in the blog package) to the context. When you do attach such attributes to + the context, do so at the earliest possible moment, such as immediately after + a `IsAnyNilOrZero` check. This is to reduce spooky action at a distance: at + any given logging site, it can be difficult to tell what log attributes have + already been attached. If we stick to a convention of only attaching things + like accounts, orders, authzs, and serials to contexts, we can act with more + confidence at logging sites. + +## Output + +The blog package can send output to stdout, syslog, or both (see below for how to configure this). Regardless of output location, the output is identical. + +All log lines are prefixed by a checksum. This is not a cryptographically secure +hash, but is intended to let us catch corruption in the log system. This is a +short chunk of base64 encoded data near the beginning of the log line. It is +consumed by cmd/log-validator. + +All lines logged via `blog.AuditInfo` or `blog.AuditError` are prefixed by the constant string `[AUDIT]`. This is used by internal infrastructure to ensure that audit-level logs have extra persistence and durability. + +All log lines have attributes for the datacenter, host, program, and pid. These are replicating information usually prepended to log lines by syslog. In the integration test environment, these attributes are suppressed for readability. + +In production, all lines are logged in JSON format. This is to optimize for machine-readability in our log analysis systems. In the unit and integration tests, all lines are logged in text format, to optimize for human readability. + +## Configuration + +Boulder components generally have a `syslog` portion of their JSON config that +indicates the maximum level of log that should be sent to a given destination. +For instance, in `test/config/wfe2.json`: + +```json "syslog": { "stdoutlevel": 4, "sysloglevel": 6 @@ -13,41 +64,32 @@ in `test/config/wfe2.json`: ``` This indicates that logs of level 4 or below (error and warning) should be -emitted to stdout/stderr, and logs of level 6 or below (error, warning, notice, and +emitted to stdout, and logs of level 6 or below (error, warning, and info) should be emitted to syslog, using the local Unix socket method. The highest meaningful value is 7, which enables debug logging. -The stdout/stderr logger uses ANSI escape codes to color warnings as yellow -and errors as red, if stdout is detected to be a terminal. - The default value for these fields is 6 (INFO) for syslogLevel and 0 (no logs) for stdoutLevel. To turn off syslog logging entirely, set syslogLevel to -1. -In Boulder's development environment, we enable stdout logging because that +In the integration test environment, we enable stdout logging because that makes it easier to see what's going on quickly. In production, we disable stdout logging because it would duplicate the syslog logging. We preferred the syslog logging because it provides things like severity level in a consistent way with -other components. But we may move to stdout/stderr logging to make it easier to -containerize Boulder. +other components, but intend to switch to stdout-only in the future. + +## Other Notes Boulder has a number of adapters to take other packages' log APIs and send them -to syslog as expected. For instance, we provide a custom logger for mysql, grpc, -and prometheus that forwards to syslog. This is configured in StatsAndLogging in +to the configured logger as expected. For instance, we provide custom loggers +for mysql, grpc, and prometheus. These are initialized in StatsAndLogging in cmd/shell.go. There are some cases where we output to stdout regardless of the JSON config settings: - - Panics are always emitted to stdout - - Packages that Boulder relies on may occasionally emit to stdout (though this - is generally not ideal and we try to get it changed). +- Panics are always emitted to stdout. +- Packages that Boulder relies on may occasionally emit to stdout (though this + is generally not ideal and we try to get it changed). Typically these output lines will be collected by systemd and forwarded to syslog. - -## Verification - -We attach a simple checksum to each log line. This is not a cryptographically -secure hash, but is intended to let us catch corruption in the log system. This -is a short chunk of base64 encoded data near the beginning of the log line. It -is consumed by cmd/log-validator. diff --git a/go.mod b/go.mod index 8a286efbc68..232a52ee5f6 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/letsencrypt/boulder go 1.26.0 require ( - github.com/aws/aws-sdk-go-v2 v1.41.6 - github.com/aws/aws-sdk-go-v2/config v1.32.16 - github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 - github.com/aws/smithy-go v1.25.0 + github.com/aws/aws-sdk-go-v2 v1.41.7 + github.com/aws/aws-sdk-go-v2/config v1.32.17 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 + github.com/aws/smithy-go v1.25.1 github.com/eggsampler/acme/v3 v3.8.1 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-logr/stdr v1.2.2 @@ -25,11 +25,11 @@ require ( github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.20.1 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 - github.com/weppos/publicsuffix-go v0.50.3 - github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 - github.com/zmap/zlint/v3 v3.6.6 + github.com/weppos/publicsuffix-go v0.50.4-0.20260507075217-1bd47f85b3da + github.com/zmap/zcrypto v0.0.0-20260514033604-a1159eb3cad9 + github.com/zmap/zlint/v3 v3.7.2-0.20260531191521-b88ecfaefc52 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/otel v1.43.0 @@ -40,33 +40,31 @@ require ( golang.org/x/crypto v0.52.0 golang.org/x/net v0.55.0 golang.org/x/sync v0.20.0 - golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 - golang.org/x/time v0.11.0 + golang.org/x/time v0.15.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 ) require ( filippo.io/edwards25519 v1.1.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -85,6 +83,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/go.sum b/go.sum index c8b9c610102..17410dc5145 100644 --- a/go.sum +++ b/go.sum @@ -7,42 +7,42 @@ github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= -github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg= -github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= -github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22/go.mod h1:ES3ynECd7fYeJIL6+oax+uIEljmfps0S70BaQzbMd/o= -github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 h1:kU/eBN5+MWNo/LcbNa4hWDdN76hdcd7hocU5kvu7IsU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1/go.mod h1:Fw9aqhJicIVee1VytBBjH+l+5ov6/PhbtIK/u3rt/ls= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= -github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= -github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -63,12 +63,9 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/eggsampler/acme/v3 v3.8.1 h1:HmpFs/CIdEXg2NCwSEFBd1BgSzzN8fPzwNZGzp0izrw= github.com/eggsampler/acme/v3 v3.8.1/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= @@ -113,7 +110,6 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/certificate-transparency-go v1.3.2-0.20250507091337-0eddb39e94f8 h1:1RSWsOSxq2gk4pD/63bhsPwoOXgz2yXVadxXPbwZ0ec= github.com/google/certificate-transparency-go v1.3.2-0.20250507091337-0eddb39e94f8/go.mod h1:6Rm5w0Mlv87LyBNOCgfKYjdIBBpF42XpXGsbQvQGomQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -138,6 +134,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -172,8 +170,6 @@ github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= -github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -181,7 +177,6 @@ github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -212,8 +207,8 @@ github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 h1:1/BDligzCa40GTllkDnY3Y5DTH github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3/go.mod h1:3dZmcLn3Qw6FLlWASn1g4y+YO9ycEFUOM+bhBmzLVKQ= github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 h1:kuvuJL/+MZIEdvtb/kTBRiRgYaOmx1l+lYJyVdrRUOs= github.com/redis/go-redis/extra/redisotel/v9 v9.5.3/go.mod h1:7f/FMrf5RRRVHXgfk7CzSVzXHiWeuOQUu2bsVqWoa+g= -github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= -github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w= +github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -222,9 +217,6 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -233,43 +225,25 @@ github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/weppos/publicsuffix-go v0.40.3-0.20250127173806-e489a31678ca/go.mod h1:43Dfyxu2dpmLg56at26Q4k9gwf3yWSUiwk8kGnwzULk= -github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE= -github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4= +github.com/weppos/publicsuffix-go v0.50.4-0.20260507075217-1bd47f85b3da h1:fvG9axse5NWVonGRSWkjrIBP5v6jXxJE2yZxcPILBX4= +github.com/weppos/publicsuffix-go v0.50.4-0.20260507075217-1bd47f85b3da/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= -github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= -github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= -github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 h1:Qp98bmMm9JHPPOaLi2Nb6oWoZ+1OyOMWI7PPeJrirI0= -github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98/go.mod h1:YTUyN/U1oJ7RzCEY5hUweYxbVUu7X+11wB7OXZT15oE= -github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= -github.com/zmap/zlint/v3 v3.6.6 h1:tH7RJM9bDmh7IonlLEkFIkIn8XDYDYjehhUPgpLVqYA= -github.com/zmap/zlint/v3 v3.6.6/go.mod h1:6yXG+CBOQBRpMCOnpIVPUUL296m5HYksZC9bj5LZkwE= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zmap/zcrypto v0.0.0-20260514033604-a1159eb3cad9 h1:k0GSZUpunq/xV9TchM0Y8WGQn2wzpeF0uTBMNN5up+o= +github.com/zmap/zcrypto v0.0.0-20260514033604-a1159eb3cad9/go.mod h1:fFnCx1pWvIjLONMF4Qkmp3LlwomUZ4VG4aUDYhMDstU= +github.com/zmap/zlint/v3 v3.7.2-0.20260531191521-b88ecfaefc52 h1:9y0S/ltWhWdGtChZd6NWjH4c7eAEmkVixtP5SuLpSJg= +github.com/zmap/zlint/v3 v3.7.2-0.20260531191521-b88ecfaefc52/go.mod h1:wg6hm/hqXUXHI0Eksegftj/a1bH2Zl6wpgcB4yeUy/o= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= @@ -294,6 +268,8 @@ go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLh 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.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -303,25 +279,11 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -332,17 +294,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -351,12 +302,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -367,61 +312,20 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.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= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -452,7 +356,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/grpc/interceptors.go b/grpc/interceptors.go index fdedc72bbb4..09b05e6d7d4 100644 --- a/grpc/interceptors.go +++ b/grpc/interceptors.go @@ -3,6 +3,7 @@ package grpc import ( "context" "fmt" + "log/slog" "strconv" "strings" "time" @@ -16,6 +17,7 @@ import ( "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/web" @@ -124,12 +126,20 @@ func (smi *serverMetadataInterceptor) Unary( return nil, status.Errorf(codes.DeadlineExceeded, "not enough time left on clock: %s", remaining) } - localCtx, cancel := context.WithDeadline(ctx, deadline) + ctx, cancel := context.WithDeadline(ctx, deadline) defer cancel() - resp, err := handler(localCtx, req) + // Attach the gRPC service and method to the context, so it will be included + // in all log lines output during handling. + service, method := splitMethodName(info.FullMethod) + ctx = blog.ContextWith(ctx, slog.Group("grpc", + slog.String("service", service), + slog.String("method", method), + )) + + resp, err := handler(ctx, req) if err != nil { - err = wrapError(localCtx, err) + err = wrapError(ctx, err) } return resp, err } @@ -189,12 +199,20 @@ func (smi *serverMetadataInterceptor) Stream( // Server stream interceptors are synchronous (they return their error, if // any, when the stream is done) so defer cancel() is safe here. - localCtx, cancel := context.WithDeadline(ctx, deadline) + ctx, cancel := context.WithDeadline(ctx, deadline) defer cancel() - err := handler(srv, interceptedServerStream{ss, localCtx}) + // Attach the gRPC service and method to the context, so it will be included + // in all log lines output during handling. + service, method := splitMethodName(info.FullMethod) + ctx = blog.ContextWith(ctx, slog.Group("grpc", + slog.String("service", service), + slog.String("method", method), + )) + + err := handler(srv, interceptedServerStream{ss, ctx}) if err != nil { - err = wrapError(localCtx, err) + err = wrapError(ctx, err) } return err } diff --git a/grpc/pb-marshalling.go b/grpc/pb-marshalling.go index 5bfb699d584..18e5e4aedb4 100644 --- a/grpc/pb-marshalling.go +++ b/grpc/pb-marshalling.go @@ -8,6 +8,7 @@ package grpc import ( "fmt" "net/netip" + "strconv" "time" "github.com/go-jose/go-jose/v4" @@ -291,7 +292,8 @@ func AuthzToPB(authz core.Authorization) (*corepb.Authorization, error) { } return &corepb.Authorization{ - Id: authz.ID, + Id: fmt.Sprintf("%d", authz.ID), + IdInt: authz.ID, Identifier: authz.Identifier.ToProto(), RegistrationID: authz.RegistrationID, Status: string(authz.Status), @@ -315,8 +317,22 @@ func PBToAuthz(pb *corepb.Authorization) (core.Authorization, error) { c := pb.Expires.AsTime() expires = &c } + + // TODO(#8722): remove this series of checks when pb.Id is int64-only + var authzIDInt int64 + if pb.IdInt != 0 { + authzIDInt = pb.IdInt + } else if pb.Id != "" { + parsed, err := strconv.ParseInt(pb.Id, 10, 64) + if err != nil { + return core.Authorization{}, ErrInvalidParameters + } + authzIDInt = parsed + } else { + return core.Authorization{}, ErrMissingParameters + } authz := core.Authorization{ - ID: pb.Id, + ID: authzIDInt, Identifier: identifier.FromProto(pb.Identifier), RegistrationID: pb.RegistrationID, Status: core.AcmeStatus(pb.Status), diff --git a/grpc/pb-marshalling_test.go b/grpc/pb-marshalling_test.go index 167afb7c9f6..196418f077b 100644 --- a/grpc/pb-marshalling_test.go +++ b/grpc/pb-marshalling_test.go @@ -216,7 +216,7 @@ func TestAuthz(t *testing.T) { Token: "asd2", } inAuthz := core.Authorization{ - ID: "1", + ID: 1, Identifier: ident, RegistrationID: 5, Status: core.StatusPending, @@ -230,7 +230,7 @@ func TestAuthz(t *testing.T) { test.AssertDeepEquals(t, inAuthz, outAuthz) inAuthzNilExpires := core.Authorization{ - ID: "1", + ID: 1, Identifier: ident, RegistrationID: 5, Status: core.StatusPending, @@ -242,6 +242,33 @@ func TestAuthz(t *testing.T) { outAuthz2, err := PBToAuthz(pbAuthz2) test.AssertNotError(t, err, "PBToAuthz failed") test.AssertDeepEquals(t, inAuthzNilExpires, outAuthz2) + + // Manipulate pbAuthz to test Authz marshalling with various ID combinations + // TODO(#8722): clean up these tests when authz IDs are int-only + pbAuthz3 := pbAuthz + + pbAuthz3.Id = "" + pbAuthz3.IdInt = 0 + _, err = PBToAuthz(pbAuthz3) + test.AssertError(t, err, "PBToAuthz with empty ID and empty IDInt unexpectedly succeeded") + + pbAuthz3.Id = "1" + pbAuthz3.IdInt = 0 + outAuthz3, err := PBToAuthz(pbAuthz3) + test.AssertNotError(t, err, "PBToAuthz with only string ID failed") + test.AssertDeepEquals(t, inAuthz, outAuthz3) + + pbAuthz3.Id = "1" + pbAuthz3.IdInt = 1 + outAuthz3, err = PBToAuthz(pbAuthz3) + test.AssertNotError(t, err, "PBToAuthz with both string ID and int IDInt failed") + test.AssertDeepEquals(t, inAuthz, outAuthz3) + + pbAuthz3.Id = "" + pbAuthz3.IdInt = 1 + outAuthz3, err = PBToAuthz(pbAuthz3) + test.AssertNotError(t, err, "PBToAuthz with only int IDInt failed") + test.AssertDeepEquals(t, inAuthz, outAuthz3) } func TestOrderValid(t *testing.T) { diff --git a/grpc/server.go b/grpc/server.go index eaf83a9694e..5d19a9ca0d3 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "log/slog" "net" "slices" "strings" @@ -21,9 +22,9 @@ import ( "google.golang.org/grpc/keepalive" "google.golang.org/grpc/status" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" bcreds "github.com/letsencrypt/boulder/grpc/creds" - blog "github.com/letsencrypt/boulder/log" ) // CodedError is a alias required to appease go vet @@ -200,7 +201,7 @@ func (sb *serverBuilder) Build(tlsConfig *tls.Config, statsRegistry prometheus.R if sb.cfg.Address == "" { return nil, errors.New("GRPC listen address not configured") } - sb.logger.Infof("grpc listening on %s", sb.cfg.Address) + sb.logger.Info(context.Background(), "grpc server listening", slog.String("addr", sb.cfg.Address)) // Finally return the functions which will start and stop the server. listener, err := net.Listen("tcp", sb.cfg.Address) @@ -263,7 +264,7 @@ func (sb *serverBuilder) initLongRunningCheck(shutdownCtx context.Context, servi var next healthpb.HealthCheckResponse_ServingStatus err := checkImpl(checkImplCtx) if err != nil { - sb.logger.Infof("health check of gRPC service %q failed: %s", service, err) + sb.logger.Info(shutdownCtx, "grpc health check failed", slog.String("service", service), blog.Error(err)) next = healthpb.HealthCheckResponse_NOT_SERVING } else { next = healthpb.HealthCheckResponse_SERVING @@ -274,12 +275,13 @@ func (sb *serverBuilder) initLongRunningCheck(shutdownCtx context.Context, servi return next } + ctx := blog.ContextWith(shutdownCtx, slog.String("old", last.String()), slog.String("new", next.String())) if next != healthpb.HealthCheckResponse_SERVING { - sb.logger.Warningf("transitioning overall health from %q to %q, due to: %s", last, next, err) - sb.logger.Warningf("transitioning health of %q from %q to %q, due to: %s", service, last, next, err) + sb.logger.Warn(ctx, "transitioning overall health", blog.Error(err)) + sb.logger.Warn(ctx, "transitioning service health", slog.String("service", service), blog.Error(err)) } else { - sb.logger.Infof("transitioning overall health from %q to %q", last, next) - sb.logger.Infof("transitioning health of %q from %q to %q", service, last, next) + sb.logger.Info(ctx, "transitioning overall health") + sb.logger.Info(ctx, "transitioning service health", slog.String("service", service)) } sb.healthSrv.SetServingStatus("", next) sb.healthSrv.SetServingStatus(service, next) diff --git a/grpc/server_test.go b/grpc/server_test.go index 16c2e86a4ec..b133033e9e5 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -6,9 +6,10 @@ import ( "testing" "time" - blog "github.com/letsencrypt/boulder/log" - "github.com/letsencrypt/boulder/test" "google.golang.org/grpc/health" + + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/test" ) func TestServerBuilderInitLongRunningCheck(t *testing.T) { @@ -39,8 +40,8 @@ func TestServerBuilderInitLongRunningCheck(t *testing.T) { // - ~0ms 1st check passed, NOT_SERVING to SERVING // - ~50ms 2nd check passed, [no transition] // - ~100ms 3rd check failed, SERVING to NOT_SERVING - serving := mockLogger.GetAllMatching(".*\"NOT_SERVING\" to \"SERVING\"") - notServing := mockLogger.GetAllMatching((".*\"SERVING\" to \"NOT_SERVING\"")) + serving := mockLogger.GetAllMatching(`old=NOT_SERVING new=SERVING`) + notServing := mockLogger.GetAllMatching(`old=SERVING new=NOT_SERVING`) test.Assert(t, len(serving) == 2, "expected two serving log lines") test.Assert(t, len(notServing) == 2, "expected two not serving log lines") @@ -65,8 +66,8 @@ func TestServerBuilderInitLongRunningCheck(t *testing.T) { // - ~0ms 1st check passed, NOT_SERVING to SERVING // - ~50ms 2nd check failed, SERVING to NOT_SERVING // - ~100ms 3rd check passed, NOT_SERVING to SERVING - serving = mockLogger.GetAllMatching(".*\"NOT_SERVING\" to \"SERVING\"") - notServing = mockLogger.GetAllMatching((".*\"SERVING\" to \"NOT_SERVING\"")) + serving = mockLogger.GetAllMatching(`old=NOT_SERVING new=SERVING`) + notServing = mockLogger.GetAllMatching(`old=SERVING new=NOT_SERVING`) test.Assert(t, len(serving) == 4, "expected four serving log lines") test.Assert(t, len(notServing) == 2, "expected two not serving log lines") } diff --git a/issuance/issuer.go b/issuance/issuer.go index f1bbffb8664..d48b601ed4a 100644 --- a/issuance/issuer.go +++ b/issuance/issuer.go @@ -2,9 +2,6 @@ package issuance import ( "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" "crypto/x509" "encoding/json" "errors" @@ -209,24 +206,9 @@ type Issuer struct { // newIssuer constructs a new Issuer from the in-memory certificate and signer. // It exists as a helper for LoadIssuer to make testing simpler. func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk clock.Clock) (*Issuer, error) { - var keyAlg x509.PublicKeyAlgorithm - var sigAlg x509.SignatureAlgorithm - switch k := cert.PublicKey.(type) { - case *rsa.PublicKey: - keyAlg = x509.RSA - sigAlg = x509.SHA256WithRSA - case *ecdsa.PublicKey: - keyAlg = x509.ECDSA - switch k.Curve { - case elliptic.P256(): - sigAlg = x509.ECDSAWithSHA256 - case elliptic.P384(): - sigAlg = x509.ECDSAWithSHA384 - default: - return nil, fmt.Errorf("unsupported ECDSA curve: %q", k.Curve.Params().Name) - } - default: - return nil, errors.New("unsupported issuer key type") + keyAlg, sigAlg, err := pubkeyParams(cert.PublicKey) + if err != nil { + return nil, err } if config.IssuerURL == "" { @@ -274,8 +256,8 @@ func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk return i, nil } -// KeyType returns either x509.RSA or x509.ECDSA, depending on whether the -// issuer has an RSA or ECDSA keypair. This is useful for determining which +// KeyType returns x509.RSA, x509.ECDSA, or x509.MLDSA depending on the +// keypair of the issuer. This is useful for determining which // issuance requests should be routed to this issuer. func (i *Issuer) KeyType() x509.PublicKeyAlgorithm { return i.keyAlg diff --git a/issuance/pubkeyparams.go b/issuance/pubkeyparams.go new file mode 100644 index 00000000000..89127e93899 --- /dev/null +++ b/issuance/pubkeyparams.go @@ -0,0 +1,32 @@ +//go:build !go1.27 + +package issuance + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" +) + +func pubkeyParams(pubkey any) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + switch k := pubkey.(type) { + case *rsa.PublicKey: + return x509.RSA, x509.SHA256WithRSA, nil + case *ecdsa.PublicKey: + switch k.Curve { + case elliptic.P256(): + return x509.ECDSA, x509.ECDSAWithSHA256, nil + case elliptic.P384(): + return x509.ECDSA, x509.ECDSAWithSHA384, nil + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, + fmt.Errorf("unsupported ECDSA curve: %q", k.Curve.Params().Name) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, + errors.New("unsupported issuer key type") + } +} diff --git a/issuance/pubkeyparams_go127.go b/issuance/pubkeyparams_go127.go new file mode 100644 index 00000000000..954bd44ba94 --- /dev/null +++ b/issuance/pubkeyparams_go127.go @@ -0,0 +1,37 @@ +//go:build go1.27 + +package issuance + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/mldsa" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" +) + +// pubkeyParams returns a PublicKeyAlgorithm and SignatureAlgorithm for the input pubkey. +// TODO(#8812): Move this back to issuer.go. +func pubkeyParams(pubkey any) (x509.PublicKeyAlgorithm, x509.SignatureAlgorithm, error) { + switch k := pubkey.(type) { + case *mldsa.PublicKey: + return x509.MLDSA, x509.MLDSA44, nil + case *rsa.PublicKey: + return x509.RSA, x509.SHA256WithRSA, nil + case *ecdsa.PublicKey: + switch k.Curve { + case elliptic.P256(): + return x509.ECDSA, x509.ECDSAWithSHA256, nil + case elliptic.P384(): + return x509.ECDSA, x509.ECDSAWithSHA384, nil + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, + fmt.Errorf("unsupported ECDSA curve: %q", k.Curve.Params().Name) + } + default: + return x509.UnknownPublicKeyAlgorithm, x509.UnknownSignatureAlgorithm, + errors.New("unsupported issuer key type") + } +} diff --git a/linter/linter.go b/linter/linter.go index 54acda8d83d..a3befcc0797 100644 --- a/linter/linter.go +++ b/linter/linter.go @@ -3,9 +3,7 @@ package linter import ( "bytes" "crypto" - "crypto/ecdsa" "crypto/rand" - "crypto/rsa" "crypto/x509" "fmt" "strings" @@ -136,26 +134,6 @@ func (l *Linter) CheckCRL(tbs *x509.RevocationList, reg lint.Registry) error { return ProcessResultSet(lintRes) } -func makeSigner(realSigner crypto.Signer) (crypto.Signer, error) { - var lintSigner crypto.Signer - var err error - switch k := realSigner.Public().(type) { - case *rsa.PublicKey: - lintSigner, err = rsa.GenerateKey(rand.Reader, k.Size()*8) - if err != nil { - return nil, fmt.Errorf("failed to create RSA lint signer: %w", err) - } - case *ecdsa.PublicKey: - lintSigner, err = ecdsa.GenerateKey(k.Curve, rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to create ECDSA lint signer: %w", err) - } - default: - return nil, fmt.Errorf("unsupported lint signer type: %T", k) - } - return lintSigner, nil -} - func makeIssuer(realIssuer *x509.Certificate, lintSigner crypto.Signer) (*x509.Certificate, error) { lintIssuerTBS := &x509.Certificate{ // This is nearly the full list of attributes that diff --git a/linter/makesigner.go b/linter/makesigner.go new file mode 100644 index 00000000000..3ab91a1236d --- /dev/null +++ b/linter/makesigner.go @@ -0,0 +1,31 @@ +//go:build !go1.27 + +package linter + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "fmt" +) + +func makeSigner(realSigner crypto.Signer) (crypto.Signer, error) { + var lintSigner crypto.Signer + var err error + switch k := realSigner.Public().(type) { + case *rsa.PublicKey: + lintSigner, err = rsa.GenerateKey(rand.Reader, k.Size()*8) + if err != nil { + return nil, fmt.Errorf("failed to create RSA lint signer: %w", err) + } + case *ecdsa.PublicKey: + lintSigner, err = ecdsa.GenerateKey(k.Curve, rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to create ECDSA lint signer: %w", err) + } + default: + return nil, fmt.Errorf("unsupported lint signer type: %T", k) + } + return lintSigner, nil +} diff --git a/linter/makesigner_go127.go b/linter/makesigner_go127.go new file mode 100644 index 00000000000..d5c15e0062a --- /dev/null +++ b/linter/makesigner_go127.go @@ -0,0 +1,40 @@ +//go:build go1.27 + +package linter + +import ( + "crypto" + "crypto/ecdsa" + "crypto/mldsa" + "crypto/rand" + "crypto/rsa" + "fmt" +) + +// makeSigner makes a signer with a throwaway key that matches `realSigner`'s type. +// +// TODO(#8812): Move this back to linter.go, above makeIssuer. +func makeSigner(realSigner crypto.Signer) (crypto.Signer, error) { + var lintSigner crypto.Signer + var err error + switch k := realSigner.Public().(type) { + case *rsa.PublicKey: + lintSigner, err = rsa.GenerateKey(rand.Reader, k.Size()*8) + if err != nil { + return nil, fmt.Errorf("failed to create RSA lint signer: %w", err) + } + case *ecdsa.PublicKey: + lintSigner, err = ecdsa.GenerateKey(k.Curve, rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to create ECDSA lint signer: %w", err) + } + case *mldsa.PublicKey: + lintSigner, err = mldsa.GenerateKey(k.Parameters()) + if err != nil { + return nil, fmt.Errorf("failed to create ML-DSA lint signer: %w", err) + } + default: + return nil, fmt.Errorf("unsupported lint signer type: %T", k) + } + return lintSigner, nil +} diff --git a/log/log.go b/log/log.go deleted file mode 100644 index 90988e0cc3a..00000000000 --- a/log/log.go +++ /dev/null @@ -1,362 +0,0 @@ -package log - -import ( - "bytes" - "encoding/base64" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "hash/crc32" - "io" - "log/syslog" - "os" - "strings" - "sync" - - "github.com/jmhodges/clock" - "golang.org/x/term" - - "github.com/letsencrypt/boulder/core" -) - -// A Logger logs messages with explicit priority levels. It is -// implemented by a logging back-end as provided by New() or -// NewMock(). Any additions to this interface with format strings should be -// added to the govet configuration in .golangci.yml -type Logger interface { - Err(msg string) - Errf(format string, a ...any) - Warning(msg string) - Warningf(format string, a ...any) - Info(msg string) - Infof(format string, a ...any) - InfoObject(string, any) - Debug(msg string) - Debugf(format string, a ...any) - AuditInfo(string, any) - AuditErr(string, error, map[string]any) -} - -// impl implements Logger. -type impl struct { - w writer -} - -// singleton defines the object of a Singleton pattern -type singleton struct { - once sync.Once - log Logger -} - -// _Singleton is the single impl entity in memory -var _Singleton singleton - -// The constant used to identify audit-specific messages -const auditTag = "[AUDIT]" - -// New returns a new Logger that uses the given syslog.Writer as a backend -// and also writes to stdout/stderr. It is safe for concurrent use. -func New(log *syslog.Writer, stdoutLogLevel int, syslogLogLevel int) (Logger, error) { - if log == nil { - return nil, errors.New("Attempted to use a nil System Logger") - } - return &impl{ - &bothWriter{ - sync.Mutex{}, - log, - newStdoutWriter(stdoutLogLevel), - syslogLogLevel, - }, - }, nil -} - -// StdoutLogger returns a Logger that writes solely to stdout and stderr. -// It is safe for concurrent use. -func StdoutLogger(level int) Logger { - return &impl{newStdoutWriter(level)} -} - -func newStdoutWriter(level int) *stdoutWriter { - prefix, clkFormat := getPrefix() - return &stdoutWriter{ - prefix: prefix, - level: level, - clkFormat: clkFormat, - clk: clock.New(), - stdout: os.Stdout, - stderr: os.Stderr, - isatty: term.IsTerminal(int(os.Stdout.Fd())), - } -} - -// initialize is used in unit tests and called by `Get` before the logger -// is fully set up. -func initialize() { - const defaultPriority = syslog.LOG_INFO | syslog.LOG_LOCAL0 - syslogger, err := syslog.Dial("", "", defaultPriority, "test") - if err != nil { - panic(err) - } - logger, err := New(syslogger, int(syslog.LOG_DEBUG), int(syslog.LOG_DEBUG)) - if err != nil { - panic(err) - } - - _ = Set(logger) -} - -// Set configures the singleton Logger. This method -// must only be called once, and before calling Get the -// first time. -func Set(logger Logger) (err error) { - if _Singleton.log != nil { - err = errors.New("You may not call Set after it has already been implicitly or explicitly set") - _Singleton.log.Warning(err.Error()) - } else { - _Singleton.log = logger - } - return -} - -// Get obtains the singleton Logger. If Set has not been called first, this -// method initializes with basic defaults. The basic defaults cannot error, and -// subsequent access to an already-set Logger also cannot error, so this method is -// error-safe. -func Get() Logger { - _Singleton.once.Do(func() { - if _Singleton.log == nil { - initialize() - } - }) - - return _Singleton.log -} - -type writer interface { - logAtLevel(syslog.Priority, string) -} - -// bothWriter implements writer and writes to both syslog and stdout. -type bothWriter struct { - sync.Mutex - *syslog.Writer - *stdoutWriter - syslogLevel int -} - -// stdoutWriter implements writer and writes just to stdout. -type stdoutWriter struct { - // prefix is a set of information that is the same for every log line, - // imitating what syslog emits for us when we use the syslog writer. - prefix string - level int - clkFormat string - clk clock.Clock - stdout io.Writer - stderr io.Writer - isatty bool -} - -// LogLineChecksum computes a CRC32 over the log line, which can be checked by -// log-validator to ensure no unexpected log corruption has occurred. -func LogLineChecksum(line string) string { - crc := crc32.ChecksumIEEE([]byte(line)) - buf := make([]byte, crc32.Size) - // Error is unreachable because we provide a supported type and buffer size - _, _ = binary.Encode(buf, binary.LittleEndian, crc) - return base64.RawURLEncoding.EncodeToString(buf) -} - -func checkSummed(msg string) string { - return fmt.Sprintf("%s %s", LogLineChecksum(msg), msg) -} - -// logAtLevel logs the provided message at the appropriate level, writing to -// both stdout and the Logger -func (w *bothWriter) logAtLevel(level syslog.Priority, msg string) { - var err error - - // Since messages are delimited by newlines, we have to escape any internal or - // trailing newlines before generating the checksum or outputting the message. - msg = strings.ReplaceAll(msg, "\n", "\\n") - - w.Lock() - defer w.Unlock() - - switch syslogAllowed := int(level) <= w.syslogLevel; level { - case syslog.LOG_ERR: - if syslogAllowed { - err = w.Err(checkSummed(msg)) - } - case syslog.LOG_WARNING: - if syslogAllowed { - err = w.Warning(checkSummed(msg)) - } - case syslog.LOG_INFO: - if syslogAllowed { - err = w.Info(checkSummed(msg)) - } - case syslog.LOG_DEBUG: - if syslogAllowed { - err = w.Debug(checkSummed(msg)) - } - default: - err = w.Err(fmt.Sprintf("%s (unknown logging level: %d)", checkSummed(msg), int(level))) - } - - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to write to syslog: %d %s (%s)\n", int(level), checkSummed(msg), err) - } - - w.stdoutWriter.logAtLevel(level, msg) -} - -// logAtLevel logs the provided message to stdout, or stderr if it is at Warning or Error level. -func (w *stdoutWriter) logAtLevel(level syslog.Priority, msg string) { - if int(level) <= w.level { - output := w.stdout - if int(level) <= int(syslog.LOG_WARNING) { - output = w.stderr - } - - msg = strings.ReplaceAll(msg, "\n", "\\n") - - var color string - var reset string - - const red = "\033[31m\033[1m" - const yellow = "\033[33m" - const gray = "\033[37m\033[2m" - - if w.isatty { - if int(level) == int(syslog.LOG_DEBUG) { - color = gray - reset = "\033[0m" - } else if int(level) == int(syslog.LOG_WARNING) { - color = yellow - reset = "\033[0m" - } else if int(level) <= int(syslog.LOG_ERR) { - color = red - reset = "\033[0m" - } - } - - if _, err := fmt.Fprintf(output, "%s%s %s%d %s %s%s\n", - color, - w.clk.Now().UTC().Format(w.clkFormat), - w.prefix, - int(level), - core.Command(), - checkSummed(msg), - reset); err != nil { - panic(fmt.Sprintf("failed to write to stdout: %v\n", err)) - } - } -} - -func (log *impl) auditAtLevel(level syslog.Priority, msg string) { - msg = fmt.Sprintf("%s %s", auditTag, msg) - log.w.logAtLevel(level, msg) -} - -// Err level messages are always marked with the audit tag, for special handling -// at the upstream system logger. -func (log *impl) Err(msg string) { - log.w.logAtLevel(syslog.LOG_ERR, msg) -} - -// Errf level messages are always marked with the audit tag, for special handling -// at the upstream system logger. -func (log *impl) Errf(format string, a ...any) { - log.w.logAtLevel(syslog.LOG_ERR, fmt.Sprintf(format, a...)) -} - -// Warning level messages pass through normally. -func (log *impl) Warning(msg string) { - log.w.logAtLevel(syslog.LOG_WARNING, msg) -} - -// Warningf level messages pass through normally. -func (log *impl) Warningf(format string, a ...any) { - log.w.logAtLevel(syslog.LOG_WARNING, fmt.Sprintf(format, a...)) -} - -// Info level messages pass through normally. -func (log *impl) Info(msg string) { - log.w.logAtLevel(syslog.LOG_INFO, msg) -} - -// Infof level messages pass through normally. -func (log *impl) Infof(format string, a ...any) { - log.w.logAtLevel(syslog.LOG_INFO, fmt.Sprintf(format, a...)) -} - -// InfoObject logs an INFO level JSON-serialized object message. -func (log *impl) InfoObject(msg string, obj any) { - jsonObj, err := formatObj(obj) - if err != nil { - log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("Object for msg %q could not be serialized to JSON. Raw: %+v", msg, obj)) - return - } - - log.Infof("%s JSON=%s", msg, jsonObj) -} - -// Debug level messages pass through normally. -func (log *impl) Debug(msg string) { - log.w.logAtLevel(syslog.LOG_DEBUG, msg) -} - -// Debugf level messages pass through normally. -func (log *impl) Debugf(format string, a ...any) { - log.w.logAtLevel(syslog.LOG_DEBUG, fmt.Sprintf(format, a...)) -} - -// AuditInfo sends an INFO-severity JSON-serialized object message that is prefixed -// with the audit tag, for special handling at the upstream system logger. -func (log *impl) AuditInfo(msg string, obj any) { - jsonObj, err := formatObj(obj) - if err != nil { - log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("Object for msg %q could not be serialized to JSON. Raw: %+v", msg, obj)) - return - } - - log.auditAtLevel(syslog.LOG_INFO, fmt.Sprintf("%s JSON=%s", msg, jsonObj)) -} - -// AuditErr sends an ERROR-level JSON-serialized message that is prefixed with -// the audit tag. It restricts its last argument to map[string]any, rather than -// allowing any struct at all like AuditInfo, so that it can add the given error -// to that map under the key "error". -func (log *impl) AuditErr(msg string, err error, obj map[string]any) { - if err != nil { - if obj == nil { - obj = make(map[string]any) - } - obj["error"] = err.Error() - } - - jsonObj, err := formatObj(obj) - if err != nil { - log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("Object for msg %q could not be serialized to JSON. Raw: %+v", msg, obj)) - return - } - - log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("%s JSON=%s", msg, jsonObj)) -} - -// formatObj marshals any object to json. It's the equivalent of json.Marshal, -// except that it doesn't escape <, >, and &, and it doesn't include the -// trailing newline. Code based on appendJSONMarshal from the slog package. -func formatObj(obj any) (string, error) { - var bb bytes.Buffer - enc := json.NewEncoder(&bb) - enc.SetEscapeHTML(false) - err := enc.Encode(obj) - if err != nil { - return "", err - } - bs := bb.String() - return strings.TrimRight(bs, "\n"), nil -} diff --git a/log/log_test.go b/log/log_test.go deleted file mode 100644 index 295c9290031..00000000000 --- a/log/log_test.go +++ /dev/null @@ -1,368 +0,0 @@ -package log - -import ( - "bytes" - "fmt" - "log/syslog" - "net" - "os" - "strings" - "sync" - "testing" - "time" - - "github.com/jmhodges/clock" - - "github.com/letsencrypt/boulder/test" -) - -const stdoutLevel = 7 -const syslogLevel = 7 - -func setup(t *testing.T) *impl { - // Write all logs to UDP on a high port so as to not bother the system - // which is running the test - writer, err := syslog.Dial("udp", "127.0.0.1:65530", syslog.LOG_INFO|syslog.LOG_LOCAL0, "") - test.AssertNotError(t, err, "Could not construct syslog object") - - logger, err := New(writer, stdoutLevel, syslogLevel) - test.AssertNotError(t, err, "Could not construct syslog object") - impl, ok := logger.(*impl) - if !ok { - t.Fatalf("Wrong type returned from New: %T", logger) - } - return impl -} - -func TestConstruction(t *testing.T) { - t.Parallel() - _ = setup(t) -} - -func TestSingleton(t *testing.T) { - t.Parallel() - log1 := Get() - test.AssertNotNil(t, log1, "Logger shouldn't be nil") - - log2 := Get() - test.AssertEquals(t, log1, log2) - - audit := setup(t) - - // Should not work - err := Set(audit) - test.AssertError(t, err, "Can't re-set") - - // Verify no change - log4 := Get() - - // Verify that log4 != log3 - test.AssertNotEquals(t, log4, audit) - - // Verify that log4 == log2 == log1 - test.AssertEquals(t, log4, log2) - test.AssertEquals(t, log4, log1) -} - -func TestConstructionNil(t *testing.T) { - t.Parallel() - _, err := New(nil, stdoutLevel, syslogLevel) - test.AssertError(t, err, "Nil shouldn't be permitted.") -} - -func TestEmit(t *testing.T) { - t.Parallel() - log := setup(t) - - log.AuditInfo("test message", nil) -} - -func TestEmitEmpty(t *testing.T) { - t.Parallel() - log := setup(t) - - log.AuditInfo("", nil) -} - -func TestStdoutLogger(t *testing.T) { - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) - logger := &impl{ - &stdoutWriter{ - prefix: "prefix ", - level: 7, - clkFormat: "2006-01-02", - clk: clock.NewFake(), - stdout: stdout, - stderr: stderr, - }, - } - - logger.AuditErr("Error Audit", fmt.Errorf("oops"), nil) - logger.Warning("Warning log") - logger.Info("Info log") - - test.AssertEquals(t, stdout.String(), "1970-01-01 prefix 6 log.test JSP6nQ Info log\n") - test.AssertEquals(t, stderr.String(), "1970-01-01 prefix 3 log.test d_ZkUQ [AUDIT] Error Audit JSON={\"error\":\"oops\"}\n1970-01-01 prefix 4 log.test d52dyA Warning log\n") -} - -func TestSyslogMethods(t *testing.T) { - t.Parallel() - impl := setup(t) - - impl.AuditInfo("audit-logger_test.go: audit-info", map[string]any{"key": "value"}) - impl.AuditErr("audit-logger_test.go: audit-err", fmt.Errorf("oops"), map[string]any{"key": "value"}) - impl.Debug("audit-logger_test.go: debug") - impl.Info("audit-logger_test.go: info") - impl.Warning("audit-logger_test.go: warning") - impl.Debugf("audit-logger_test.go: %s", "debug") - impl.Errf("audit-logger_test.go: %s", "err") - impl.Infof("audit-logger_test.go: %s", "info") - impl.Warningf("audit-logger_test.go: %s", "warning") -} - -func TestAuditInfo(t *testing.T) { - t.Parallel() - - log := NewMock() - - // Test a simple object - log.AuditInfo("Prefix", "String") - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit log simple object") - } - - // Test a system object - log.Clear() - log.AuditInfo("Prefix", t) - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit log system object") - } - - // Test a complex object - log.Clear() - type validObj struct { - A string - B string - } - var valid = validObj{A: "B", B: "C"} - log.AuditInfo("Prefix", valid) - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit log complex object") - } - - // Test a map - log.Clear() - log.AuditInfo("Prefix", map[string]any{"hello": "world", "number": 123}) - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit log map") - } - - // Test a nil object - log.Clear() - log.AuditInfo("Prefix", nil) - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit nil object") - } - - // Test logging an unserializable object - log.Clear() - type invalidObj struct { - A chan string - } - var invalid = invalidObj{A: make(chan string)} - log.AuditInfo("Prefix", invalid) - if len(log.GetAllMatching("[AUDIT]")) != 1 { - t.Errorf("Failed to audit log unserializable object %v", log.GetAllMatching("[AUDIT]")) - } -} - -func TestTransmission(t *testing.T) { - t.Parallel() - - l, err := newUDPListener("127.0.0.1:0") - test.AssertNotError(t, err, "Failed to open log server") - defer func() { - err = l.Close() - test.AssertNotError(t, err, "listener.Close returned error") - }() - - fmt.Printf("Going to %s\n", l.LocalAddr().String()) - writer, err := syslog.Dial("udp", l.LocalAddr().String(), syslog.LOG_INFO|syslog.LOG_LOCAL0, "") - test.AssertNotError(t, err, "Failed to find connect to log server") - - impl, err := New(writer, stdoutLevel, syslogLevel) - test.AssertNotError(t, err, "Failed to construct audit logger") - - data := make([]byte, 128) - - impl.AuditInfo("audit-logger_test.go: audit-info", map[string]any{"key": "value"}) - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.AuditErr("audit-logger_test.go: audit-err", fmt.Errorf("oops"), nil) - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Debug("audit-logger_test.go: debug") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Info("audit-logger_test.go: info") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Warning("audit-logger_test.go: warning") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Debugf("audit-logger_test.go: %s", "debug") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Errf("audit-logger_test.go: %s", "err") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Infof("audit-logger_test.go: %s", "info") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - - impl.Warningf("audit-logger_test.go: %s", "warning") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") -} - -func TestSyslogLevels(t *testing.T) { - t.Parallel() - - l, err := newUDPListener("127.0.0.1:0") - test.AssertNotError(t, err, "Failed to open log server") - defer func() { - err = l.Close() - test.AssertNotError(t, err, "listener.Close returned error") - }() - - fmt.Printf("Going to %s\n", l.LocalAddr().String()) - writer, err := syslog.Dial("udp", l.LocalAddr().String(), syslog.LOG_INFO|syslog.LOG_LOCAL0, "") - test.AssertNotError(t, err, "Failed to find connect to log server") - - // create a logger with syslog level debug - impl, err := New(writer, stdoutLevel, int(syslog.LOG_DEBUG)) - test.AssertNotError(t, err, "Failed to construct audit logger") - - data := make([]byte, 512) - - // debug messages should be sent to the logger - impl.Debug("log_test.go: debug") - _, _, err = l.ReadFrom(data) - test.AssertNotError(t, err, "Failed to find packet") - test.Assert(t, strings.Contains(string(data), "log_test.go: debug"), "Failed to find log message") - - // create a logger with syslog level info - impl, err = New(writer, stdoutLevel, int(syslog.LOG_INFO)) - test.AssertNotError(t, err, "Failed to construct audit logger") - - // debug messages should not be sent to the logger - impl.Debug("log_test.go: debug") - n, _, err := l.ReadFrom(data) - if n != 0 && err == nil { - t.Error("Failed to withhold debug log message") - } -} - -func newUDPListener(addr string) (*net.UDPConn, error) { - l, err := net.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - err = l.SetDeadline(time.Now().Add(100 * time.Millisecond)) - if err != nil { - return nil, err - } - err = l.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - if err != nil { - return nil, err - } - err = l.SetWriteDeadline(time.Now().Add(100 * time.Millisecond)) - if err != nil { - return nil, err - } - return l.(*net.UDPConn), nil -} - -// TestStdoutFailure tests that audit logging with a bothWriter panics if stdout -// becomes unavailable. -func TestStdoutFailure(t *testing.T) { - // Save the stdout fd so we can restore it later - saved := os.Stdout - - // Create a throw-away pipe FD to replace stdout with - _, w, err := os.Pipe() - test.AssertNotError(t, err, "failed to create pipe") - os.Stdout = w - - // Setup the logger - log := setup(t) - - // Close Stdout so that the fmt.Printf in bothWriter's logAtLevel - // function will return an err on next log. - err = os.Stdout.Close() - test.AssertNotError(t, err, "failed to close stdout") - - // Defer a function that will check if there was a panic to recover from. If - // there wasn't then the test should fail, we were able to AuditInfo when - // Stdout was inoperable. - defer func() { - if recovered := recover(); recovered == nil { - t.Errorf("log.AuditInfo with Stdout closed did not panic") - } - - // Restore stdout so that subsequent tests don't fail - os.Stdout = saved - }() - - // Try to audit log something - log.AuditInfo("This should cause a panic, stdout is closed!", nil) -} - -func TestLogAtLevelEscapesNewlines(t *testing.T) { - var buf bytes.Buffer - w := &bothWriter{sync.Mutex{}, - nil, - &stdoutWriter{ - stdout: &buf, - clk: clock.NewFake(), - level: 6, - }, - -1, - } - w.logAtLevel(6, "foo\nbar") - - test.Assert(t, strings.Contains(buf.String(), "foo\\nbar"), "failed to escape newline") -} - -func TestLogLineChecksum(t *testing.T) { - testCases := []struct { - name string - function func(string) string - input string - expected string - }{ - { - name: "LogLineChecksum with Hello, World!", - function: LogLineChecksum, - input: "Hello, World!", - expected: "0MNK7A", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - checksum := tc.function(tc.input) - if checksum != tc.expected { - t.Fatalf("got %q, want %q", checksum, tc.expected) - } - }) - } -} diff --git a/log/mock.go b/log/mock.go deleted file mode 100644 index 22286b052b7..00000000000 --- a/log/mock.go +++ /dev/null @@ -1,130 +0,0 @@ -package log - -import ( - "fmt" - "log/syslog" - "regexp" - "strings" -) - -// UseMock sets a mock logger as the default logger, and returns it. -func UseMock() *Mock { - m := NewMock() - _ = Set(m) - return m -} - -// NewMock creates a mock logger. -func NewMock() *Mock { - return &Mock{impl{newMockWriter()}} -} - -// Mock is a logger that stores all log messages in memory to be examined by a -// test. -type Mock struct { - impl -} - -// WaitingMock is a logger that stores all messages in memory to be examined by a test with methods -type WaitingMock struct { - impl -} - -// Mock implements the writer interface. It -// stores all logged messages in a buffer for inspection by test -// functions (via GetAll()) instead of sending them to syslog. -type mockWriter struct { - logged []string - msgChan chan<- string - getChan <-chan []string - clearChan chan<- struct{} - closeChan chan<- struct{} -} - -var levelName = map[syslog.Priority]string{ - syslog.LOG_ERR: "ERR", - syslog.LOG_WARNING: "WARNING", - syslog.LOG_INFO: "INFO", - syslog.LOG_DEBUG: "DEBUG", -} - -func (w *mockWriter) logAtLevel(p syslog.Priority, msg string) { - w.msgChan <- fmt.Sprintf("%s: %s", levelName[p&7], msg) -} - -// newMockWriter returns a new mockWriter -func newMockWriter() *mockWriter { - msgChan := make(chan string) - getChan := make(chan []string) - clearChan := make(chan struct{}) - closeChan := make(chan struct{}) - w := &mockWriter{ - logged: []string{}, - msgChan: msgChan, - getChan: getChan, - clearChan: clearChan, - closeChan: closeChan, - } - go func() { - for { - select { - case logMsg := <-msgChan: - w.logged = append(w.logged, logMsg) - case getChan <- w.logged: - case <-clearChan: - w.logged = []string{} - case <-closeChan: - close(getChan) - return - } - } - }() - return w -} - -// GetAll returns all messages logged since instantiation or the last call to -// Clear(). -// -// The caller must not modify the returned slice or its elements. -func (m *Mock) GetAll() []string { - w := m.w.(*mockWriter) - return <-w.getChan -} - -// GetAllMatching returns all messages logged since instantiation or the last -// Clear() whose text matches the given regexp. The regexp is -// accepted as a string and compiled on the fly, because convenience -// is more important than performance. -// -// The caller must not modify the elements of the returned slice. -func (m *Mock) GetAllMatching(reString string) []string { - var matches []string - w := m.w.(*mockWriter) - re := regexp.MustCompile(reString) - for _, logMsg := range <-w.getChan { - if re.MatchString(logMsg) { - matches = append(matches, logMsg) - } - } - return matches -} - -func (m *Mock) ExpectMatch(reString string) error { - results := m.GetAllMatching(reString) - if len(results) == 0 { - return fmt.Errorf("expected log line %q, got %q", reString, strings.Join(m.GetAll(), "\n")) - } - return nil -} - -// Clear resets the log buffer. -func (m *Mock) Clear() { - w := m.w.(*mockWriter) - w.clearChan <- struct{}{} -} - -// Close shuts down the mock's background goroutine. -func (m *Mock) Close() { - w := m.w.(*mockWriter) - close(w.closeChan) -} diff --git a/log/prod_prefix.go b/log/prod_prefix.go deleted file mode 100644 index b4cf55daff5..00000000000 --- a/log/prod_prefix.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !integration - -package log - -import ( - "fmt" - "os" - "strings" - - "github.com/letsencrypt/boulder/core" -) - -// getPrefix returns the prefix and clkFormat that should be used by the -// stdout logger. -func getPrefix() (string, string) { - shortHostname := "unknown" - datacenter := "unknown" - hostname, err := os.Hostname() - if err == nil { - splits := strings.SplitN(hostname, ".", 3) - shortHostname = splits[0] - if len(splits) > 1 { - datacenter = splits[1] - } - } - - prefix := fmt.Sprintf("%s %s %s[%d]: ", shortHostname, datacenter, core.Command(), os.Getpid()) - clkFormat := "2006-01-02T15:04:05.000000+00:00Z" - - return prefix, clkFormat -} diff --git a/log/test_prefix.go b/log/test_prefix.go deleted file mode 100644 index d1fb8949127..00000000000 --- a/log/test_prefix.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build integration - -package log - -// getPrefix returns the prefix and clkFormat that should be used by the -// stdout logger. -func getPrefix() (string, string) { - return "", "15:04:05.000000" -} diff --git a/log/validator/tail_logger.go b/log/validator/tail_logger.go deleted file mode 100644 index 697ac00e459..00000000000 --- a/log/validator/tail_logger.go +++ /dev/null @@ -1,40 +0,0 @@ -package validator - -import ( - "fmt" - - "github.com/letsencrypt/boulder/log" -) - -// tailLogger is an adapter to the nxadm/tail module's logging interface. -type tailLogger struct { - log.Logger -} - -func (tl tailLogger) Fatal(v ...any) { - tl.Err(fmt.Sprint(v...)) -} -func (tl tailLogger) Fatalf(format string, v ...any) { - tl.Errf(format, v...) -} -func (tl tailLogger) Fatalln(v ...any) { - tl.Err(fmt.Sprint(v...) + "\n") -} -func (tl tailLogger) Panic(v ...any) { - tl.Err(fmt.Sprint(v...)) -} -func (tl tailLogger) Panicf(format string, v ...any) { - tl.Errf(format, v...) -} -func (tl tailLogger) Panicln(v ...any) { - tl.Err(fmt.Sprint(v...) + "\n") -} -func (tl tailLogger) Print(v ...any) { - tl.Info(fmt.Sprint(v...)) -} -func (tl tailLogger) Printf(format string, v ...any) { - tl.Infof(format, v...) -} -func (tl tailLogger) Println(v ...any) { - tl.Info(fmt.Sprint(v...) + "\n") -} diff --git a/mtca/mtca.go b/mtca/mtca.go index f57a15578f5..c40eb1df21c 100644 --- a/mtca/mtca.go +++ b/mtca/mtca.go @@ -1,8 +1,12 @@ +//go:build go1.27 + package mtca import ( "context" + "encoding/asn1" "fmt" + "sync" "github.com/letsencrypt/boulder/issuance" mtcapb "github.com/letsencrypt/boulder/mtca/proto" @@ -11,16 +15,50 @@ import ( var _ mtcapb.MTCAServer = &mtca{} func New(issuer *issuance.Issuer) *mtca { + var mtcaID string + testingTrustAnchorIDOID := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 47, 1} + for _, attribute := range issuer.Cert.Subject.Names { + if attribute.Type.Equal(testingTrustAnchorIDOID) { + mtcaID, _ = attribute.Value.(string) + break + } + } return &mtca{ issuer: issuer, + mtcaID: mtcaID, + // TODO: collect this from config + logNumber: 0, + latestEntryIndex: 0, } } type mtca struct { mtcapb.UnimplementedMTCAServer - issuer *issuance.Issuer + + issuer *issuance.Issuer + mtcaID string + logNumber uint16 + + // This is just a dummy for testing; in reality this will come from the DB. + latestEntryIndex int64 + + sequencing sync.Mutex +} + +// mtcLogID returns the string-formatted relative OID for this log. +// The .0. arc relative to the MTCA ID contains log numbers. +// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#ca-ids +func (m *mtca) mtcLogID() string { + return fmt.Sprintf("%s.0.%d", m.mtcaID, m.logNumber) } func (m *mtca) Issue(ctx context.Context, req *mtcapb.IssueRequest) (*mtcapb.IssueResponse, error) { - return nil, fmt.Errorf("not implemented") + m.sequencing.Lock() + defer m.sequencing.Unlock() + m.latestEntryIndex++ + + return &mtcapb.IssueResponse{ + MtcLogID: m.mtcLogID(), + MtcEntryIndex: m.latestEntryIndex, + }, nil } diff --git a/mtpublisher/mtpublisher.go b/mtpublisher/mtpublisher.go new file mode 100644 index 00000000000..dab4283bcbd --- /dev/null +++ b/mtpublisher/mtpublisher.go @@ -0,0 +1,111 @@ +package mtpublisher + +import ( + "context" + "crypto/ed25519" + "database/sql" + "encoding/binary" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/db" +) + +// MTPublisher polls the MTC issuance log and adds a dummy cosignature to the +// latest checkpoint if it lacks one. It is a stub for the real MTPublisher. +type MTPublisher struct { + db *db.WrappedMap + interval time.Duration + mtcLogID string + mirrorID string + clk clock.Clock + log blog.Logger +} + +// New returns a new *MTPublisher. +func New(dbMap *db.WrappedMap, interval time.Duration, mtcLogID, mirrorID string, clk clock.Clock, log blog.Logger) (*MTPublisher, error) { + if interval <= 0 { + return nil, fmt.Errorf("interval must be positive, got %s", interval) + } + if mtcLogID == "" { + return nil, errors.New("mtcLogID must not be empty") + } + if mirrorID == "" { + return nil, errors.New("mirrorID must not be empty") + } + return &MTPublisher{ + db: dbMap, + interval: interval, + mtcLogID: mtcLogID, + mirrorID: mirrorID, + clk: clk, + log: log, + }, nil +} + +type checkpointEntry struct { + ID int64 `db:"id"` + MTCLogID string `db:"mtcLogID"` + TreeSize int64 `db:"treeSize"` + MirrorSignature []byte `db:"mirrorSignature"` +} + +// dummyCosignature returns a dummy Ed25519 tlog-cosignature: a big-endian +// uint64 timestamp followed by the Ed25519 signature. +func (p *MTPublisher) dummyCosignature() []byte { + out := make([]byte, 8+ed25519.SignatureSize) + binary.BigEndian.PutUint64(out[:8], uint64(p.clk.Now().Unix())) //nolint:gosec // G115: a Unix timestamp is non-negative. + return out +} + +func (p *MTPublisher) publish(ctx context.Context) error { + var latest checkpointEntry + err := p.db.SelectOne(ctx, &latest, + "SELECT id, mtcLogID, treeSize, mirrorSignature FROM checkpoints WHERE mtcLogID = ? ORDER BY treeSize DESC LIMIT 1", + p.mtcLogID) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return fmt.Errorf("selecting the latest checkpoint: %w", err) + } + if latest.MirrorSignature != nil { + return nil + } + + _, err = p.db.ExecContext(ctx, + "UPDATE checkpoints SET mirrorID = ?, mirrorSignature = ? WHERE id = ? AND mtcLogID = ?", + p.mirrorID, p.dummyCosignature(), latest.ID, p.mtcLogID) + if err != nil { + return fmt.Errorf("cosigning checkpoint %d (%s size %d): %w", latest.ID, latest.MTCLogID, latest.TreeSize, err) + } + p.log.Info(ctx, "Cosigned checkpoint", + slog.String("logID", latest.MTCLogID), + slog.Int64("treesize", latest.TreeSize), + slog.Int64("checkpoint", latest.ID), + ) + return nil +} + +// Start attempts to cosign the latest checkpoint at each interval until ctx is +// cancelled. +func (p *MTPublisher) Start(ctx context.Context) { + ticker := time.NewTicker(p.interval) + defer ticker.Stop() + for { + err := p.publish(ctx) + if err != nil { + p.log.Error(ctx, "Cosigning pass failed", err) + } + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } +} diff --git a/mtpublisher/mtpublisher_test.go b/mtpublisher/mtpublisher_test.go new file mode 100644 index 00000000000..e7de2c8025d --- /dev/null +++ b/mtpublisher/mtpublisher_test.go @@ -0,0 +1,162 @@ +package mtpublisher + +import ( + "context" + "crypto/ed25519" + "testing" + "time" + + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/db" + "github.com/letsencrypt/boulder/sa" + "github.com/letsencrypt/boulder/test/vars" +) + +const ( + mtcLogID = "44947.4.1.0.44" + mirrorID = "32473.9" +) + +func setupDB(t *testing.T) *db.WrappedMap { + t.Helper() + + dbMap, err := sa.DBMapForTest(vars.DBConnMTCMeta_44947_4_1_0_44FullPerms) + if err != nil { + t.Fatalf("opening mtcmeta dbMap: %s", err) + } + _, err = dbMap.ExecContext(t.Context(), "TRUNCATE TABLE checkpoints") + if err != nil { + t.Fatalf("truncating checkpoints: %s", err) + } + t.Cleanup(func() { + _, err := dbMap.ExecContext(context.Background(), "TRUNCATE TABLE checkpoints") + if err != nil { + t.Logf("cleaning up checkpoints: %s", err) + } + }) + return dbMap +} + +func insertCheckpoint(t *testing.T, dbMap *db.WrappedMap, logID string, treeSize int64) int64 { + t.Helper() + + res, err := dbMap.ExecContext(t.Context(), + "INSERT INTO checkpoints (mtcLogID, mtcaSignature, treeSize, rootHash) VALUES (?, ?, ?, ?)", + logID, []byte("mtca-signature"), treeSize, make([]byte, 32)) + if err != nil { + t.Fatalf("inserting checkpoint (%s size %d): %s", logID, treeSize, err) + } + id, err := res.LastInsertId() + if err != nil { + t.Fatalf("reading insert id: %s", err) + } + return id +} + +func lacksCosignature(t *testing.T, dbMap *db.WrappedMap, id int64) bool { + t.Helper() + var count int64 + err := dbMap.SelectOne(t.Context(), &count, + "SELECT COUNT(*) FROM checkpoints WHERE id = ? AND mirrorID IS NULL AND mirrorSignature IS NULL", id) + if err != nil { + t.Fatalf("querying checkpoint %d: %s", id, err) + } + return count == 1 +} + +func TestPublish(t *testing.T) { + dbMap := setupDB(t) + p, err := New(dbMap, time.Second, mtcLogID, mirrorID, clock.NewFake(), blog.NewMock()) + if err != nil { + t.Fatalf("New: %s", err) + } + + // When there are no checkpoints at all, p.publish() should return without + // error. + err = p.publish(t.Context()) + if err != nil { + t.Fatalf("p.publish() on an empty table: %s", err) + } + + // An older checkpoint that is not cosigned, which must be left untouched. + olderCheckpointID := insertCheckpoint(t, dbMap, mtcLogID, 256) + + // The latest checkpoint, which we expect to be cosigned by p.publish(). + latestCheckpointID := insertCheckpoint(t, dbMap, mtcLogID, 512) + + // A checkpoint for another log that was somehow inserted into this table, + // which must be left untouched thanks to the mtcLogID guard. + otherLogID := insertCheckpoint(t, dbMap, "44947.4.2.0.99", 1024) + + err = p.publish(t.Context()) + if err != nil { + t.Fatalf("p.publish(): %s", err) + } + + // Fetch the latest checkpoint. + type row struct { + MirrorID string `db:"mirrorID"` + MirrorSig []byte `db:"mirrorSignature"` + } + var cosigned row + err = dbMap.SelectOne(t.Context(), &cosigned, "SELECT mirrorID, mirrorSignature FROM checkpoints WHERE id = ?", latestCheckpointID) + if err != nil { + t.Fatalf("selecting the latest checkpoint: %s", err) + } + + // Check that the latest checkpoint was cosigned, and the others were + // untouched. + if cosigned.MirrorID != mirrorID { + t.Errorf("mirrorID = %q, want %q", cosigned.MirrorID, mirrorID) + } + if len(cosigned.MirrorSig) != 8+ed25519.SignatureSize { + t.Errorf("latest checkpoint's mirrorSignature is %d bytes, want %d", len(cosigned.MirrorSig), 8+ed25519.SignatureSize) + } + if !lacksCosignature(t, dbMap, olderCheckpointID) { + t.Error("older checkpoint was cosigned, only the latest should be") + } + if !lacksCosignature(t, dbMap, otherLogID) { + t.Errorf("otherLogID checkpoint (id=%d), despite guard on mtcLogID", otherLogID) + } +} + +func TestPublishWhenLatestAlreadySigned(t *testing.T) { + dbMap := setupDB(t) + p, err := New(dbMap, time.Second, mtcLogID, mirrorID, clock.NewFake(), blog.NewMock()) + if err != nil { + t.Fatalf("New: %s", err) + } + + // Insert a checkpoint that is already cosigned, which must be left + // untouched. + _, err = dbMap.ExecContext(t.Context(), + "INSERT INTO checkpoints (mtcLogID, mtcaSignature, treeSize, rootHash, mirrorID, mirrorSignature) VALUES (?, ?, ?, ?, ?, ?)", + mtcLogID, []byte("mtca-signature"), int64(512), make([]byte, 32), "existing.cosigner", []byte("already-signed-bruh")) + if err != nil { + t.Fatalf("inserting cosigned checkpoint: %s", err) + } + + // Insert an older (non-latest) checkpoint that is not cosigned, which must + // be left untouched. + olderID := insertCheckpoint(t, dbMap, mtcLogID, 256) + + err = p.publish(t.Context()) + if err != nil { + t.Fatalf("p.publish(): %s", err) + } + + // The latest checkpoint is already cosigned and the older checkpoint is left untouched. + if !lacksCosignature(t, dbMap, olderID) { + t.Error("older checkpoint was cosigned, the pass should have stopped at the signed latest") + } + var cosignature []byte + err = dbMap.SelectOne(t.Context(), &cosignature, "SELECT mirrorSignature FROM checkpoints WHERE mtcLogID = ? AND treeSize = 512", mtcLogID) + if err != nil { + t.Fatalf("selecting the cosigned checkpoint: %s", err) + } + if string(cosignature) != "already-signed-bruh" { + t.Errorf("existing cosignature was replaced: %q", cosignature) + } +} diff --git a/observer/monitor.go b/observer/monitor.go index 02a22c3b197..788fd4f5d7e 100644 --- a/observer/monitor.go +++ b/observer/monitor.go @@ -2,10 +2,11 @@ package observer import ( "context" + "log/slog" "strconv" "time" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/observer/probers" ) @@ -35,11 +36,19 @@ func (m monitor) start(logger blog.Logger) { // Log the outcome of the probe attempt. if err != nil { - logger.Errf("kind=[%s] success=[%t] duration=[%f] name=[%s] error=[%s]", - m.prober.Kind(), err == nil, dur.Seconds(), m.prober.Name(), err) + logger.Error(ctx, "Probe complete", err, + slog.String("kind", m.prober.Kind()), + slog.String("name", m.prober.Name()), + slog.Bool("success", false), + slog.Duration("duration", dur), + ) } else { - logger.Infof("kind=[%s] success=[%t] duration=[%f] name=[%s]", - m.prober.Kind(), err == nil, dur.Seconds(), m.prober.Name()) + logger.Info(ctx, "Probe complete", + slog.String("kind", m.prober.Kind()), + slog.String("name", m.prober.Name()), + slog.Bool("success", true), + slog.Duration("duration", dur), + ) } }() <-ticker.C diff --git a/observer/obs_conf.go b/observer/obs_conf.go index 6f6b6b48965..7ee71a8d8e4 100644 --- a/observer/obs_conf.go +++ b/observer/obs_conf.go @@ -1,12 +1,15 @@ package observer import ( + "context" "errors" "fmt" + "log/slog" "strconv" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/observer/probers" ) @@ -24,9 +27,9 @@ var ( // ObsConf is exported to receive YAML configuration. type ObsConf struct { - DebugAddr string `yaml:"debugaddr" validate:"omitempty,hostname_port"` - Buckets []float64 `yaml:"buckets" validate:"min=1,dive"` - Syslog cmd.SyslogConfig `yaml:"syslog"` + DebugAddr string `yaml:"debugaddr" validate:"omitempty,hostname_port"` + Buckets []float64 `yaml:"buckets" validate:"min=1,dive"` + Syslog blog.Config `yaml:"syslog"` OpenTelemetry cmd.OpenTelemetryConfig MonConfs []*MonConf `yaml:"monitors" validate:"min=1,dive"` } @@ -100,17 +103,15 @@ func (c *ObsConf) MakeObserver() (*Observer, error) { metrics.MustRegister(countMonitors) metrics.MustRegister(histObservations) cmd.LogStartup(logger) - logger.Infof("Initializing boulder-observer daemon") - logger.Debugf("Using config: %+v", c) + + ctx := context.Background() + logger.Debug(ctx, "Using config", slog.Any("config", c)) monitors, errs, err := c.makeMonitors(metrics) if len(errs) != 0 { - logger.Errf("%d of %d monitors failed validation", len(errs), len(c.MonConfs)) for _, err := range errs { - logger.Errf("%s", err) + logger.Error(ctx, "Monitor failed config validation", err) } - } else { - logger.Info("all monitors passed validation") } if err != nil { return nil, err diff --git a/observer/obs_conf_test.go b/observer/obs_conf_test.go index dc2269e2e57..c54eb7dca4f 100644 --- a/observer/obs_conf_test.go +++ b/observer/obs_conf_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/observer/probers" @@ -20,7 +20,7 @@ const ( func TestObsConf_makeMonitors(t *testing.T) { var errDBZ = errors.New(errDBZMsg) - var cfgSyslog = cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: 6} + var cfgSyslog = blog.Config{StdoutLevel: 6, SyslogLevel: 6} var cfgDur = config.Duration{Duration: time.Second * 5} var cfgBuckets = []float64{.001} var validMonConf = &MonConf{ @@ -28,7 +28,7 @@ func TestObsConf_makeMonitors(t *testing.T) { var invalidMonConf = &MonConf{ cfgDur, mockConf, probers.Settings{"valid": false, "errmsg": errDBZMsg, "pname": "foo", "pkind": "bar"}} type fields struct { - Syslog cmd.SyslogConfig + Syslog blog.Config Buckets []float64 DebugAddr string MonConfs []*MonConf diff --git a/observer/observer.go b/observer/observer.go index 1c25ed08134..0b5d44b85f4 100644 --- a/observer/observer.go +++ b/observer/observer.go @@ -3,8 +3,8 @@ package observer import ( "context" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" _ "github.com/letsencrypt/boulder/observer/probers/aia" _ "github.com/letsencrypt/boulder/observer/probers/ccadb" _ "github.com/letsencrypt/boulder/observer/probers/crl" diff --git a/policy/pa.go b/policy/pa.go index 822863a7da6..1f375a98f7d 100644 --- a/policy/pa.go +++ b/policy/pa.go @@ -1,10 +1,12 @@ package policy import ( + "context" "crypto/sha256" "encoding/hex" "errors" "fmt" + "log/slog" "net/mail" "net/netip" "os" @@ -16,12 +18,12 @@ import ( "golang.org/x/net/idna" "golang.org/x/text/unicode/norm" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/iana" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/strictyaml" ) @@ -83,7 +85,7 @@ func (pa *AuthorityImpl) LoadIdentPolicyFile(f string) error { return err } hash := sha256.Sum256(configBytes) - pa.log.Infof("loading identifier policy, sha256: %s", hex.EncodeToString(hash[:])) + pa.log.Info(context.Background(), "Loading identifier policy", slog.String("sha256", hex.EncodeToString(hash[:]))) var policy blockedIdentsPolicy err = strictyaml.Unmarshal(configBytes, &policy) if err != nil { diff --git a/policy/pa_test.go b/policy/pa_test.go index 2ee368c3741..30769cdf048 100644 --- a/policy/pa_test.go +++ b/policy/pa_test.go @@ -10,11 +10,11 @@ import ( "go.yaml.in/yaml/v3" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) diff --git a/privatekey/privatekey.go b/privatekey/privatekey.go index 3f5fb59b5e3..81caed3878e 100644 --- a/privatekey/privatekey.go +++ b/privatekey/privatekey.go @@ -57,29 +57,6 @@ func verifyECDSA(privKey *ecdsa.PrivateKey, pubKey *ecdsa.PublicKey, msgHash has return privKey, privKey.Public(), nil } -// verify ensures that the embedded PublicKey of the provided privateKey is -// actually a match for the private key. For an example of private keys -// embedding a mismatched public key, see: -// https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html. -func verify(privateKey crypto.Signer) (crypto.Signer, crypto.PublicKey, error) { - verifyHash, err := makeVerifyHash() - if err != nil { - return nil, nil, err - } - - switch k := privateKey.(type) { - case *rsa.PrivateKey: - return verifyRSA(k, &k.PublicKey, verifyHash) - - case *ecdsa.PrivateKey: - return verifyECDSA(k, &k.PublicKey, verifyHash) - - default: - // This should never happen. - return nil, nil, errors.New("the provided private key could not be asserted to ECDSA or RSA") - } -} - // Load decodes and parses a private key from the provided file path and returns // the private key as crypto.Signer. keyPath is expected to be a PEM formatted // RSA or ECDSA private key in a PKCS #1, PKCS# 8, or SEC 1 container. The diff --git a/privatekey/verify.go b/privatekey/verify.go new file mode 100644 index 00000000000..9768ccd0e59 --- /dev/null +++ b/privatekey/verify.go @@ -0,0 +1,33 @@ +//go:build !go1.27 + +package privatekey + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "errors" +) + +// verify ensures that the embedded PublicKey of the provided privateKey is +// actually a match for the private key. For an example of private keys +// embedding a mismatched public key, see: +// https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html. +func verify(privateKey crypto.Signer) (crypto.Signer, crypto.PublicKey, error) { + verifyHash, err := makeVerifyHash() + if err != nil { + return nil, nil, err + } + + switch k := privateKey.(type) { + case *rsa.PrivateKey: + return verifyRSA(k, &k.PublicKey, verifyHash) + + case *ecdsa.PrivateKey: + return verifyECDSA(k, &k.PublicKey, verifyHash) + + default: + // This should never happen. + return nil, nil, errors.New("the provided private key could not be asserted to ECDSA or RSA") + } +} diff --git a/privatekey/verify_go127.go b/privatekey/verify_go127.go new file mode 100644 index 00000000000..557ea4052d7 --- /dev/null +++ b/privatekey/verify_go127.go @@ -0,0 +1,55 @@ +//go:build go1.27 + +package privatekey + +import ( + "crypto" + "crypto/ecdsa" + "crypto/mldsa" + "crypto/rsa" + "errors" + "fmt" + "hash" +) + +// verify ensures that the embedded PublicKey of the provided privateKey is +// actually a match for the private key. For an example of private keys +// embedding a mismatched public key, see: +// https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html. +// +// TODO(#8812): move this back to privatekey.go, above Load(). +func verify(privateKey crypto.Signer) (crypto.Signer, crypto.PublicKey, error) { + verifyHash, err := makeVerifyHash() + if err != nil { + return nil, nil, err + } + + switch k := privateKey.(type) { + case *rsa.PrivateKey: + return verifyRSA(k, &k.PublicKey, verifyHash) + + case *ecdsa.PrivateKey: + return verifyECDSA(k, &k.PublicKey, verifyHash) + + case *mldsa.PrivateKey: + return verifyMLDSA(k, k.PublicKey(), verifyHash) + + default: + // This should never happen. + return nil, nil, errors.New("the provided private key was not *rsa.PrivateKey, *ecdsa.PrivateKey, or *mldsa.PrivateKey") + } +} + +// verifyMLDSA verifies ML-DSA private keys. +func verifyMLDSA(privKey *mldsa.PrivateKey, pubKey *mldsa.PublicKey, msgHash hash.Hash) (crypto.Signer, crypto.PublicKey, error) { + sig, err := privKey.Sign(nil, msgHash.Sum(nil), nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign using the provided ML-DSA private key: %s", err) + } + + err = mldsa.Verify(pubKey, msgHash.Sum(nil), sig, nil) + if err != nil { + return nil, nil, fmt.Errorf("the provided ML-DSA private key failed signature verification: %s", err) + } + return privKey, privKey.Public(), nil +} diff --git a/publisher/publisher.go b/publisher/publisher.go index de88bff92b4..b55acee3e7e 100644 --- a/publisher/publisher.go +++ b/publisher/publisher.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "math/big" "net/http" "net/url" @@ -26,9 +27,9 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" pubpb "github.com/letsencrypt/boulder/publisher/proto" ) @@ -263,15 +264,14 @@ func (pub *Impl) SubmitToSingleCTWithResult(ctx context.Context, req *pubpb.Requ if ok && rspErr.StatusCode < 500 { body = string(rspErr.Body) } - pub.log.InfoObject("Failed to submit certificate to CT log", struct { - LogURL string - Error string - Body string - }{ - LogURL: ctLog.uri, - Error: err.Error(), - Body: body, - }) + pub.log.Info(ctx, "Failed to submit certificate to CT log", + blog.Serial(cert.SerialNumber.String()), + slog.String("issuer", cert.Issuer.CommonName), + slog.String("log", ctLog.uri), + slog.String("body", body), + blog.Error(err), + ) + return nil, err } diff --git a/publisher/publisher_test.go b/publisher/publisher_test.go index 98a501989fd..6cb0c2f118b 100644 --- a/publisher/publisher_test.go +++ b/publisher/publisher_test.go @@ -25,15 +25,14 @@ import ( ct "github.com/google/certificate-transparency-go" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" pubpb "github.com/letsencrypt/boulder/publisher/proto" "github.com/letsencrypt/boulder/test" ) -var log = blog.UseMock() var ctx = context.Background() func getPort(srvURL string) (int, error) { @@ -143,7 +142,7 @@ func setup(t *testing.T) (*Impl, *x509.Certificate, *ecdsa.PrivateKey) { pub := New( issuerBundles, "test-user-agent/1.0", - log, + blog.NewMock(), metrics.NoopRegisterer) // Load leaf certificate @@ -160,7 +159,7 @@ func addLog(t *testing.T, port int, pubKey *ecdsa.PublicKey) *Log { uri := fmt.Sprintf("http://localhost:%d", port) der, err := x509.MarshalPKIXPublicKey(pubKey) test.AssertNotError(t, err, "Failed to marshal key") - newLog, err := NewLog(uri, base64.StdEncoding.EncodeToString(der), "test-user-agent/1.0", log) + newLog, err := NewLog(uri, base64.StdEncoding.EncodeToString(der), "test-user-agent/1.0", blog.NewMock()) test.AssertNotError(t, err, "Couldn't create log") test.AssertEquals(t, newLog.uri, fmt.Sprintf("http://localhost:%d", port)) return newLog @@ -265,13 +264,14 @@ func TestLogCache(t *testing.T) { cache := logCache{ logs: make(map[cacheKey]*Log), } + mocklog := blog.NewMock() // Adding a log with an invalid base64 public key should error - _, err := cache.AddLog("www.test.com", "1234", "test-user-agent/1.0", log) + _, err := cache.AddLog("www.test.com", "1234", "test-user-agent/1.0", blog.NewMock()) test.AssertError(t, err, "AddLog() with invalid base64 pk didn't error") // Adding a log with an invalid URI should error - _, err = cache.AddLog(":", "", "test-user-agent/1.0", log) + _, err = cache.AddLog(":", "", "test-user-agent/1.0", blog.NewMock()) test.AssertError(t, err, "AddLog() with an invalid log URI didn't error") // Create one keypair & base 64 public key @@ -289,21 +289,21 @@ func TestLogCache(t *testing.T) { k2b64 := base64.StdEncoding.EncodeToString(der2) // Adding the first log should not produce an error - l1, err := cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", log) + l1, err := cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", mocklog) test.AssertNotError(t, err, "cache.AddLog() failed for log 1") test.AssertEquals(t, cache.Len(), 1) test.AssertEquals(t, l1.uri, "http://log.one.example.com") test.AssertEquals(t, l1.logID, k1b64) // Adding it again should not produce any errors, or increase the Len() - l1, err = cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", log) + l1, err = cache.AddLog("http://log.one.example.com", k1b64, "test-user-agent/1.0", mocklog) test.AssertNotError(t, err, "cache.AddLog() failed for second add of log 1") test.AssertEquals(t, cache.Len(), 1) test.AssertEquals(t, l1.uri, "http://log.one.example.com") test.AssertEquals(t, l1.logID, k1b64) // Adding a second log should not error and should increase the Len() - l2, err := cache.AddLog("http://log.two.example.com", k2b64, "test-user-agent/1.0", log) + l2, err := cache.AddLog("http://log.two.example.com", k2b64, "test-user-agent/1.0", mocklog) test.AssertNotError(t, err, "cache.AddLog() failed for log 2") test.AssertEquals(t, cache.Len(), 2) test.AssertEquals(t, l2.uri, "http://log.two.example.com") @@ -318,7 +318,6 @@ func TestLogErrorBody(t *testing.T) { port, err := getPort(srv.URL) test.AssertNotError(t, err, "Failed to get test server port") - log.Clear() logURI := fmt.Sprintf("http://localhost:%d", port) pkDER, err := x509.MarshalPKIXPublicKey(&k.PublicKey) test.AssertNotError(t, err, "Failed to marshal key") @@ -330,7 +329,9 @@ func TestLogErrorBody(t *testing.T) { Kind: pubpb.SubmissionType_final, }) test.AssertError(t, err, "SubmitToSingleCTWithResult didn't fail") - test.AssertEquals(t, len(log.GetAllMatching("well this isn't good now is it")), 1) + + mocklog := pub.log.(*blog.Mock) + test.AssertEquals(t, len(mocklog.GetAllMatching("well this isn't good now is it")), 1) } // TestErrorMetrics checks that the ct_errors_count and diff --git a/ra/ra.go b/ra/ra.go index 26552496021..e6a8651598b 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -9,6 +9,7 @@ import ( "encoding/asn1" "errors" "fmt" + "log/slog" "os" "slices" "strconv" @@ -26,6 +27,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/allowlist" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" @@ -38,8 +40,8 @@ import ( bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" + mtcapb "github.com/letsencrypt/boulder/mtca/proto" "github.com/letsencrypt/boulder/probs" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" @@ -70,11 +72,12 @@ var ( type RegistrationAuthorityImpl struct { rapb.UnsafeRegistrationAuthorityServer rapb.UnsafeSCTProviderServer - CA capb.CertificateAuthorityClient - VA va.RemoteClients - SA sapb.StorageAuthorityClient - PA core.PolicyAuthority - publisher pubpb.PublisherClient + CA capb.CertificateAuthorityClient + VA va.RemoteClients + SA sapb.StorageAuthorityClient + PA core.PolicyAuthority + publisher pubpb.PublisherClient + profileToMTCA map[string]mtcapb.MTCAClient clk clock.Clock log blog.Logger @@ -126,6 +129,7 @@ func NewRegistrationAuthorityImpl( finalizeTimeout time.Duration, ctp *ctpolicy.CTPolicy, issuers []*issuance.Certificate, + profileToMTCA map[string]mtcapb.MTCAClient, ) *RegistrationAuthorityImpl { ctpolicyResults := promauto.With(stats).NewHistogramVec( prometheus.HistogramOpts{ @@ -215,6 +219,7 @@ func NewRegistrationAuthorityImpl( limiter: limiter, txnBuilder: txnBuilder, publisher: pubc, + profileToMTCA: profileToMTCA, finalizeTimeout: finalizeTimeout, ctpolicy: ctp, ctpolicyResults: ctpolicyResults, @@ -262,6 +267,9 @@ type ValidationProfileConfig struct { // specified, the profile is open to all accounts. If the file // exists but is empty, the profile is closed to all accounts. AllowList string `validate:"omitempty"` + // MTC indicates that orders with this profile should be sent to an + // MTCA instance for issuance. + MTC bool `validate:"omitempty"` // IdentifierTypes is a list of identifier types that may be issued under // this profile. IdentifierTypes []identifier.IdentifierType `validate:"required,dive,oneof=dns ip"` @@ -292,6 +300,9 @@ type validationProfile struct { // identifierTypes is a list of identifier types that may be issued under // this profile. identifierTypes []identifier.IdentifierType + // MTC indicates that orders with this profile should be sent to an + // MTCA instance for issuance. + mtc bool } // validationProfiles provides access to the set of configured profiles, @@ -359,6 +370,7 @@ func NewValidationProfiles(defaultName string, configs map[string]*ValidationPro maxNames: config.MaxNames, allowList: allowList, identifierTypes: config.IdentifierTypes, + mtc: config.MTC, } } @@ -384,96 +396,6 @@ func (vp *validationProfiles) get(name string) (*validationProfile, error) { return profile, nil } -// certificateRequestAuthz is a struct for logging information about when and -// how an identifier was validated. We include the challenge type that solved -// the authorization and when the challenge was completed to make some common -// analysis easier. -type identifierLog struct { - Ident identifier.ACMEIdentifier - Authz string - Challenge core.AcmeChallenge - Validated time.Time -} - -// certificateRequestEvent is a struct for holding information that is logged as -// JSON to the audit log as the result of an issuance event. -type certificateRequestEvent struct { - ID string `json:",omitempty"` - // Requester is the associated account ID - Requester int64 `json:",omitempty"` - // OrderID is the associated order ID (may be empty for an ACME v1 issuance) - OrderID int64 `json:",omitempty"` - // SerialNumber is the string representation of the issued certificate's - // serial number - SerialNumber string `json:",omitempty"` - // VerifiedFields are required by the baseline requirements and are always - // a static value for Boulder. - VerifiedFields []string `json:",omitempty"` - // CommonName is the subject common name from the issued cert - CommonName string `json:",omitempty"` - // Identifiers are the identifiers and validation data from the issued cert - Identifiers []identifierLog `json:",omitempty"` - // NotBefore is the starting timestamp of the issued cert's validity period - NotBefore time.Time - // NotAfter is the ending timestamp of the issued cert's validity period - NotAfter time.Time - // RequestTime and ResponseTime are for tracking elapsed time during issuance - RequestTime time.Time - ResponseTime time.Time - // Error contains any encountered errors - Error string `json:",omitempty"` - // CertProfileName is a human readable name used to refer to the certificate - // profile. - CertProfileName string `json:",omitempty"` - // CertProfileHash is SHA256 sum over every exported field of an - // issuance.ProfileConfig, represented here as a hexadecimal string. - CertProfileHash string `json:",omitempty"` - // PreviousCertificateIssued is present when this certificate uses the same set - // of FQDNs as a previous certificate (from any account) and contains the - // notBefore of the most recent such certificate. - PreviousCertificateIssued time.Time - // UserAgent is the User-Agent header from the ACME client (provided to the - // RA via gRPC metadata). - UserAgent string -} - -// certificateRevocationEvent is a struct for holding information that is logged -// as JSON to the audit log as the result of a revocation event. -type certificateRevocationEvent struct { - ID string `json:",omitempty"` - // SerialNumber is the string representation of the revoked certificate's - // serial number. - SerialNumber string `json:",omitempty"` - // Reason is the integer representing the revocation reason used. - Reason revocation.Reason `json:"reason"` - // Method is the way in which revocation was requested. - // It will be one of the strings: "applicant", "subscriber", "control", "key", or "admin". - Method string `json:",omitempty"` - // Requester is the account ID of the requester. - // Will be zero for admin revocations. - Requester int64 `json:",omitempty"` - CRLShard int64 - // AdminName is the name of the admin requester. - // Will be zero for subscriber revocations. - AdminName string `json:",omitempty"` - // Error contains any error encountered during revocation. - Error string `json:",omitempty"` -} - -// finalizationCAACheckEvent is a struct for holding information logged as JSON -// to the info log as the result of an issuance event. It is logged when the RA -// performs the final CAA check of a certificate finalization request. -type finalizationCAACheckEvent struct { - // Requester is the associated account ID. - Requester int64 `json:",omitempty"` - // Reused is a count of Authz where the original CAA check was performed in - // the last 7 hours. - Reused int `json:",omitempty"` - // Rechecked is a count of Authz where a new CAA check was performed because - // the original check was older than 7 hours. - Rechecked int `json:",omitempty"` -} - // NewRegistration constructs a new Registration from a request. func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, request *corepb.Registration) (*corepb.Registration, error) { // Error if the request is nil, there is no account key or IP address @@ -653,7 +575,7 @@ func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( if !features.Get().CAARechecksFailOrder { // Check that the authzs either don't need CAA rechecking, or do the // necessary CAA rechecks right now. - err = ra.checkAuthorizationsCAA(ctx, int64(acctID), authzs, now) + err = ra.checkAuthorizationsCAA(ctx, authzs, now) if err != nil { return nil, err } @@ -667,10 +589,10 @@ func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( func validatedBefore(authz *core.Authorization, caaRecheckTime time.Time) (bool, error) { numChallenges := len(authz.Challenges) if numChallenges != 1 { - return false, berrors.InternalServerError("authorization has incorrect number of challenges. 1 expected, %d found for: id %s", numChallenges, authz.ID) + return false, berrors.InternalServerError("authorization has incorrect number of challenges. 1 expected, %d found for: id %d", numChallenges, authz.ID) } if authz.Challenges[0].Validated == nil { - return false, berrors.InternalServerError("authorization's challenge has no validated timestamp for: id %s", authz.ID) + return false, berrors.InternalServerError("authorization's challenge has no validated timestamp for: id %d", authz.ID) } return authz.Challenges[0].Validated.Before(caaRecheckTime), nil } @@ -681,7 +603,6 @@ func validatedBefore(authz *core.Authorization, caaRecheckTime time.Time) (bool, // be of type BoulderError. func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( ctx context.Context, - acctID int64, authzs map[identifier.ACMEIdentifier]*core.Authorization, now time.Time) error { if len(authzs) == 0 { @@ -721,12 +642,10 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( } } - caaEvent := &finalizationCAACheckEvent{ - Requester: acctID, - Reused: len(authzs) - len(recheckAuthzs), - Rechecked: len(recheckAuthzs), - } - ra.log.InfoObject("FinalizationCaaCheck", caaEvent) + ra.log.Info(ctx, "FinalizationCaaCheck", + slog.Int("reused", len(authzs)-len(recheckAuthzs)), + slog.Int("rechecked", len(recheckAuthzs)), + ) return nil } @@ -770,14 +689,15 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c Identifier: authz.Identifier.ToProto(), ValidationMethod: method, AccountURIID: authz.RegistrationID, - AuthzID: authz.ID, + AuthzID: fmt.Sprintf("%d", authz.ID), + AuthzIDInt: authz.ID, }) if err != nil { - ra.log.AuditErr("Rechecking CAA", err, map[string]any{ - "requester": authz.RegistrationID, - "identifier": authz.Identifier.Value, - "method": method, - }) + ra.log.AuditError(ctx, "Rechecking CAA", err, + blog.Acct(authz.RegistrationID), + blog.Idents(authz.Identifier), + slog.String("method", method), + ) err = berrors.InternalServerError( "Internal error rechecking CAA for authorization ID %v (%v)", authz.ID, authz.Identifier.Value, @@ -854,11 +774,11 @@ func (ra *RegistrationAuthorityImpl) failOrder( Error: order.Error, }) if err != nil { - ra.log.AuditErr("Persisting failed order", err, map[string]any{ - "requester": order.RegistrationID, - "order": order.Id, - "prob": order.Error.String(), - }) + ra.log.AuditError(ctx, "Persisting failed order", err, + blog.Acct(order.RegistrationID), + blog.Order(order.Id), + slog.String("prob", order.Error.String()), + ) } } @@ -881,14 +801,15 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap return nil, errIncompleteGRPCRequest } - logEvent := certificateRequestEvent{ - ID: core.NewToken(), - OrderID: req.Order.Id, - Requester: req.Order.RegistrationID, - RequestTime: ra.clk.Now(), - UserAgent: web.UserAgent(ctx), - } - csr, authzs, err := ra.validateFinalizeRequest(ctx, req, &logEvent) + ctx = blog.ContextWith(ctx, + slog.String("id", core.NewToken()), + blog.Acct(req.Order.RegistrationID), + blog.Order(req.Order.Id), + slog.Time("requestTime", ra.clk.Now()), + slog.String("ua", web.UserAgent(ctx)), + ) + + csr, authzs, err := ra.validateFinalizeRequest(ctx, req) if err != nil { return nil, err } @@ -923,7 +844,10 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap // Steps 3 (issuance) and 4 (cleanup) are done inside a helper function so // that we can control whether or not that work happens asynchronously. - if features.Get().AsyncFinalize { + // For MTC issuance we don't immediately go async: we wait on the MTCA + // sequencing an entry. This allows us to quickly return errors if sequencing + // is unavailable for any reason. + if features.Get().AsyncFinalize && !ra.isMTC(order) { // We do this work in a goroutine so that we can better handle latency from // getting SCTs and writing the (pre)certificate to the database. This lets // us return the order in the Processing state to the client immediately, @@ -938,22 +862,48 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), ra.finalizeTimeout) defer cancel() - _, err := ra.issueCertificateOuter(ctx, proto.Clone(order).(*corepb.Order), csr, authzs, logEvent) + _, err := ra.issueCertificateOuter(ctx, proto.Clone(order).(*corepb.Order), csr, authzs) if err != nil { // We only log here, because this is in a background goroutine with // no parent goroutine waiting for it to receive the error. - ra.log.AuditErr("Asynchronous finalization failed", err, map[string]any{ - "requester": order.RegistrationID, - "order": order.Id, - }) + ra.log.AuditError(ctx, "Asynchronous finalization failed", err) } }) return order, nil } else { - return ra.issueCertificateOuter(ctx, order, csr, authzs, logEvent) + return ra.issueCertificateOuter(ctx, order, csr, authzs) } } +func (ra *RegistrationAuthorityImpl) issueMTC( + ctx context.Context, + order *corepb.Order, + subjectPublicKeyInfo []byte, +) error { + profileName := ra.profileName(order) + mtca := ra.profileToMTCA[profileName] + if mtca == nil { + return fmt.Errorf("no MTCA configured for MTC profile %q", profileName) + } + + resp, err := mtca.Issue(ctx, &mtcapb.IssueRequest{ + Pubkey: subjectPublicKeyInfo, + Identifiers: order.Identifiers, + Profile: profileName, + }) + + if err != nil { + return fmt.Errorf("issuing MTC: %s", err) + } + + ra.log.Info(ctx, "issued MTC", + slog.String("logID", resp.MtcLogID), + slog.Int64("entryIndex", resp.MtcEntryIndex), + ) + + return nil +} + // containsMustStaple returns true if the provided set of extensions includes // an entry whose OID and value both match the expected values for the OCSP // Must-Staple (a.k.a. id-pe-tlsFeature) extension. @@ -978,10 +928,7 @@ func containsMustStaple(extensions []pkix.Extension) bool { // and ready for issuance. // // Returns a CertificateRequest, a map of identifiers to authorizations, and an error. -func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( - ctx context.Context, - req *rapb.FinalizeOrderRequest, - logEvent *certificateRequestEvent) ( +func (ra *RegistrationAuthorityImpl) validateFinalizeRequest(ctx context.Context, req *rapb.FinalizeOrderRequest) ( *x509.CertificateRequest, map[identifier.ACMEIdentifier]*core.Authorization, error) { if req.Order.Id <= 0 { return nil, nil, berrors.MalformedError("invalid order ID: %d", req.Order.Id) @@ -1069,31 +1016,11 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( return nil, nil, err } - // Collect up identifierLogs to log validation information for each identifier. - logIdents := make([]identifierLog, 0) - for ident, authz := range authzs { - // We know that at least one challenge is valid, because this was just - // confirmed by ra.checkOrderAuthorizations. - var solvedChall core.Challenge - for _, chall := range authz.Challenges { - if chall.Status == core.StatusValid { - solvedChall = chall - break - } - } - logIdents = append(logIdents, identifierLog{ - Ident: ident, - Authz: authz.ID, - Challenge: solvedChall.Type, - Validated: *solvedChall.Validated, - }) + // Track the age of used authzs. + for _, authz := range authzs { authzAge := (profile.validAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() ra.authzAges.WithLabelValues("FinalizeOrder", string(authz.Status)).Observe(authzAge) } - logEvent.Identifiers = logIdents - - // Mark that we verified the CN and SANs - logEvent.VerifiedFields = []string{"subject.commonName", "subjectAltName"} return csr, authzs, nil } @@ -1107,11 +1034,37 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( order *corepb.Order, csr *x509.CertificateRequest, authzs map[identifier.ACMEIdentifier]*core.Authorization, - logEvent certificateRequestEvent, ) (*corepb.Order, error) { ra.inflightFinalizes.Inc() defer ra.inflightFinalizes.Dec() + // Log the authzs used to validate this order. We log this now to reflect the + // fact that we *intended* to use these authzs to issue a cert, even if that + // issuance ends up failing. And even if that issuance does fail, this func + // can't exit without logging the attempt, so these authz logs will always + // correspond to (and carry the same `id` field as) a "Certificate request + // complete" line logged below. + for ident, authz := range authzs { + // We know that exactly one challenge is valid, because this was just + // confirmed by ra.checkOrderAuthorizations. + for _, chall := range authz.Challenges { + if chall.Status == core.StatusValid { + ra.log.AuditInfo(ctx, "Authz used for issuance", + blog.Authz(authz.ID), + blog.Idents(ident), + slog.String("method", string(chall.Type)), + slog.Time("validated", *chall.Validated), + ) + break + } + } + } + + // Mark that we verified the CN and SANs + ctx = blog.ContextWith(ctx, + slog.Any("verifiedFields", []string{"subject.commonName", "subjectAltName"}), + ) + idents := identifier.FromProtoSlice(order.Identifiers) isRenewal := false @@ -1125,20 +1078,26 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( } if len(timestamps.Timestamps) > 0 { isRenewal = true - logEvent.PreviousCertificateIssued = timestamps.Timestamps[0].AsTime() + ctx = blog.ContextWith(ctx, slog.Time("prevCertificateIssued", timestamps.Timestamps[0].AsTime())) } - profileName := order.CertificateProfileName - if profileName == "" { - profileName = ra.profiles.defaultName + if ra.isMTC(order) { + err := ra.issueMTC(ctx, order, csr.RawSubjectPublicKeyInfo) + if err != nil { + ra.failOrder(ctx, order, web.ProblemDetailsForError(err, "Error finalizing order")) + return nil, err + } + + ra.countCertificateIssued(ctx, order.RegistrationID, idents, isRenewal) + return order, nil } // Step 3: Issue the Certificate + profileName := ra.profileName(order) cert, err := ra.issueCertificateInner( ctx, csr, authzs, isRenewal, profileName, accountID(order.RegistrationID), orderID(order.Id)) // Step 4: Fail the order if necessary, and update metrics and log fields - var result string if err != nil { // The problem is computed using `web.ProblemDetailsForError`, the same // function the WFE uses to convert between `berrors` and problems. This @@ -1149,8 +1108,11 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( ra.failOrder(ctx, order, web.ProblemDetailsForError(err, "Error finalizing order")) order.Status = string(core.StatusInvalid) - logEvent.Error = err.Error() - result = "error" + ra.log.AuditInfo(ctx, "Certificate request - error", + slog.String("result", "error"), + slog.Time("responseTime", ra.clk.Now()), + blog.Error(err), + ) } else { order.CertificateSerial = core.SerialToString(cert.SerialNumber) order.Status = string(core.StatusValid) @@ -1161,21 +1123,33 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( ra.newCertCounter.Inc() - logEvent.SerialNumber = core.SerialToString(cert.SerialNumber) - logEvent.CommonName = cert.Subject.CommonName - logEvent.NotBefore = cert.NotBefore - logEvent.NotAfter = cert.NotAfter - logEvent.CertProfileName = profileName - - result = "successful" + ra.log.AuditInfo(ctx, "Certificate request - successful", + slog.String("result", "success"), + blog.Serial(core.SerialToString(cert.SerialNumber)), + slog.String("profile", profileName), + slog.String("commonName", cert.Subject.CommonName), + slog.Time("notBefore", cert.NotBefore), + slog.Time("notAfter", cert.NotAfter), + slog.Time("responseTime", ra.clk.Now()), + ) } - logEvent.ResponseTime = ra.clk.Now() - ra.log.AuditInfo(fmt.Sprintf("Certificate request - %s", result), logEvent) - return order, err } +func (ra *RegistrationAuthorityImpl) profileName(order *corepb.Order) string { + if order.CertificateProfileName == "" { + return ra.profiles.defaultName + } + return order.CertificateProfileName +} + +func (ra *RegistrationAuthorityImpl) isMTC(order *corepb.Order) bool { + profileName := ra.profileName(order) + profile := ra.profiles.byName[profileName] + return profile != nil && profile.mtc +} + // countCertificateIssued increments the certificates (per domain and per // account) and duplicate certificate rate limits. There is no reason to surface // errors from this function to the Subscriber, spends against these limit are @@ -1185,14 +1159,14 @@ func (ra *RegistrationAuthorityImpl) countCertificateIssued(ctx context.Context, if !isRenewal { txns, err := ra.txnBuilder.CertificatesPerDomainSpendOnlyTransactions(regId, orderIdents) if err != nil { - ra.log.Warningf("building rate limit transactions at finalize: %s", err) + ra.log.Warn(ctx, "building rate limit transactions at finalize", blog.Error(err)) } transactions = append(transactions, txns...) } txn, err := ra.txnBuilder.CertificatesPerFQDNSetSpendOnlyTransaction(orderIdents) if err != nil { - ra.log.Warningf("building rate limit transaction at finalize: %s", err) + ra.log.Warn(ctx, "building rate limit transaction at finalize", blog.Error(err)) } transactions = append(transactions, txn) @@ -1201,7 +1175,7 @@ func (ra *RegistrationAuthorityImpl) countCertificateIssued(ctx context.Context, if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return } - ra.log.Warningf("spending against rate limits at finalize: %s", err) + ra.log.Warn(ctx, "spending against rate limits at finalize", blog.Error(err)) } } @@ -1229,7 +1203,7 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner( if features.Get().CAARechecksFailOrder { // Check that the authzs either don't need CAA rechecking, or do the // necessary CAA rechecks right now. - err := ra.checkAuthorizationsCAA(ctx, int64(acctID), authzs, ra.clk.Now()) + err := ra.checkAuthorizationsCAA(ctx, authzs, ra.clk.Now()) if err != nil { return nil, err } @@ -1291,7 +1265,7 @@ func (ra *RegistrationAuthorityImpl) GetSCTs(ctx context.Context, sctRequest *ra // otherwise it will be a generic serverInternalError err = berrors.MissingSCTsError("failed to get SCTs: %s", err.Error()) } - ra.log.Warningf("ctpolicy.GetSCTs failed: %s", err) + ra.log.Warn(ctx, "ctpolicy.GetSCTs failed", blog.Error(err)) ra.ctpolicyResults.With(prometheus.Labels{"result": state}).Observe(took.Seconds()) return nil, err } @@ -1365,13 +1339,13 @@ func (ra *RegistrationAuthorityImpl) countFailedValidations(ctx context.Context, func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Context, regId int64, ident identifier.ACMEIdentifier) { txns, err := ra.txnBuilder.NewPausingResetTransactions(regId, ident) if err != nil { - ra.log.Warningf("building reset transaction for regID=[%d] identifier=[%s]: %s", regId, ident.Value, err) + ra.log.Warn(ctx, "building reset transaction", blog.Acct(regId), blog.Idents(ident), blog.Error(err)) return } err = ra.limiter.BatchReset(ctx, txns) if err != nil { - ra.log.Warningf("resetting bucket for regID=[%d] identifier=[%s]: %s", regId, ident.Value, err) + ra.log.Warn(ctx, "resetting bucket", blog.Acct(regId), blog.Idents(ident), blog.Error(err)) } } @@ -1410,7 +1384,8 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( // Clock for start of PerformValidation. vStart := ra.clk.Now() - if core.IsAnyNilOrZero(req.Authz, req.Authz.Id, req.Authz.Identifier, req.Authz.Status, req.Authz.Expires) { + // TODO(#8722): Re-add req.Authz.Id to this check once int64-only + if core.IsAnyNilOrZero(req.Authz, req.Authz.Identifier, req.Authz.Status, req.Authz.Expires) { return nil, errIncompleteGRPCRequest } @@ -1418,10 +1393,6 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( if err != nil { return nil, err } - authzID, err := strconv.ParseInt(authz.ID, 10, 64) - if err != nil { - return nil, err - } // Refuse to update expired authorizations if authz.Expires == nil || authz.Expires.Before(ra.clk.Now()) { @@ -1485,19 +1456,20 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( &vapb.PerformValidationRequest{ Identifier: authz.Identifier.ToProto(), Challenge: &corepb.Challenge{Type: string(ch.Type), Status: string(ch.Status), Token: ch.Token}, - Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID}, + Authz: &vapb.AuthzMeta{Id: fmt.Sprintf("%d", authz.ID), RegID: authz.RegistrationID, IdInt: authz.ID}, ExpectedKeyAuthorization: expectedKeyAuthorization, }, &vapb.IsCAAValidRequest{ Identifier: authz.Identifier.ToProto(), ValidationMethod: string(ch.Type), AccountURIID: authz.RegistrationID, - AuthzID: authz.ID, + AuthzID: fmt.Sprintf("%d", authz.ID), + AuthzIDInt: authz.ID, }, ) if err != nil { prob = bgrpc.ProblemDetailsToPB(probs.ServerInternal("Could not communicate with VA")) - ra.log.Errf("Failed to communicate with VA: %s", err) + ra.log.Error(ctx, "Failed to communicate with VA", err) } var status core.AcmeStatus @@ -1507,7 +1479,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( expires = *authz.Expires err := ra.countFailedValidations(ctx, authz.RegistrationID, authz.Identifier) if err != nil { - ra.log.Warningf("incrementing failed validations: %s", err) + ra.log.Warn(ctx, "incrementing failed validations", blog.Error(err)) } } else { status = core.StatusValid @@ -1518,7 +1490,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( } _, err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{ - Id: authzID, + Id: authz.ID, Status: string(status), Expires: timestamppb.New(expires), Attempted: string(ch.Type), @@ -1532,16 +1504,16 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( // parallel-validation race: a different validation attempt has already // updated this authz, so we failed to find a *pending* authz with the // given ID to update. - ra.log.InfoObject("Failed to record validation (likely parallel validation race)", map[string]any{ - "requester": authz.RegistrationID, - "authz": authz.ID, - "error": err.Error(), - }) + ra.log.Info(ctx, "Failed to record validation (likely parallel validation race)", + blog.Acct(authz.RegistrationID), + blog.Authz(authz.ID), + blog.Error(err), + ) } else { - ra.log.AuditErr("Failed to record validation (likely parallel validation race)", err, map[string]any{ - "requester": authz.RegistrationID, - "authz": authz.ID, - }) + ra.log.AuditError(ctx, "Failed to record validation (likely parallel validation race)", err, + blog.Acct(authz.RegistrationID), + blog.Authz(authz.ID), + ) } } }) @@ -1653,23 +1625,17 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, } serialString := core.SerialToString(cert.SerialNumber) + ctx = blog.ContextWith(ctx, blog.Acct(req.RegID), blog.Serial(serialString)) - logEvent := certificateRevocationEvent{ - ID: core.NewToken(), - SerialNumber: serialString, - Reason: reasonCode, - Method: "applicant", - Requester: req.RegID, - } - - // Below this point, do not re-declare `err` (i.e. type `err :=`) in a - // nested scope. Doing so will create a new `err` variable that is not - // captured by this closure. + // Below this point, do not re-declare `err` (i.e. type `err :=`) or `ctx` in + // a nested scope. Doing so will create a new variable that is not captured by + // this closure. defer func() { if err != nil { - logEvent.Error = err.Error() + ra.log.AuditError(ctx, "Revocation request", err) + } else { + ra.log.AuditInfo(ctx, "Revocation request") } - ra.log.AuditInfo("Revocation request", logEvent) }() metadata, err := ra.SA.GetSerialMetadata(ctx, &sapb.Serial{Serial: serialString}) @@ -1679,11 +1645,11 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, if req.RegID == metadata.RegistrationID { // The requester is the original subscriber. They can revoke for any reason. - logEvent.Method = "subscriber" + ctx = blog.ContextWith(ctx, slog.String("method", "subscriber")) } else { // The requester is a different account. We need to confirm that they have // authorizations for all names in the cert. - logEvent.Method = "control" + ctx = blog.ContextWith(ctx, slog.String("method", "control")) idents := identifier.FromCert(cert) var authzPB *sapb.Authorizations @@ -1713,9 +1679,9 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, // circumstances where "the certificate subscriber no longer owns the // domain names in the certificate". Override the reason code to match. reasonCode = revocation.CessationOfOperation - logEvent.Reason = reasonCode } + ctx = blog.ContextWith(ctx, slog.Int64("reason", int64(reasonCode))) err = ra.revokeCertificate(ctx, cert, reasonCode) if err != nil { return nil, err @@ -1798,22 +1764,21 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r return nil, err } - logEvent := certificateRevocationEvent{ - ID: core.NewToken(), - SerialNumber: core.SerialToString(cert.SerialNumber), - Reason: revocation.KeyCompromise, - Method: "key", - Requester: 0, - } + ctx = blog.ContextWith(ctx, + blog.Serial(core.SerialToString(cert.SerialNumber)), + slog.Int64("reason", int64(revocation.KeyCompromise)), + slog.String("method", "key"), + ) - // Below this point, do not re-declare `err` (i.e. type `err :=`) in a - // nested scope. Doing so will create a new `err` variable that is not - // captured by this closure. + // Below this point, do not re-declare `err` (i.e. type `err :=`) or `ctx` in + // a nested scope. Doing so will create a new variable that is not captured by + // this closure. defer func() { if err != nil { - logEvent.Error = err.Error() + ra.log.AuditError(ctx, "Revocation request", err) + } else { + ra.log.AuditInfo(ctx, "Revocation request") } - ra.log.AuditInfo("Revocation request", logEvent) }() // We revoke the cert before adding it to the blocked keys list, to avoid a @@ -1885,24 +1850,24 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte return nil, fmt.Errorf("cannot revoke malformed certificate for KeyCompromise") } - logEvent := certificateRevocationEvent{ - ID: core.NewToken(), - SerialNumber: req.Serial, - Reason: reasonCode, - CRLShard: req.CrlShard, - Method: "admin", - AdminName: req.AdminName, - } + ctx = blog.ContextWith(ctx, + blog.Serial(req.Serial), + slog.Int64("reason", int64(reasonCode)), + slog.String("method", "admin"), + slog.String("adminName", req.AdminName), + slog.Int64("crlShard", req.CrlShard), + ) - // Below this point, do not re-declare `err` (i.e. type `err :=`) in a - // nested scope. Doing so will create a new `err` variable that is not - // captured by this closure. + // Below this point, do not re-declare `err` (i.e. type `err :=`) or `ctx` in + // a nested scope. Doing so will create a new variable that is not captured by + // this closure. var err error defer func() { if err != nil { - logEvent.Error = err.Error() + ra.log.AuditError(ctx, "Revocation request", err) + } else { + ra.log.AuditInfo(ctx, "Revocation request") } - ra.log.AuditInfo("Revocation request", logEvent) }() var cert *x509.Certificate @@ -2010,14 +1975,23 @@ func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, req *corepb.Authorization) (*emptypb.Empty, error) { ident := identifier.FromProto(req.Identifier) - if core.IsAnyNilOrZero(req.Id, ident, req.Status, req.RegistrationID) { + if core.IsAnyNilOrZero(ident, req.Status, req.RegistrationID) { return nil, errIncompleteGRPCRequest } - authzID, err := strconv.ParseInt(req.Id, 10, 64) - if err != nil { - return nil, err + // TODO(#8722): Re-add req.Id to IsAnyNilOrZero check above, and cleanup following blocks when authz ids are int64-only + var authzIDInt int64 + if req.IdInt != 0 { + authzIDInt = req.IdInt + } else if req.Id != "" { + parsed, err := strconv.ParseInt(req.Id, 10, 64) + if err != nil { + return nil, fmt.Errorf("malformed gRPC request message field: %w", err) + } + authzIDInt = parsed + } else { + return nil, errIncompleteGRPCRequest } - if _, err := ra.SA.DeactivateAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}); err != nil { + if _, err := ra.SA.DeactivateAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzIDInt}); err != nil { return nil, err } if req.Status == string(core.StatusPending) { @@ -2026,7 +2000,7 @@ func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context // internal errors in the client. From our perspective this uses storage // resources similar to how failed authorizations do, so we increment the // failed authorizations limit. - err = ra.countFailedValidations(ctx, req.RegistrationID, ident) + err := ra.countFailedValidations(ctx, req.RegistrationID, ident) if err != nil { return nil, fmt.Errorf("failed to update rate limits: %w", err) } @@ -2186,7 +2160,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New !(features.Get().DNSAccount01Enabled && chall.Type == core.ChallengeTypeDNSAccount01) && !(features.Get().DNSPersist01Enabled && chall.Type == core.ChallengeTypeDNSPersist01) { return nil, berrors.InternalServerError( - "SA.GetAuthorizations returned a DNS wildcard authz (%s) with invalid challenge(s)", + "SA.GetAuthorizations returned a DNS wildcard authz (%d) with invalid challenge(s)", authz.ID, ) } @@ -2202,7 +2176,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New if err != nil { // This should never happen. return nil, berrors.InternalServerError( - "SA.GetAuthorizations returned a DNS wildcard authz (%s) with invalid challenge(s)", + "SA.GetAuthorizations returned a DNS wildcard authz (%d) with invalid challenge(s)", authz.ID, ) } @@ -2215,11 +2189,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // If we reached this point then the existing authz was acceptable for // reuse. - authzID, err := strconv.ParseInt(authz.ID, 10, 64) - if err != nil { - return nil, err - } - newOrderAuthzs = append(newOrderAuthzs, authzID) + newOrderAuthzs = append(newOrderAuthzs, authz.ID) ra.authzAges.WithLabelValues("NewOrder", string(authz.Status)).Observe(authzAge) } @@ -2258,7 +2228,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // An authz without an expiry is an unexpected internal server event if core.IsAnyNilOrZero(authz.Expires) { return nil, berrors.InternalServerError( - "SA.GetAuthorizations returned an authz (%s) with zero expiry", + "SA.GetAuthorizations returned an authz (%d) with zero expiry", authz.ID) } // If the reused authorization expires before the minExpiry, it's expiry diff --git a/ra/ra_test.go b/ra/ra_test.go index 1e6711d8b6b..2b6828047e1 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -21,7 +21,6 @@ import ( "net/netip" "regexp" "strconv" - "strings" "sync" "testing" "time" @@ -37,6 +36,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/allowlist" + "github.com/letsencrypt/boulder/blog" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" @@ -49,9 +49,9 @@ import ( bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" + mtcapb "github.com/letsencrypt/boulder/mtca/proto" "github.com/letsencrypt/boulder/policy" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" @@ -126,7 +126,7 @@ func createPendingAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, re ) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") - return getAuthorization(t, fmt.Sprint(res.V2Authorizations[0]), sa) + return getAuthorization(t, res.V2Authorizations[0], sa) } func createFinalizedAuthorization(t *testing.T, saClient sapb.StorageAuthorityClient, regID int64, ident identifier.ACMEIdentifier, exp time.Time, chall core.AcmeChallenge, attemptedAt time.Time) int64 { @@ -145,11 +145,9 @@ func createFinalizedAuthorization(t *testing.T, saClient sapb.StorageAuthorityCl return pendingID } -func getAuthorization(t *testing.T, id string, sa sapb.StorageAuthorityClient) *corepb.Authorization { +func getAuthorization(t *testing.T, id int64, sa sapb.StorageAuthorityClient) *corepb.Authorization { t.Helper() - idInt, err := strconv.ParseInt(id, 10, 64) - test.AssertNotError(t, err, "strconv.ParseInt failed") - dbAuthz, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: idInt}) + dbAuthz, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: id}) test.AssertNotError(t, err, "Could not fetch authorization from database") return dbAuthz } @@ -204,7 +202,7 @@ func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req Identifier: req.Identifier, ValidationMethod: req.Challenge.Type, AccountURIID: req.Authz.RegID, - AuthzID: req.Authz.Id, + AuthzIDInt: req.Authz.IdInt, }) if err != nil { return nil, err @@ -279,8 +277,6 @@ var ( ExampleCSR = &x509.CertificateRequest{} Identifier = "not-example.com" - - log = blog.UseMock() ) var ctx = context.Background() @@ -299,6 +295,8 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho err = json.Unmarshal(ShortKeyJSON, &ShortKey) test.AssertNotError(t, err, "Failed to unmarshal JWK") + log := blog.NewMock() + fc := clock.NewFake() // Set to some non-zero time. fc.Set(time.Date(2020, 3, 4, 5, 0, 0, 0, time.UTC)) @@ -379,10 +377,12 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho }) test.AssertNotError(t, err, "making validation profiles") + profileToMTCA := make(map[string]mtcapb.MTCAClient) + ra := NewRegistrationAuthorityImpl( fc, log, stats, 1, testKeyPolicy, limiter, txnBuilder, - profiles, nil, 5*time.Minute, ctp, nil) + profiles, nil, 5*time.Minute, ctp, nil, profileToMTCA) ra.SA = sa ra.VA = va ra.CA = ca @@ -484,7 +484,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) { // Create a finalized authorization exp := ra.clk.Now().Add(365 * 24 * time.Hour) authz := core.Authorization{ - ID: "1337", + ID: 1337, Identifier: identifier.NewDNS("not-example.com"), RegistrationID: registration.Id, Status: "valid", @@ -573,7 +573,7 @@ func TestPerformValidationSuccess(t *testing.T) { // Sleep so the RA has a chance to write to the SA time.Sleep(100 * time.Millisecond) - dbAuthzPB := getAuthorization(t, authzPB.Id, sa) + dbAuthzPB := getAuthorization(t, authzPB.IdInt, sa) t.Log("dbAuthz:", dbAuthzPB) // Verify that the responses are reflected @@ -789,7 +789,7 @@ func TestPerformValidationVAError(t *testing.T) { // Sleep so the RA has a chance to write to the SA time.Sleep(100 * time.Millisecond) - dbAuthzPB := getAuthorization(t, authzPB.Id, sa) + dbAuthzPB := getAuthorization(t, authzPB.IdInt, sa) t.Log("dbAuthz:", dbAuthzPB) // Verify that the responses are reflected @@ -850,12 +850,21 @@ func TestDeactivateAuthorization(t *testing.T) { exp := ra.clk.Now().Add(365 * 24 * time.Hour) authzID := createFinalizedAuthorization(t, sa, registration.Id, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) - dbAuthzPB := getAuthorization(t, fmt.Sprint(authzID), sa) + dbAuthzPB := getAuthorization(t, authzID, sa) _, err := ra.DeactivateAuthorization(ctx, dbAuthzPB) test.AssertNotError(t, err, "Could not deactivate authorization") deact, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "Could not get deactivated authorization with ID "+dbAuthzPB.Id) test.AssertEquals(t, deact.Status, string(core.StatusDeactivated)) + + dbAuthzPBIdChecks := dbAuthzPB + dbAuthzPBIdChecks.Id = fmt.Sprintf("%d", authzID) + dbAuthzPBIdChecks.IdInt = authzID + _, err = ra.DeactivateAuthorization(ctx, dbAuthzPBIdChecks) + test.AssertNotError(t, err, "Could not deactivate authorization") + deact, err = sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) + test.AssertNotError(t, err, "Could not get deactivated authorization with ID "+dbAuthzPBIdChecks.Id) + test.AssertEquals(t, deact.Status, string(core.StatusDeactivated)) } type mockSARecordingPauses struct { @@ -1002,7 +1011,7 @@ func (cr *caaRecorder) DoCAA( // Test that the right set of domain names have their CAA rechecked, based on // their `Validated` (attemptedAt in the database) timestamp. func TestRecheckCAADates(t *testing.T) { - _, _, ra, _, fc, registration, cleanUp := initAuthorities(t) + _, _, ra, _, fc, _, cleanUp := initAuthorities(t) defer cleanUp() recorder := &caaRecorder{names: make(map[string]bool)} ra.VA = va.RemoteClients{CAAClient: recorder} @@ -1077,7 +1086,7 @@ func TestRecheckCAADates(t *testing.T) { } twoChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("twochallenges.com"): { - ID: "twochal", + ID: 13372, Identifier: identifier.NewDNS("twochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{ @@ -1098,7 +1107,7 @@ func TestRecheckCAADates(t *testing.T) { } noChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("nochallenges.com"): { - ID: "nochal", + ID: 13370, Identifier: identifier.NewDNS("nochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{}, @@ -1106,7 +1115,7 @@ func TestRecheckCAADates(t *testing.T) { } noValidationTime := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("novalidationtime.com"): { - ID: "noval", + ID: 13371, Identifier: identifier.NewDNS("novalidationtime.com"), Expires: &recentExpires, Challenges: []core.Challenge{ @@ -1122,23 +1131,23 @@ func TestRecheckCAADates(t *testing.T) { // NOTE: The names provided here correspond to authorizations in the // `mockSAWithRecentAndOlder` - err := ra.checkAuthorizationsCAA(context.Background(), registration.Id, authzs, fc.Now()) + err := ra.checkAuthorizationsCAA(context.Background(), authzs, fc.Now()) // We expect that there is no error rechecking authorizations for these names if err != nil { t.Errorf("expected nil err, got %s", err) } // Should error if a authorization has `!= 1` challenge - err = ra.checkAuthorizationsCAA(context.Background(), registration.Id, twoChallenges, fc.Now()) - test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 2 found for: id twochal") + err = ra.checkAuthorizationsCAA(context.Background(), twoChallenges, fc.Now()) + test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 2 found for: id 13372") // Should error if a authorization has `!= 1` challenge - err = ra.checkAuthorizationsCAA(context.Background(), registration.Id, noChallenges, fc.Now()) - test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 0 found for: id nochal") + err = ra.checkAuthorizationsCAA(context.Background(), noChallenges, fc.Now()) + test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 0 found for: id 13370") // Should error if authorization's challenge has no validated timestamp - err = ra.checkAuthorizationsCAA(context.Background(), registration.Id, noValidationTime, fc.Now()) - test.AssertEquals(t, err.Error(), "authorization's challenge has no validated timestamp for: id noval") + err = ra.checkAuthorizationsCAA(context.Background(), noValidationTime, fc.Now()) + test.AssertEquals(t, err.Error(), "authorization's challenge has no validated timestamp for: id 13371") // We expect that "recent.com" is not checked because its mock authorization // isn't expired @@ -1313,7 +1322,7 @@ func TestRecheckCAAInternalServerError(t *testing.T) { } func TestRecheckSkipIPAddress(t *testing.T) { - _, _, ra, _, fc, registration, cleanUp := initAuthorities(t) + _, _, ra, _, fc, _, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} ident := identifier.NewIP(netip.MustParseAddr("127.0.0.1")) @@ -1333,12 +1342,12 @@ func TestRecheckSkipIPAddress(t *testing.T) { }, }, } - err := ra.checkAuthorizationsCAA(context.Background(), registration.Id, authzs, fc.Now()) + err := ra.checkAuthorizationsCAA(context.Background(), authzs, fc.Now()) test.AssertNotError(t, err, "rechecking CAA for IP address, should have skipped") } func TestRecheckInvalidIdentifierType(t *testing.T) { - _, _, ra, _, fc, registration, cleanUp := initAuthorities(t) + _, _, ra, _, fc, _, cleanUp := initAuthorities(t) defer cleanUp() ident := identifier.ACMEIdentifier{ Type: "fnord", @@ -1360,7 +1369,7 @@ func TestRecheckInvalidIdentifierType(t *testing.T) { }, }, } - err := ra.checkAuthorizationsCAA(context.Background(), registration.Id, authzs, fc.Now()) + err := ra.checkAuthorizationsCAA(context.Background(), authzs, fc.Now()) test.AssertError(t, err, "expected err, got nil") test.AssertErrorIs(t, err, berrors.Malformed) test.AssertContains(t, err.Error(), "invalid identifier type") @@ -1954,7 +1963,7 @@ func (msa *mockSAWithAuthzs) GetValidAuthorizations2(ctx context.Context, req *s func (msa *mockSAWithAuthzs) GetAuthorization2(ctx context.Context, req *sapb.AuthorizationID2, _ ...grpc.CallOption) (*corepb.Authorization, error) { for _, authz := range msa.authzs { - if authz.ID == fmt.Sprintf("%d", req.Id) { + if authz.ID == req.Id { return bgrpc.AuthzToPB(*authz) } } @@ -2073,7 +2082,7 @@ func TestNewOrderAuthzReuseSafety(t *testing.T) { ra.SA = &mockSAWithAuthzs{ authzs: []*core.Authorization{ { - ID: "1", + ID: 1, Identifier: identifier.NewDNS("*.zombo.com"), RegistrationID: registration.Id, Status: core.StatusValid, @@ -2294,7 +2303,7 @@ func TestNewOrderExpiry(t *testing.T) { authzs: []*core.Authorization{ { // A static fake ID we can check for in a unit test - ID: "1", + ID: 1, Identifier: identifier.NewDNS("zombo.com"), RegistrationID: registration.Id, Expires: &fakeAuthzExpires, @@ -2993,7 +3002,6 @@ func TestIssueCertificateAuditLog(t *testing.T) { parsedCerts, err := x509.ParseCertificates(cert) test.AssertNotError(t, err, "Failed to parse mock cert DER bytes") test.AssertEquals(t, len(parsedCerts), 1) - parsedCert := parsedCerts[0] // Cast the RA's mock log so we can ensure its cleared and can access the // matched log lines @@ -3009,54 +3017,43 @@ func TestIssueCertificateAuditLog(t *testing.T) { test.AssertNotError(t, err, "Error finalizing test order") // Get the logged lines from the audit logger - loglines := mockLog.GetAllMatching("Certificate request - successful JSON=") + loglines := mockLog.GetAllMatching("Certificate request - ") // There should be exactly 1 matching log line test.AssertEquals(t, len(loglines), 1) - // Strip away the stuff before 'JSON=' - jsonContent := strings.TrimPrefix(loglines[0], "INFO: [AUDIT] Certificate request - successful JSON=") - - // Unmarshal the JSON into a certificate request event object - var event certificateRequestEvent - err = json.Unmarshal([]byte(jsonContent), &event) - // The JSON should unmarshal without error - test.AssertNotError(t, err, "Error unmarshalling logged JSON issuance event") + t.Log(loglines[0]) + // The event should have no error - test.AssertEquals(t, event.Error, "") + test.AssertNotContains(t, loglines[0], "err=") // The event requester should be the expected reg ID - test.AssertEquals(t, event.Requester, registration.Id) + test.AssertContains(t, loglines[0], fmt.Sprintf("acct=%d", registration.Id)) // The event order ID should be the expected order ID - test.AssertEquals(t, event.OrderID, order.Id) + test.AssertContains(t, loglines[0], fmt.Sprintf("order=%d", order.Id)) // The event serial number should be the expected serial number - test.AssertEquals(t, event.SerialNumber, core.SerialToString(template.SerialNumber)) + test.AssertContains(t, loglines[0], fmt.Sprintf("serial=%s", core.SerialToString(template.SerialNumber))) // The event verified fields should be the expected value - test.AssertDeepEquals(t, event.VerifiedFields, []string{"subject.commonName", "subjectAltName"}) + test.AssertContains(t, loglines[0], "verifiedFields=\"[subject.commonName subjectAltName]\"") // The event CommonName should match the expected common name - test.AssertEquals(t, event.CommonName, "not-example.com") - // The event's NotBefore and NotAfter should match the cert's - test.AssertEquals(t, event.NotBefore, parsedCert.NotBefore) - test.AssertEquals(t, event.NotAfter, parsedCert.NotAfter) + test.AssertContains(t, loglines[0], "commonName=not-example.com") - // There should be one event identifier/authz entry for each name. - test.AssertEquals(t, len(event.Identifiers), len(names)) + // The event's NotBefore and NotAfter should match the cert's + // TODO(https://github.com/golang/go/issues/78215): Restore these checks + // when slog's time formatting is fixed to match Time.Format(). + // test.AssertContains(t, loglines[0], fmt.Sprintf("notBefore=%s", parsedCerts[0].NotBefore.Format(time.RFC3339Nano))) + // test.AssertContains(t, loglines[0], fmt.Sprintf("notAfter=%s", parsedCerts[0].NotAfter.Format(time.RFC3339Nano))) - // The event identifiers should match the order identifiers - eventIdents := make([]identifier.ACMEIdentifier, 0) - for _, eventIdent := range event.Identifiers { - eventIdents = append(eventIdents, eventIdent.Ident) - } - test.AssertDeepEquals(t, identifier.Normalize(eventIdents), identifier.Normalize(identifier.FromProtoSlice(order.Identifiers))) + // Now do the same for each identifier/authz in the cert. + loglines = mockLog.GetAllMatching("Authz used for issuance") + test.AssertEquals(t, len(loglines), len(idents)) - // Check the identifier/authz entry for each name for i, name := range names { - for _, entry := range event.Identifiers { - if entry.Ident.Value == name { - // The authz entry should have the correct authz ID - test.AssertEquals(t, entry.Authz, fmt.Sprintf("%d", authzIDs[i])) - // The authz entry should have the correct challenge type - test.AssertEquals(t, entry.Challenge, challs[i]) - } - } + loglines = mockLog.GetAllMatching(fmt.Sprintf("Authz used for issuance.*Value:%s", name)) + test.AssertEquals(t, len(loglines), 1) + + // The authz entry should have the correct authz ID + test.AssertContains(t, loglines[0], fmt.Sprintf("authz=%d", authzIDs[i])) + // The authz entry should have the correct challenge type + test.AssertContains(t, loglines[0], fmt.Sprintf("method=%s", challs[i])) } } @@ -3145,25 +3142,17 @@ func TestIssueCertificateCAACheckLog(t *testing.T) { test.AssertNotError(t, err, "Error finalizing test order") // Get the logged lines from the mock logger. - loglines := mockLog.GetAllMatching("FinalizationCaaCheck JSON=") + loglines := mockLog.GetAllMatching("FinalizationCaaCheck") // There should be exactly 1 matching log line. test.AssertEquals(t, len(loglines), 1) - // Strip away the stuff before 'JSON='. - jsonContent := strings.TrimPrefix(loglines[0], "INFO: FinalizationCaaCheck JSON=") - - // Unmarshal the JSON into an event object. - var event finalizationCAACheckEvent - err = json.Unmarshal([]byte(jsonContent), &event) - // The JSON should unmarshal without error. - test.AssertNotError(t, err, "Error unmarshalling logged JSON issuance event.") // The event requester should be the expected registration ID. - test.AssertEquals(t, event.Requester, registration.Id) + test.AssertContains(t, loglines[0], fmt.Sprintf("acct=%d", registration.Id)) // The event should have the expected number of Authzs where CAA was reused. - test.AssertEquals(t, event.Reused, 2) + test.AssertContains(t, loglines[0], "reused=2") // The event should have the expected number of Authzs where CAA was // rechecked. - test.AssertEquals(t, event.Rechecked, 2) + test.AssertContains(t, loglines[0], "rechecked=2") } func TestPerformValidationBadChallengeType(t *testing.T) { @@ -3175,7 +3164,7 @@ func TestPerformValidationBadChallengeType(t *testing.T) { exp := fc.Now().Add(10 * time.Hour) authz := core.Authorization{ - ID: "1337", + ID: 1337, Identifier: identifier.NewDNS("not-example.com"), RegistrationID: 1, Status: "valid", @@ -3213,7 +3202,7 @@ func TestCTPolicyMeasurements(t *testing.T) { ra.ctpolicy = ctpolicy.New(&timeoutPub{}, loglist.List{ {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, - }, nil, nil, 0, log, metrics.NoopRegisterer) + }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) _, cert := test.ThrowAwayCert(t, clock.NewFake()) _, err := ra.GetSCTs(context.Background(), &rapb.SCTRequest{ @@ -3333,7 +3322,7 @@ func TestIssueCertificateOuter(t *testing.T) { CertificateProfileName: tc.profile, } - order, err = ra.issueCertificateOuter(context.Background(), order, csr, nil, certificateRequestEvent{}) + order, err = ra.issueCertificateOuter(context.Background(), order, csr, nil) // The resulting order should have new fields populated if order.Status != string(core.StatusValid) { @@ -3613,7 +3602,10 @@ func (msa *mockSARevocationWithAuthzs) GetValidAuthorizations2(ctx context.Conte } for _, ident := range req.Identifiers { - authzs.Authzs = append(authzs.Authzs, &corepb.Authorization{Identifier: ident}) + authzs.Authzs = append(authzs.Authzs, &corepb.Authorization{ + IdInt: mrand.Int64(), + Identifier: ident, + }) } return authzs, nil @@ -3943,7 +3935,7 @@ func TestGetAuthorization(t *testing.T) { ra.SA = &mockSAWithAuthzs{ authzs: []*core.Authorization{ { - ID: "1", + ID: 1, Identifier: identifier.NewDNS("example.com"), Status: "valid", Challenges: []core.Challenge{ diff --git a/ratelimits/limit.go b/ratelimits/limit.go index 09382aac7ba..82668a47ad4 100644 --- a/ratelimits/limit.go +++ b/ratelimits/limit.go @@ -15,10 +15,10 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/strictyaml" ) @@ -386,7 +386,7 @@ func (l *limitRegistry) loadOverrides(ctx context.Context) error { if len(newOverrides) < 1 { // If it's an empty set, don't replace any current overrides. - l.logger.Warning("loading overrides: no valid overrides") + l.logger.Warn(ctx, "loading overrides: no valid overrides") return nil } @@ -430,7 +430,7 @@ func (l *limitRegistry) loadOverridesWithRetry(ctx context.Context) error { if err == nil { return nil } - l.logger.Errf("loading overrides: %v", err) + l.logger.Error(ctx, "loading overrides", err) retries++ select { case <-time.After(core.RetryBackoff(retries, time.Second/6, time.Second*15, 2)): @@ -448,7 +448,7 @@ func (l *limitRegistry) NewRefresher(interval time.Duration) context.CancelFunc go func() { err := l.loadOverridesWithRetry(ctx) if err != nil { - l.logger.Errf("loading overrides (initial): %v", err) + l.logger.Error(ctx, "loading overrides (initial)", err) } ticker := time.NewTicker(interval) @@ -458,7 +458,7 @@ func (l *limitRegistry) NewRefresher(interval time.Duration) context.CancelFunc case <-ticker.C: err := l.loadOverridesWithRetry(ctx) if err != nil { - l.logger.Errf("loading overrides (refresh): %v", err) + l.logger.Error(ctx, "loading overrides (refresh)", err) } case <-ctx.Done(): return diff --git a/ratelimits/limit_test.go b/ratelimits/limit_test.go index 933fbff1f55..71af852fc68 100644 --- a/ratelimits/limit_test.go +++ b/ratelimits/limit_test.go @@ -7,7 +7,6 @@ import ( "net/netip" "os" "path/filepath" - "slices" "strings" "testing" "time" @@ -15,10 +14,10 @@ import ( "github.com/prometheus/client_golang/prometheus" io_prometheus_client "github.com/prometheus/client_model/go" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) @@ -305,7 +304,7 @@ func TestLoadOverrides(t *testing.T) { return Limits{}, nil } err = tb.limitRegistry.loadOverrides(context.Background()) - test.AssertEquals(t, mockLog.GetAll()[0], "WARNING: loading overrides: no valid overrides") + test.AssertContains(t, mockLog.GetAll()[0], `level=WARN msg="loading overrides: no valid overrides"`) test.AssertNotError(t, err, "load empty overrides") test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides) } @@ -314,8 +313,8 @@ func TestNewRefresher(t *testing.T) { mockLog := blog.NewMock() reg := &limitRegistry{ - refreshOverrides: func(_ context.Context, _ prometheus.Gauge, logger blog.Logger) (Limits, error) { - logger.Info("refreshed") + refreshOverrides: func(ctx context.Context, _ prometheus.Gauge, logger blog.Logger) (Limits, error) { + logger.Info(ctx, "refreshed") return nil, nil }, logger: mockLog, @@ -326,18 +325,20 @@ func TestNewRefresher(t *testing.T) { time.Sleep(time.Millisecond * 20) // The refresher should have run once, but then been cancelled before the // first tick. - test.AssertDeepEquals(t, mockLog.GetAll(), []string{"INFO: refreshed", "WARNING: loading overrides: no valid overrides"}) + logs := mockLog.GetAll() + test.AssertEquals(t, len(logs), 2) + test.AssertContains(t, logs[0], "level=INFO msg=refreshed") + test.AssertContains(t, logs[1], `level=WARN msg="loading overrides: no valid overrides"`) - reg.NewRefresher(time.Nanosecond) + reg.NewRefresher(time.Millisecond) retries := 0 for retries < 5 { - if slices.Contains(mockLog.GetAll(), "INFO: refreshed") { + if len(mockLog.GetAllMatching("refreshed")) >= 1 { break } retries++ time.Sleep(core.RetryBackoff(retries, time.Millisecond*2, time.Millisecond*50, 2)) } - test.AssertSliceContains(t, mockLog.GetAll(), "INFO: refreshed") test.Assert(t, len(mockLog.GetAll()) > 1, "refresher didn't run more than once") } diff --git a/ratelimits/limiter_test.go b/ratelimits/limiter_test.go index 47427e3c4c5..d5395730f2e 100644 --- a/ratelimits/limiter_test.go +++ b/ratelimits/limiter_test.go @@ -10,9 +10,9 @@ import ( "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) diff --git a/ratelimits/transaction.go b/ratelimits/transaction.go index 80f963be78a..17bdaa408b5 100644 --- a/ratelimits/transaction.go +++ b/ratelimits/transaction.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/netip" "strconv" "time" @@ -15,10 +16,10 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -235,7 +236,10 @@ func NewTransactionBuilderFromDatabase(defaults string, overrides GetOverridesFu err = ValidateLimit(override) if err != nil { - logger.Errf("hydrating %s override with key %q: %s", override.Name.String(), resp.Override.BucketKey, err) + logger.Error(ctx, "Hydrating override", err, + slog.String("limit", override.Name.String()), + slog.String("bucketKey", resp.Override.BucketKey), + ) errorCount++ continue } diff --git a/ratelimits/transaction_test.go b/ratelimits/transaction_test.go index 76d1dad71ae..8fad706f762 100644 --- a/ratelimits/transaction_test.go +++ b/ratelimits/transaction_test.go @@ -14,10 +14,10 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" sapb "github.com/letsencrypt/boulder/sa/proto" @@ -306,7 +306,7 @@ func TestNewTransactionBuilderFromDatabase(t *testing.T) { joinWithColon(CertificatesPerDomain.EnumString(), "example.com"): {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true}, joinWithColon(CertificatesPerDomain.EnumString(), "example.net"): {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true}, }, - expectLog: fmt.Sprintf("ERR: hydrating CertificatesPerDomain override with key %q: invalid burst '0', must be > 0", joinWithColon(CertificatesPerDomain.EnumString(), "bad-example.com")), + expectLog: fmt.Sprintf(`msg="Hydrating override" limit=CertificatesPerDomain bucketKey=%s error="invalid burst '0', must be > 0"`, joinWithColon(CertificatesPerDomain.EnumString(), "bad-example.com")), expectOverrideErrors: 4, }, } @@ -326,7 +326,10 @@ func TestNewTransactionBuilderFromDatabase(t *testing.T) { test.AssertNotError(t, err, tc.name) if tc.expectLog != "" { - test.AssertSliceContains(t, mockLog.GetAll(), tc.expectLog) + got := mockLog.GetAllMatching(tc.expectLog) + if len(got) == 0 { + t.Errorf("Expected log line containing %q, got: %v", tc.expectLog, mockLog.GetAll()) + } } for bucketKey, limit := range tc.expectOverrides { diff --git a/redis/config.go b/redis/config.go index c858a4beb1b..0e0a35f86bf 100644 --- a/redis/config.go +++ b/redis/config.go @@ -7,9 +7,9 @@ import ( "github.com/redis/go-redis/extra/redisotel/v9" "github.com/redis/go-redis/v9" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" - blog "github.com/letsencrypt/boulder/log" ) // Config contains the configuration needed to act as a Redis client. diff --git a/redis/lookup.go b/redis/lookup.go index 2621306b277..6750fcfcf70 100644 --- a/redis/lookup.go +++ b/redis/lookup.go @@ -4,14 +4,16 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "strings" "time" - "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/blog" + "github.com/letsencrypt/boulder/cmd" + "github.com/redis/go-redis/v9" ) @@ -98,7 +100,7 @@ func newLookup(srvLookups []cmd.ServiceDomain, dnsAuthority string, frequency ti if tempErr != nil { // Log and discard temporary errors, as they're likely to be transient // (e.g. network connectivity issues). - logger.Warningf("resolving ring shards: %s", tempErr) + logger.Warn(ctx, "Resolving ring shards", blog.Error(tempErr)) } if nonTempErr != nil && errors.Is(nonTempErr, ErrNoShardsResolved) { // Non-temporary errors are always logged inside of updateNow(), so we @@ -125,7 +127,8 @@ func (look *lookup) updateNow(ctx context.Context) (tempError, nonTempError erro } // Log non-temporary DNS errors as they occur, as they're likely to be // indicative of misconfiguration. - look.logger.Errf("resolving service _%s._tcp.%s: %s", srv.Service, srv.Domain, err) + look.logger.Error(ctx, "Resolving service", err, + slog.String("name", fmt.Sprintf("_%s._tcp.%s", srv.Service, srv.Domain))) } nextAddrs := make(map[string]string) @@ -198,11 +201,11 @@ func (look *lookup) start() { tempErrs, nonTempErrs := look.updateNow(timeoutCtx) cancel() if tempErrs != nil { - look.logger.Warningf("resolving ring shards, temporary errors: %s", tempErrs) + look.logger.Warn(timeoutCtx, "Temporary error while resolving ring shards", blog.Error(tempErrs)) continue } if nonTempErrs != nil { - look.logger.Errf("resolving ring shards, non-temporary errors: %s", nonTempErrs) + look.logger.Error(timeoutCtx, "Error while resolving ring shards", nonTempErrs) continue } diff --git a/redis/lookup_test.go b/redis/lookup_test.go index d81870c9fe4..261205bad4b 100644 --- a/redis/lookup_test.go +++ b/redis/lookup_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" diff --git a/sa/database.go b/sa/database.go index c5b09532831..9a2c494c442 100644 --- a/sa/database.go +++ b/sa/database.go @@ -1,6 +1,7 @@ package sa import ( + "context" "database/sql" "fmt" "time" @@ -10,10 +11,10 @@ import ( "github.com/letsencrypt/borp" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" boulderDB "github.com/letsencrypt/boulder/db" - blog "github.com/letsencrypt/boulder/log" ) // DbSettings contains settings for the database/sql driver. The zero @@ -231,7 +232,7 @@ type SQLLogger struct { // Printf adapts the Logger to borp's interface func (log *SQLLogger) Printf(format string, v ...any) { - log.Debugf(format, v...) + log.Debug(context.Background(), fmt.Sprintf(format, v...)) } // initTables constructs the table map for the ORM. diff --git a/sa/db/01-boulder_sa_next.sql b/sa/db/01-boulder_sa_next.sql index 30b8e291cba..b981eb03cb0 100644 --- a/sa/db/01-boulder_sa_next.sql +++ b/sa/db/01-boulder_sa_next.sql @@ -249,6 +249,5 @@ ALTER TABLE `certificateStatus` DROP COLUMN `LockCol`; ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`); ALTER TABLE `orders` - ADD COLUMN `isMTC` bool NOT NULL DEFAULT FALSE, ADD COLUMN `mtcLogID` varchar(255) DEFAULT NULL, ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL; diff --git a/sa/db/01-mtca.sql b/sa/db/01-mtcmeta_44947_4_1_0_44.sql similarity index 100% rename from sa/db/01-mtca.sql rename to sa/db/01-mtcmeta_44947_4_1_0_44.sql diff --git a/sa/db/02-users.sql b/sa/db/02-users.sql index 436d1ef7e3b..19f9840398f 100644 --- a/sa/db/02-users.sql +++ b/sa/db/02-users.sql @@ -94,3 +94,13 @@ GRANT CREATE,SELECT,INSERT ON * TO 'incidents_sa_admin'@'%'; -- Test setup and teardown GRANT ALL PRIVILEGES ON * to 'test_setup'@'%'; + +USE mtcmeta_44947_4_1_0_44; + +CREATE USER IF NOT EXISTS 'mtpublisher'@'%'; + +-- MTPublisher stub: reads checkpoints awaiting a cosignature and writes one. +GRANT SELECT,UPDATE ON checkpoints TO 'mtpublisher'@'%'; + +-- Test setup and teardown +GRANT ALL PRIVILEGES ON * to 'test_setup'@'%'; diff --git a/sa/db/02-users_next.sql b/sa/db/02-users_next.sql index 9d5237a9326..986a7110b59 100644 --- a/sa/db/02-users_next.sql +++ b/sa/db/02-users_next.sql @@ -94,3 +94,13 @@ GRANT CREATE,SELECT,INSERT ON * TO 'incidents_sa_admin'@'%'; -- Test setup and teardown GRANT ALL PRIVILEGES ON * to 'test_setup'@'%'; + +USE mtcmeta_44947_4_1_0_44; + +CREATE USER IF NOT EXISTS 'mtpublisher'@'%'; + +-- MTPublisher stub: reads checkpoints awaiting a cosignature and writes one. +GRANT SELECT,UPDATE ON checkpoints TO 'mtpublisher'@'%'; + +-- Test setup and teardown +GRANT ALL PRIVILEGES ON * to 'test_setup'@'%'; diff --git a/sa/model.go b/sa/model.go index 8a60718c71c..86fd0303347 100644 --- a/sa/model.go +++ b/sa/model.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "google.golang.org/protobuf/proto" "math" "net/netip" "net/url" @@ -18,6 +17,7 @@ import ( "time" "github.com/go-jose/go-jose/v4" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -638,15 +638,16 @@ func authzPBToModel(authz *corepb.Authorization) (*authzModel, error) { profile := authz.CertificateProfileName am.CertificateProfileName = &profile } - if authz.Id != "" { - // The v1 internal authorization objects use a string for the ID, the v2 - // storage format uses a integer ID. In order to maintain compatibility we - // convert the integer ID to a string. + if authz.IdInt != 0 { + am.ID = authz.IdInt + } else if authz.Id != "" { id, err := strconv.Atoi(authz.Id) if err != nil { return nil, err } am.ID = int64(id) + } else { + return nil, errors.New("authorization is missing an ID value") } if hasMultipleNonPendingChallenges(authz.Challenges) { return nil, errors.New("multiple challenges are non-pending") @@ -782,6 +783,7 @@ func modelToAuthzPB(am authzModel) (*corepb.Authorization, error) { pb := &corepb.Authorization{ Id: fmt.Sprintf("%d", am.ID), + IdInt: am.ID, Status: string(uintToStatus[am.Status]), Identifier: identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue}.ToProto(), RegistrationID: am.RegistrationID, diff --git a/sa/model_test.go b/sa/model_test.go index 73ef55a180f..50901aeaae1 100644 --- a/sa/model_test.go +++ b/sa/model_test.go @@ -64,7 +64,7 @@ func TestAuthzModel(t *testing.T) { // customize them after calling this. newTestAuthzPB := func(validated time.Time) *corepb.Authorization { return &corepb.Authorization{ - Id: "1", + IdInt: 1, Identifier: identifier.NewDNS("example.com").ToProto(), RegistrationID: 1, Status: string(core.StatusValid), @@ -114,6 +114,42 @@ func TestAuthzModel(t *testing.T) { test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges) test.AssertEquals(t, authzPBOut.CertificateProfileName, authzPB.CertificateProfileName) + // Manipulate authzPB to test marshalling between corepb.Authorization and + // the SA authz model + // TODO(#8722): clean up these tests when authz IDs are int-only + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Id = "" + authzPB.IdInt = 0 + _, err = authzPBToModel(authzPB) + test.AssertError(t, err, "authzPBToModel with empty Id and empty IdInt unexpectedly succeeded") + + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Id = "1" + authzPB.IdInt = 0 + model, err = authzPBToModel(authzPB) + test.AssertNotError(t, err, "authzPBToModel with a value for string Id and empty IdInt failed") + authzPBOut, err = modelToAuthzPB(*model) + test.AssertNotError(t, err, "modelToAuthzPB failed") + test.AssertEquals(t, fmt.Sprintf("%d", authzPBOut.IdInt), authzPBOut.Id) + + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Id = "" + authzPB.IdInt = 1 + model, err = authzPBToModel(authzPB) + test.AssertNotError(t, err, "authzPBToModel with empty Id and an int value for IdInt failed") + authzPBOut, err = modelToAuthzPB(*model) + test.AssertNotError(t, err, "modelToAuthzPB failed") + test.AssertEquals(t, fmt.Sprintf("%d", authzPBOut.IdInt), authzPBOut.Id) + + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Id = "1" + authzPB.IdInt = 1 + model, err = authzPBToModel(authzPB) + test.AssertNotError(t, err, "authzPBToModel with values for both string Id and int IdInt failed") + authzPBOut, err = modelToAuthzPB(*model) + test.AssertNotError(t, err, "modelToAuthzPB failed") + test.AssertEquals(t, fmt.Sprintf("%d", authzPBOut.IdInt), authzPBOut.Id) + authzPB = newTestAuthzPB(clk.Now()) validationErr := probs.Connection("weewoo") diff --git a/sa/proto/sa.pb.go b/sa/proto/sa.pb.go index a0c46217f52..3605e0e69e7 100644 --- a/sa/proto/sa.pb.go +++ b/sa/proto/sa.pb.go @@ -113,50 +113,6 @@ func (x *JSONWebKey) GetJwk() []byte { return nil } -type AuthorizationID struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AuthorizationID) Reset() { - *x = AuthorizationID{} - mi := &file_sa_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AuthorizationID) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthorizationID) ProtoMessage() {} - -func (x *AuthorizationID) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AuthorizationID.ProtoReflect.Descriptor instead. -func (*AuthorizationID) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{2} -} - -func (x *AuthorizationID) GetId() string { - if x != nil { - return x.Id - } - return "" -} - type GetValidAuthorizationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 7 @@ -170,7 +126,7 @@ type GetValidAuthorizationsRequest struct { func (x *GetValidAuthorizationsRequest) Reset() { *x = GetValidAuthorizationsRequest{} - mi := &file_sa_proto_msgTypes[3] + mi := &file_sa_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -182,7 +138,7 @@ func (x *GetValidAuthorizationsRequest) String() string { func (*GetValidAuthorizationsRequest) ProtoMessage() {} func (x *GetValidAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[3] + mi := &file_sa_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -195,7 +151,7 @@ func (x *GetValidAuthorizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetValidAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetValidAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{3} + return file_sa_proto_rawDescGZIP(), []int{2} } func (x *GetValidAuthorizationsRequest) GetRegistrationID() int64 { @@ -235,7 +191,7 @@ type Serial struct { func (x *Serial) Reset() { *x = Serial{} - mi := &file_sa_proto_msgTypes[4] + mi := &file_sa_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -247,7 +203,7 @@ func (x *Serial) String() string { func (*Serial) ProtoMessage() {} func (x *Serial) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[4] + mi := &file_sa_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -260,7 +216,7 @@ func (x *Serial) ProtoReflect() protoreflect.Message { // Deprecated: Use Serial.ProtoReflect.Descriptor instead. func (*Serial) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{4} + return file_sa_proto_rawDescGZIP(), []int{3} } func (x *Serial) GetSerial() string { @@ -283,7 +239,7 @@ type SerialMetadata struct { func (x *SerialMetadata) Reset() { *x = SerialMetadata{} - mi := &file_sa_proto_msgTypes[5] + mi := &file_sa_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -295,7 +251,7 @@ func (x *SerialMetadata) String() string { func (*SerialMetadata) ProtoMessage() {} func (x *SerialMetadata) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[5] + mi := &file_sa_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -308,7 +264,7 @@ func (x *SerialMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use SerialMetadata.ProtoReflect.Descriptor instead. func (*SerialMetadata) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{5} + return file_sa_proto_rawDescGZIP(), []int{4} } func (x *SerialMetadata) GetSerial() string { @@ -349,7 +305,7 @@ type Range struct { func (x *Range) Reset() { *x = Range{} - mi := &file_sa_proto_msgTypes[6] + mi := &file_sa_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -361,7 +317,7 @@ func (x *Range) String() string { func (*Range) ProtoMessage() {} func (x *Range) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[6] + mi := &file_sa_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -374,7 +330,7 @@ func (x *Range) ProtoReflect() protoreflect.Message { // Deprecated: Use Range.ProtoReflect.Descriptor instead. func (*Range) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{6} + return file_sa_proto_rawDescGZIP(), []int{5} } func (x *Range) GetEarliest() *timestamppb.Timestamp { @@ -400,7 +356,7 @@ type Count struct { func (x *Count) Reset() { *x = Count{} - mi := &file_sa_proto_msgTypes[7] + mi := &file_sa_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +368,7 @@ func (x *Count) String() string { func (*Count) ProtoMessage() {} func (x *Count) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[7] + mi := &file_sa_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -425,7 +381,7 @@ func (x *Count) ProtoReflect() protoreflect.Message { // Deprecated: Use Count.ProtoReflect.Descriptor instead. func (*Count) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{7} + return file_sa_proto_rawDescGZIP(), []int{6} } func (x *Count) GetCount() int64 { @@ -444,7 +400,7 @@ type Timestamps struct { func (x *Timestamps) Reset() { *x = Timestamps{} - mi := &file_sa_proto_msgTypes[8] + mi := &file_sa_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -456,7 +412,7 @@ func (x *Timestamps) String() string { func (*Timestamps) ProtoMessage() {} func (x *Timestamps) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[8] + mi := &file_sa_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -469,7 +425,7 @@ func (x *Timestamps) ProtoReflect() protoreflect.Message { // Deprecated: Use Timestamps.ProtoReflect.Descriptor instead. func (*Timestamps) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{8} + return file_sa_proto_rawDescGZIP(), []int{7} } func (x *Timestamps) GetTimestamps() []*timestamppb.Timestamp { @@ -492,7 +448,7 @@ type CountInvalidAuthorizationsRequest struct { func (x *CountInvalidAuthorizationsRequest) Reset() { *x = CountInvalidAuthorizationsRequest{} - mi := &file_sa_proto_msgTypes[9] + mi := &file_sa_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -504,7 +460,7 @@ func (x *CountInvalidAuthorizationsRequest) String() string { func (*CountInvalidAuthorizationsRequest) ProtoMessage() {} func (x *CountInvalidAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[9] + mi := &file_sa_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -517,7 +473,7 @@ func (x *CountInvalidAuthorizationsRequest) ProtoReflect() protoreflect.Message // Deprecated: Use CountInvalidAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*CountInvalidAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{9} + return file_sa_proto_rawDescGZIP(), []int{8} } func (x *CountInvalidAuthorizationsRequest) GetRegistrationID() int64 { @@ -552,7 +508,7 @@ type CountFQDNSetsRequest struct { func (x *CountFQDNSetsRequest) Reset() { *x = CountFQDNSetsRequest{} - mi := &file_sa_proto_msgTypes[10] + mi := &file_sa_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -564,7 +520,7 @@ func (x *CountFQDNSetsRequest) String() string { func (*CountFQDNSetsRequest) ProtoMessage() {} func (x *CountFQDNSetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[10] + mi := &file_sa_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -577,7 +533,7 @@ func (x *CountFQDNSetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CountFQDNSetsRequest.ProtoReflect.Descriptor instead. func (*CountFQDNSetsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{10} + return file_sa_proto_rawDescGZIP(), []int{9} } func (x *CountFQDNSetsRequest) GetIdentifiers() []*proto.Identifier { @@ -610,7 +566,7 @@ type FQDNSetExistsRequest struct { func (x *FQDNSetExistsRequest) Reset() { *x = FQDNSetExistsRequest{} - mi := &file_sa_proto_msgTypes[11] + mi := &file_sa_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -622,7 +578,7 @@ func (x *FQDNSetExistsRequest) String() string { func (*FQDNSetExistsRequest) ProtoMessage() {} func (x *FQDNSetExistsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[11] + mi := &file_sa_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -635,7 +591,7 @@ func (x *FQDNSetExistsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FQDNSetExistsRequest.ProtoReflect.Descriptor instead. func (*FQDNSetExistsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{11} + return file_sa_proto_rawDescGZIP(), []int{10} } func (x *FQDNSetExistsRequest) GetIdentifiers() []*proto.Identifier { @@ -654,7 +610,7 @@ type Exists struct { func (x *Exists) Reset() { *x = Exists{} - mi := &file_sa_proto_msgTypes[12] + mi := &file_sa_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -666,7 +622,7 @@ func (x *Exists) String() string { func (*Exists) ProtoMessage() {} func (x *Exists) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[12] + mi := &file_sa_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -679,7 +635,7 @@ func (x *Exists) ProtoReflect() protoreflect.Message { // Deprecated: Use Exists.ProtoReflect.Descriptor instead. func (*Exists) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{12} + return file_sa_proto_rawDescGZIP(), []int{11} } func (x *Exists) GetExists() bool { @@ -702,7 +658,7 @@ type AddSerialRequest struct { func (x *AddSerialRequest) Reset() { *x = AddSerialRequest{} - mi := &file_sa_proto_msgTypes[13] + mi := &file_sa_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -714,7 +670,7 @@ func (x *AddSerialRequest) String() string { func (*AddSerialRequest) ProtoMessage() {} func (x *AddSerialRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[13] + mi := &file_sa_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -727,7 +683,7 @@ func (x *AddSerialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddSerialRequest.ProtoReflect.Descriptor instead. func (*AddSerialRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{13} + return file_sa_proto_rawDescGZIP(), []int{12} } func (x *AddSerialRequest) GetRegID() int64 { @@ -771,7 +727,7 @@ type AddCertificateRequest struct { func (x *AddCertificateRequest) Reset() { *x = AddCertificateRequest{} - mi := &file_sa_proto_msgTypes[14] + mi := &file_sa_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -783,7 +739,7 @@ func (x *AddCertificateRequest) String() string { func (*AddCertificateRequest) ProtoMessage() {} func (x *AddCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[14] + mi := &file_sa_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -796,7 +752,7 @@ func (x *AddCertificateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddCertificateRequest.ProtoReflect.Descriptor instead. func (*AddCertificateRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{14} + return file_sa_proto_rawDescGZIP(), []int{13} } func (x *AddCertificateRequest) GetDer() []byte { @@ -836,7 +792,7 @@ type OrderRequest struct { func (x *OrderRequest) Reset() { *x = OrderRequest{} - mi := &file_sa_proto_msgTypes[15] + mi := &file_sa_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -848,7 +804,7 @@ func (x *OrderRequest) String() string { func (*OrderRequest) ProtoMessage() {} func (x *OrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[15] + mi := &file_sa_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -861,7 +817,7 @@ func (x *OrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OrderRequest.ProtoReflect.Descriptor instead. func (*OrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{15} + return file_sa_proto_rawDescGZIP(), []int{14} } func (x *OrderRequest) GetId() int64 { @@ -892,7 +848,7 @@ type NewOrderRequest struct { func (x *NewOrderRequest) Reset() { *x = NewOrderRequest{} - mi := &file_sa_proto_msgTypes[16] + mi := &file_sa_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -904,7 +860,7 @@ func (x *NewOrderRequest) String() string { func (*NewOrderRequest) ProtoMessage() {} func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[16] + mi := &file_sa_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -917,7 +873,7 @@ func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewOrderRequest.ProtoReflect.Descriptor instead. func (*NewOrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{16} + return file_sa_proto_rawDescGZIP(), []int{15} } func (x *NewOrderRequest) GetRegistrationID() int64 { @@ -983,7 +939,7 @@ type NewAuthzRequest struct { func (x *NewAuthzRequest) Reset() { *x = NewAuthzRequest{} - mi := &file_sa_proto_msgTypes[17] + mi := &file_sa_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -995,7 +951,7 @@ func (x *NewAuthzRequest) String() string { func (*NewAuthzRequest) ProtoMessage() {} func (x *NewAuthzRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[17] + mi := &file_sa_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1008,7 +964,7 @@ func (x *NewAuthzRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewAuthzRequest.ProtoReflect.Descriptor instead. func (*NewAuthzRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{17} + return file_sa_proto_rawDescGZIP(), []int{16} } func (x *NewAuthzRequest) GetIdentifier() *proto.Identifier { @@ -1061,7 +1017,7 @@ type NewOrderAndAuthzsRequest struct { func (x *NewOrderAndAuthzsRequest) Reset() { *x = NewOrderAndAuthzsRequest{} - mi := &file_sa_proto_msgTypes[18] + mi := &file_sa_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1073,7 +1029,7 @@ func (x *NewOrderAndAuthzsRequest) String() string { func (*NewOrderAndAuthzsRequest) ProtoMessage() {} func (x *NewOrderAndAuthzsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[18] + mi := &file_sa_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1086,7 +1042,7 @@ func (x *NewOrderAndAuthzsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewOrderAndAuthzsRequest.ProtoReflect.Descriptor instead. func (*NewOrderAndAuthzsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{18} + return file_sa_proto_rawDescGZIP(), []int{17} } func (x *NewOrderAndAuthzsRequest) GetNewOrder() *NewOrderRequest { @@ -1113,7 +1069,7 @@ type SetOrderErrorRequest struct { func (x *SetOrderErrorRequest) Reset() { *x = SetOrderErrorRequest{} - mi := &file_sa_proto_msgTypes[19] + mi := &file_sa_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1125,7 +1081,7 @@ func (x *SetOrderErrorRequest) String() string { func (*SetOrderErrorRequest) ProtoMessage() {} func (x *SetOrderErrorRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[19] + mi := &file_sa_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1138,7 +1094,7 @@ func (x *SetOrderErrorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOrderErrorRequest.ProtoReflect.Descriptor instead. func (*SetOrderErrorRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{19} + return file_sa_proto_rawDescGZIP(), []int{18} } func (x *SetOrderErrorRequest) GetId() int64 { @@ -1165,7 +1121,7 @@ type GetOrderAuthorizationsRequest struct { func (x *GetOrderAuthorizationsRequest) Reset() { *x = GetOrderAuthorizationsRequest{} - mi := &file_sa_proto_msgTypes[20] + mi := &file_sa_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1177,7 +1133,7 @@ func (x *GetOrderAuthorizationsRequest) String() string { func (*GetOrderAuthorizationsRequest) ProtoMessage() {} func (x *GetOrderAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[20] + mi := &file_sa_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1190,7 +1146,7 @@ func (x *GetOrderAuthorizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOrderAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetOrderAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{20} + return file_sa_proto_rawDescGZIP(), []int{19} } func (x *GetOrderAuthorizationsRequest) GetId() int64 { @@ -1218,7 +1174,7 @@ type GetOrderForNamesRequest struct { func (x *GetOrderForNamesRequest) Reset() { *x = GetOrderForNamesRequest{} - mi := &file_sa_proto_msgTypes[21] + mi := &file_sa_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1230,7 +1186,7 @@ func (x *GetOrderForNamesRequest) String() string { func (*GetOrderForNamesRequest) ProtoMessage() {} func (x *GetOrderForNamesRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[21] + mi := &file_sa_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1243,7 +1199,7 @@ func (x *GetOrderForNamesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOrderForNamesRequest.ProtoReflect.Descriptor instead. func (*GetOrderForNamesRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{21} + return file_sa_proto_rawDescGZIP(), []int{20} } func (x *GetOrderForNamesRequest) GetAcctID() int64 { @@ -1270,7 +1226,7 @@ type FinalizeOrderRequest struct { func (x *FinalizeOrderRequest) Reset() { *x = FinalizeOrderRequest{} - mi := &file_sa_proto_msgTypes[22] + mi := &file_sa_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1282,7 +1238,7 @@ func (x *FinalizeOrderRequest) String() string { func (*FinalizeOrderRequest) ProtoMessage() {} func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[22] + mi := &file_sa_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1295,7 +1251,7 @@ func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeOrderRequest.ProtoReflect.Descriptor instead. func (*FinalizeOrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{22} + return file_sa_proto_rawDescGZIP(), []int{21} } func (x *FinalizeOrderRequest) GetId() int64 { @@ -1325,7 +1281,7 @@ type GetAuthorizationsRequest struct { func (x *GetAuthorizationsRequest) Reset() { *x = GetAuthorizationsRequest{} - mi := &file_sa_proto_msgTypes[23] + mi := &file_sa_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1337,7 +1293,7 @@ func (x *GetAuthorizationsRequest) String() string { func (*GetAuthorizationsRequest) ProtoMessage() {} func (x *GetAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[23] + mi := &file_sa_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1350,7 +1306,7 @@ func (x *GetAuthorizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{23} + return file_sa_proto_rawDescGZIP(), []int{22} } func (x *GetAuthorizationsRequest) GetRegistrationID() int64 { @@ -1390,7 +1346,7 @@ type Authorizations struct { func (x *Authorizations) Reset() { *x = Authorizations{} - mi := &file_sa_proto_msgTypes[24] + mi := &file_sa_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1402,7 +1358,7 @@ func (x *Authorizations) String() string { func (*Authorizations) ProtoMessage() {} func (x *Authorizations) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[24] + mi := &file_sa_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1415,7 +1371,7 @@ func (x *Authorizations) ProtoReflect() protoreflect.Message { // Deprecated: Use Authorizations.ProtoReflect.Descriptor instead. func (*Authorizations) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{24} + return file_sa_proto_rawDescGZIP(), []int{23} } func (x *Authorizations) GetAuthzs() []*proto.Authorization { @@ -1425,50 +1381,6 @@ func (x *Authorizations) GetAuthzs() []*proto.Authorization { return nil } -type AuthorizationIDs struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AuthorizationIDs) Reset() { - *x = AuthorizationIDs{} - mi := &file_sa_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AuthorizationIDs) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthorizationIDs) ProtoMessage() {} - -func (x *AuthorizationIDs) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[25] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AuthorizationIDs.ProtoReflect.Descriptor instead. -func (*AuthorizationIDs) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{25} -} - -func (x *AuthorizationIDs) GetIds() []string { - if x != nil { - return x.Ids - } - return nil -} - type AuthorizationID2 struct { state protoimpl.MessageState `protogen:"open.v1"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` @@ -1478,7 +1390,7 @@ type AuthorizationID2 struct { func (x *AuthorizationID2) Reset() { *x = AuthorizationID2{} - mi := &file_sa_proto_msgTypes[26] + mi := &file_sa_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1490,7 +1402,7 @@ func (x *AuthorizationID2) String() string { func (*AuthorizationID2) ProtoMessage() {} func (x *AuthorizationID2) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[26] + mi := &file_sa_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1503,7 +1415,7 @@ func (x *AuthorizationID2) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizationID2.ProtoReflect.Descriptor instead. func (*AuthorizationID2) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{26} + return file_sa_proto_rawDescGZIP(), []int{24} } func (x *AuthorizationID2) GetId() int64 { @@ -1529,7 +1441,7 @@ type RevokeCertificateRequest struct { func (x *RevokeCertificateRequest) Reset() { *x = RevokeCertificateRequest{} - mi := &file_sa_proto_msgTypes[27] + mi := &file_sa_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1541,7 +1453,7 @@ func (x *RevokeCertificateRequest) String() string { func (*RevokeCertificateRequest) ProtoMessage() {} func (x *RevokeCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[27] + mi := &file_sa_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1554,7 +1466,7 @@ func (x *RevokeCertificateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeCertificateRequest.ProtoReflect.Descriptor instead. func (*RevokeCertificateRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{27} + return file_sa_proto_rawDescGZIP(), []int{25} } func (x *RevokeCertificateRequest) GetSerial() string { @@ -1622,7 +1534,7 @@ type FinalizeAuthorizationRequest struct { func (x *FinalizeAuthorizationRequest) Reset() { *x = FinalizeAuthorizationRequest{} - mi := &file_sa_proto_msgTypes[28] + mi := &file_sa_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1634,7 +1546,7 @@ func (x *FinalizeAuthorizationRequest) String() string { func (*FinalizeAuthorizationRequest) ProtoMessage() {} func (x *FinalizeAuthorizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[28] + mi := &file_sa_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1647,7 +1559,7 @@ func (x *FinalizeAuthorizationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeAuthorizationRequest.ProtoReflect.Descriptor instead. func (*FinalizeAuthorizationRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{28} + return file_sa_proto_rawDescGZIP(), []int{26} } func (x *FinalizeAuthorizationRequest) GetId() int64 { @@ -1713,7 +1625,7 @@ type AddBlockedKeyRequest struct { func (x *AddBlockedKeyRequest) Reset() { *x = AddBlockedKeyRequest{} - mi := &file_sa_proto_msgTypes[29] + mi := &file_sa_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1725,7 +1637,7 @@ func (x *AddBlockedKeyRequest) String() string { func (*AddBlockedKeyRequest) ProtoMessage() {} func (x *AddBlockedKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[29] + mi := &file_sa_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1738,7 +1650,7 @@ func (x *AddBlockedKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddBlockedKeyRequest.ProtoReflect.Descriptor instead. func (*AddBlockedKeyRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{29} + return file_sa_proto_rawDescGZIP(), []int{27} } func (x *AddBlockedKeyRequest) GetKeyHash() []byte { @@ -1785,7 +1697,7 @@ type SPKIHash struct { func (x *SPKIHash) Reset() { *x = SPKIHash{} - mi := &file_sa_proto_msgTypes[30] + mi := &file_sa_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1797,7 +1709,7 @@ func (x *SPKIHash) String() string { func (*SPKIHash) ProtoMessage() {} func (x *SPKIHash) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[30] + mi := &file_sa_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1810,7 +1722,7 @@ func (x *SPKIHash) ProtoReflect() protoreflect.Message { // Deprecated: Use SPKIHash.ProtoReflect.Descriptor instead. func (*SPKIHash) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{30} + return file_sa_proto_rawDescGZIP(), []int{28} } func (x *SPKIHash) GetKeyHash() []byte { @@ -1834,7 +1746,7 @@ type Incident struct { func (x *Incident) Reset() { *x = Incident{} - mi := &file_sa_proto_msgTypes[31] + mi := &file_sa_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1846,7 +1758,7 @@ func (x *Incident) String() string { func (*Incident) ProtoMessage() {} func (x *Incident) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[31] + mi := &file_sa_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1859,7 +1771,7 @@ func (x *Incident) ProtoReflect() protoreflect.Message { // Deprecated: Use Incident.ProtoReflect.Descriptor instead. func (*Incident) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{31} + return file_sa_proto_rawDescGZIP(), []int{29} } func (x *Incident) GetId() int64 { @@ -1906,7 +1818,7 @@ type Incidents struct { func (x *Incidents) Reset() { *x = Incidents{} - mi := &file_sa_proto_msgTypes[32] + mi := &file_sa_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1918,7 +1830,7 @@ func (x *Incidents) String() string { func (*Incidents) ProtoMessage() {} func (x *Incidents) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[32] + mi := &file_sa_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1931,7 +1843,7 @@ func (x *Incidents) ProtoReflect() protoreflect.Message { // Deprecated: Use Incidents.ProtoReflect.Descriptor instead. func (*Incidents) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{32} + return file_sa_proto_rawDescGZIP(), []int{30} } func (x *Incidents) GetIncidents() []*Incident { @@ -1950,7 +1862,7 @@ type SerialsForIncidentRequest struct { func (x *SerialsForIncidentRequest) Reset() { *x = SerialsForIncidentRequest{} - mi := &file_sa_proto_msgTypes[33] + mi := &file_sa_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1962,7 +1874,7 @@ func (x *SerialsForIncidentRequest) String() string { func (*SerialsForIncidentRequest) ProtoMessage() {} func (x *SerialsForIncidentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[33] + mi := &file_sa_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1975,7 +1887,7 @@ func (x *SerialsForIncidentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SerialsForIncidentRequest.ProtoReflect.Descriptor instead. func (*SerialsForIncidentRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{33} + return file_sa_proto_rawDescGZIP(), []int{31} } func (x *SerialsForIncidentRequest) GetIncidentTable() string { @@ -1996,7 +1908,7 @@ type CreateIncidentRequest struct { func (x *CreateIncidentRequest) Reset() { *x = CreateIncidentRequest{} - mi := &file_sa_proto_msgTypes[34] + mi := &file_sa_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2008,7 +1920,7 @@ func (x *CreateIncidentRequest) String() string { func (*CreateIncidentRequest) ProtoMessage() {} func (x *CreateIncidentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[34] + mi := &file_sa_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2021,7 +1933,7 @@ func (x *CreateIncidentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateIncidentRequest.ProtoReflect.Descriptor instead. func (*CreateIncidentRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{34} + return file_sa_proto_rawDescGZIP(), []int{32} } func (x *CreateIncidentRequest) GetSerialTable() string { @@ -2061,7 +1973,7 @@ type UpdateIncidentRequest struct { func (x *UpdateIncidentRequest) Reset() { *x = UpdateIncidentRequest{} - mi := &file_sa_proto_msgTypes[35] + mi := &file_sa_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2073,7 +1985,7 @@ func (x *UpdateIncidentRequest) String() string { func (*UpdateIncidentRequest) ProtoMessage() {} func (x *UpdateIncidentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[35] + mi := &file_sa_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2086,7 +1998,7 @@ func (x *UpdateIncidentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateIncidentRequest.ProtoReflect.Descriptor instead. func (*UpdateIncidentRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{35} + return file_sa_proto_rawDescGZIP(), []int{33} } func (x *UpdateIncidentRequest) GetSerialTable() string { @@ -2130,7 +2042,7 @@ type AddSerialsToIncidentRequest struct { func (x *AddSerialsToIncidentRequest) Reset() { *x = AddSerialsToIncidentRequest{} - mi := &file_sa_proto_msgTypes[36] + mi := &file_sa_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2142,7 +2054,7 @@ func (x *AddSerialsToIncidentRequest) String() string { func (*AddSerialsToIncidentRequest) ProtoMessage() {} func (x *AddSerialsToIncidentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[36] + mi := &file_sa_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2155,7 +2067,7 @@ func (x *AddSerialsToIncidentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddSerialsToIncidentRequest.ProtoReflect.Descriptor instead. func (*AddSerialsToIncidentRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{36} + return file_sa_proto_rawDescGZIP(), []int{34} } func (x *AddSerialsToIncidentRequest) GetPayload() isAddSerialsToIncidentRequest_Payload { @@ -2208,7 +2120,7 @@ type AddSerialsToIncidentMetadata struct { func (x *AddSerialsToIncidentMetadata) Reset() { *x = AddSerialsToIncidentMetadata{} - mi := &file_sa_proto_msgTypes[37] + mi := &file_sa_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2220,7 +2132,7 @@ func (x *AddSerialsToIncidentMetadata) String() string { func (*AddSerialsToIncidentMetadata) ProtoMessage() {} func (x *AddSerialsToIncidentMetadata) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[37] + mi := &file_sa_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2233,7 +2145,7 @@ func (x *AddSerialsToIncidentMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use AddSerialsToIncidentMetadata.ProtoReflect.Descriptor instead. func (*AddSerialsToIncidentMetadata) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{37} + return file_sa_proto_rawDescGZIP(), []int{35} } func (x *AddSerialsToIncidentMetadata) GetSerialTable() string { @@ -2252,7 +2164,7 @@ type AddSerialsToIncidentBatch struct { func (x *AddSerialsToIncidentBatch) Reset() { *x = AddSerialsToIncidentBatch{} - mi := &file_sa_proto_msgTypes[38] + mi := &file_sa_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2264,7 +2176,7 @@ func (x *AddSerialsToIncidentBatch) String() string { func (*AddSerialsToIncidentBatch) ProtoMessage() {} func (x *AddSerialsToIncidentBatch) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[38] + mi := &file_sa_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2277,7 +2189,7 @@ func (x *AddSerialsToIncidentBatch) ProtoReflect() protoreflect.Message { // Deprecated: Use AddSerialsToIncidentBatch.ProtoReflect.Descriptor instead. func (*AddSerialsToIncidentBatch) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{38} + return file_sa_proto_rawDescGZIP(), []int{36} } func (x *AddSerialsToIncidentBatch) GetSerials() []string { @@ -2300,7 +2212,7 @@ type IncidentSerial struct { func (x *IncidentSerial) Reset() { *x = IncidentSerial{} - mi := &file_sa_proto_msgTypes[39] + mi := &file_sa_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2312,7 +2224,7 @@ func (x *IncidentSerial) String() string { func (*IncidentSerial) ProtoMessage() {} func (x *IncidentSerial) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[39] + mi := &file_sa_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2325,7 +2237,7 @@ func (x *IncidentSerial) ProtoReflect() protoreflect.Message { // Deprecated: Use IncidentSerial.ProtoReflect.Descriptor instead. func (*IncidentSerial) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{39} + return file_sa_proto_rawDescGZIP(), []int{37} } func (x *IncidentSerial) GetSerial() string { @@ -2368,7 +2280,7 @@ type GetRevokedCertsByShardRequest struct { func (x *GetRevokedCertsByShardRequest) Reset() { *x = GetRevokedCertsByShardRequest{} - mi := &file_sa_proto_msgTypes[40] + mi := &file_sa_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2380,7 +2292,7 @@ func (x *GetRevokedCertsByShardRequest) String() string { func (*GetRevokedCertsByShardRequest) ProtoMessage() {} func (x *GetRevokedCertsByShardRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[40] + mi := &file_sa_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2393,7 +2305,7 @@ func (x *GetRevokedCertsByShardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRevokedCertsByShardRequest.ProtoReflect.Descriptor instead. func (*GetRevokedCertsByShardRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{40} + return file_sa_proto_rawDescGZIP(), []int{38} } func (x *GetRevokedCertsByShardRequest) GetIssuerNameID() int64 { @@ -2435,7 +2347,7 @@ type RevocationStatus struct { func (x *RevocationStatus) Reset() { *x = RevocationStatus{} - mi := &file_sa_proto_msgTypes[41] + mi := &file_sa_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2447,7 +2359,7 @@ func (x *RevocationStatus) String() string { func (*RevocationStatus) ProtoMessage() {} func (x *RevocationStatus) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[41] + mi := &file_sa_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2460,7 +2372,7 @@ func (x *RevocationStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use RevocationStatus.ProtoReflect.Descriptor instead. func (*RevocationStatus) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{41} + return file_sa_proto_rawDescGZIP(), []int{39} } func (x *RevocationStatus) GetStatus() int64 { @@ -2496,7 +2408,7 @@ type LeaseCRLShardRequest struct { func (x *LeaseCRLShardRequest) Reset() { *x = LeaseCRLShardRequest{} - mi := &file_sa_proto_msgTypes[42] + mi := &file_sa_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2508,7 +2420,7 @@ func (x *LeaseCRLShardRequest) String() string { func (*LeaseCRLShardRequest) ProtoMessage() {} func (x *LeaseCRLShardRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[42] + mi := &file_sa_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2521,7 +2433,7 @@ func (x *LeaseCRLShardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaseCRLShardRequest.ProtoReflect.Descriptor instead. func (*LeaseCRLShardRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{42} + return file_sa_proto_rawDescGZIP(), []int{40} } func (x *LeaseCRLShardRequest) GetIssuerNameID() int64 { @@ -2562,7 +2474,7 @@ type LeaseCRLShardResponse struct { func (x *LeaseCRLShardResponse) Reset() { *x = LeaseCRLShardResponse{} - mi := &file_sa_proto_msgTypes[43] + mi := &file_sa_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2574,7 +2486,7 @@ func (x *LeaseCRLShardResponse) String() string { func (*LeaseCRLShardResponse) ProtoMessage() {} func (x *LeaseCRLShardResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[43] + mi := &file_sa_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2587,7 +2499,7 @@ func (x *LeaseCRLShardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaseCRLShardResponse.ProtoReflect.Descriptor instead. func (*LeaseCRLShardResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{43} + return file_sa_proto_rawDescGZIP(), []int{41} } func (x *LeaseCRLShardResponse) GetIssuerNameID() int64 { @@ -2616,7 +2528,7 @@ type UpdateCRLShardRequest struct { func (x *UpdateCRLShardRequest) Reset() { *x = UpdateCRLShardRequest{} - mi := &file_sa_proto_msgTypes[44] + mi := &file_sa_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2628,7 +2540,7 @@ func (x *UpdateCRLShardRequest) String() string { func (*UpdateCRLShardRequest) ProtoMessage() {} func (x *UpdateCRLShardRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[44] + mi := &file_sa_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2641,7 +2553,7 @@ func (x *UpdateCRLShardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateCRLShardRequest.ProtoReflect.Descriptor instead. func (*UpdateCRLShardRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{44} + return file_sa_proto_rawDescGZIP(), []int{42} } func (x *UpdateCRLShardRequest) GetIssuerNameID() int64 { @@ -2681,7 +2593,7 @@ type Identifiers struct { func (x *Identifiers) Reset() { *x = Identifiers{} - mi := &file_sa_proto_msgTypes[45] + mi := &file_sa_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2693,7 +2605,7 @@ func (x *Identifiers) String() string { func (*Identifiers) ProtoMessage() {} func (x *Identifiers) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[45] + mi := &file_sa_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2706,7 +2618,7 @@ func (x *Identifiers) ProtoReflect() protoreflect.Message { // Deprecated: Use Identifiers.ProtoReflect.Descriptor instead. func (*Identifiers) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{45} + return file_sa_proto_rawDescGZIP(), []int{43} } func (x *Identifiers) GetIdentifiers() []*proto.Identifier { @@ -2726,7 +2638,7 @@ type PauseRequest struct { func (x *PauseRequest) Reset() { *x = PauseRequest{} - mi := &file_sa_proto_msgTypes[46] + mi := &file_sa_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2738,7 +2650,7 @@ func (x *PauseRequest) String() string { func (*PauseRequest) ProtoMessage() {} func (x *PauseRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[46] + mi := &file_sa_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2751,7 +2663,7 @@ func (x *PauseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseRequest.ProtoReflect.Descriptor instead. func (*PauseRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{46} + return file_sa_proto_rawDescGZIP(), []int{44} } func (x *PauseRequest) GetRegistrationID() int64 { @@ -2778,7 +2690,7 @@ type PauseIdentifiersResponse struct { func (x *PauseIdentifiersResponse) Reset() { *x = PauseIdentifiersResponse{} - mi := &file_sa_proto_msgTypes[47] + mi := &file_sa_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2790,7 +2702,7 @@ func (x *PauseIdentifiersResponse) String() string { func (*PauseIdentifiersResponse) ProtoMessage() {} func (x *PauseIdentifiersResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[47] + mi := &file_sa_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2803,7 +2715,7 @@ func (x *PauseIdentifiersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseIdentifiersResponse.ProtoReflect.Descriptor instead. func (*PauseIdentifiersResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{47} + return file_sa_proto_rawDescGZIP(), []int{45} } func (x *PauseIdentifiersResponse) GetPaused() int64 { @@ -2830,7 +2742,7 @@ type UpdateRegistrationKeyRequest struct { func (x *UpdateRegistrationKeyRequest) Reset() { *x = UpdateRegistrationKeyRequest{} - mi := &file_sa_proto_msgTypes[48] + mi := &file_sa_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2842,7 +2754,7 @@ func (x *UpdateRegistrationKeyRequest) String() string { func (*UpdateRegistrationKeyRequest) ProtoMessage() {} func (x *UpdateRegistrationKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[48] + mi := &file_sa_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2855,7 +2767,7 @@ func (x *UpdateRegistrationKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRegistrationKeyRequest.ProtoReflect.Descriptor instead. func (*UpdateRegistrationKeyRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{48} + return file_sa_proto_rawDescGZIP(), []int{46} } func (x *UpdateRegistrationKeyRequest) GetRegistrationID() int64 { @@ -2886,7 +2798,7 @@ type RateLimitOverride struct { func (x *RateLimitOverride) Reset() { *x = RateLimitOverride{} - mi := &file_sa_proto_msgTypes[49] + mi := &file_sa_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2898,7 +2810,7 @@ func (x *RateLimitOverride) String() string { func (*RateLimitOverride) ProtoMessage() {} func (x *RateLimitOverride) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[49] + mi := &file_sa_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2911,7 +2823,7 @@ func (x *RateLimitOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use RateLimitOverride.ProtoReflect.Descriptor instead. func (*RateLimitOverride) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{49} + return file_sa_proto_rawDescGZIP(), []int{47} } func (x *RateLimitOverride) GetLimitEnum() int64 { @@ -2966,7 +2878,7 @@ type AddRateLimitOverrideRequest struct { func (x *AddRateLimitOverrideRequest) Reset() { *x = AddRateLimitOverrideRequest{} - mi := &file_sa_proto_msgTypes[50] + mi := &file_sa_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2978,7 +2890,7 @@ func (x *AddRateLimitOverrideRequest) String() string { func (*AddRateLimitOverrideRequest) ProtoMessage() {} func (x *AddRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[50] + mi := &file_sa_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2991,7 +2903,7 @@ func (x *AddRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddRateLimitOverrideRequest.ProtoReflect.Descriptor instead. func (*AddRateLimitOverrideRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{50} + return file_sa_proto_rawDescGZIP(), []int{48} } func (x *AddRateLimitOverrideRequest) GetOverride() *RateLimitOverride { @@ -3019,7 +2931,7 @@ type AddRateLimitOverrideResponse struct { func (x *AddRateLimitOverrideResponse) Reset() { *x = AddRateLimitOverrideResponse{} - mi := &file_sa_proto_msgTypes[51] + mi := &file_sa_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3031,7 +2943,7 @@ func (x *AddRateLimitOverrideResponse) String() string { func (*AddRateLimitOverrideResponse) ProtoMessage() {} func (x *AddRateLimitOverrideResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[51] + mi := &file_sa_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3044,7 +2956,7 @@ func (x *AddRateLimitOverrideResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddRateLimitOverrideResponse.ProtoReflect.Descriptor instead. func (*AddRateLimitOverrideResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{51} + return file_sa_proto_rawDescGZIP(), []int{49} } func (x *AddRateLimitOverrideResponse) GetInserted() bool { @@ -3078,7 +2990,7 @@ type EnableRateLimitOverrideRequest struct { func (x *EnableRateLimitOverrideRequest) Reset() { *x = EnableRateLimitOverrideRequest{} - mi := &file_sa_proto_msgTypes[52] + mi := &file_sa_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3090,7 +3002,7 @@ func (x *EnableRateLimitOverrideRequest) String() string { func (*EnableRateLimitOverrideRequest) ProtoMessage() {} func (x *EnableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[52] + mi := &file_sa_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3103,7 +3015,7 @@ func (x *EnableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EnableRateLimitOverrideRequest.ProtoReflect.Descriptor instead. func (*EnableRateLimitOverrideRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{52} + return file_sa_proto_rawDescGZIP(), []int{50} } func (x *EnableRateLimitOverrideRequest) GetLimitEnum() int64 { @@ -3130,7 +3042,7 @@ type DisableRateLimitOverrideRequest struct { func (x *DisableRateLimitOverrideRequest) Reset() { *x = DisableRateLimitOverrideRequest{} - mi := &file_sa_proto_msgTypes[53] + mi := &file_sa_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3142,7 +3054,7 @@ func (x *DisableRateLimitOverrideRequest) String() string { func (*DisableRateLimitOverrideRequest) ProtoMessage() {} func (x *DisableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[53] + mi := &file_sa_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3155,7 +3067,7 @@ func (x *DisableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DisableRateLimitOverrideRequest.ProtoReflect.Descriptor instead. func (*DisableRateLimitOverrideRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{53} + return file_sa_proto_rawDescGZIP(), []int{51} } func (x *DisableRateLimitOverrideRequest) GetLimitEnum() int64 { @@ -3182,7 +3094,7 @@ type GetRateLimitOverrideRequest struct { func (x *GetRateLimitOverrideRequest) Reset() { *x = GetRateLimitOverrideRequest{} - mi := &file_sa_proto_msgTypes[54] + mi := &file_sa_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3194,7 +3106,7 @@ func (x *GetRateLimitOverrideRequest) String() string { func (*GetRateLimitOverrideRequest) ProtoMessage() {} func (x *GetRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[54] + mi := &file_sa_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3207,7 +3119,7 @@ func (x *GetRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRateLimitOverrideRequest.ProtoReflect.Descriptor instead. func (*GetRateLimitOverrideRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{54} + return file_sa_proto_rawDescGZIP(), []int{52} } func (x *GetRateLimitOverrideRequest) GetLimitEnum() int64 { @@ -3235,7 +3147,7 @@ type RateLimitOverrideResponse struct { func (x *RateLimitOverrideResponse) Reset() { *x = RateLimitOverrideResponse{} - mi := &file_sa_proto_msgTypes[55] + mi := &file_sa_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3247,7 +3159,7 @@ func (x *RateLimitOverrideResponse) String() string { func (*RateLimitOverrideResponse) ProtoMessage() {} func (x *RateLimitOverrideResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[55] + mi := &file_sa_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3260,7 +3172,7 @@ func (x *RateLimitOverrideResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RateLimitOverrideResponse.ProtoReflect.Descriptor instead. func (*RateLimitOverrideResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{55} + return file_sa_proto_rawDescGZIP(), []int{53} } func (x *RateLimitOverrideResponse) GetOverride() *RateLimitOverride { @@ -3299,787 +3211,782 @@ var file_sa_proto_rawDesc = string([]byte{ 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1e, 0x0a, 0x0a, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x03, 0x6a, 0x77, 0x6b, 0x22, 0x21, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xdd, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, - 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, - 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4a, 0x04, 0x08, 0x02, - 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x20, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0xc8, 0x01, 0x0a, 0x0e, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, - 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, - 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x7f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x36, - 0x0a, 0x08, 0x65, 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x65, 0x61, - 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, - 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x1d, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4e, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x4a, - 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0xa4, 0x01, 0x0a, 0x21, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, - 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, - 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x9f, 0x01, 0x0a, - 0x14, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x77, 0x69, 0x6e, - 0x64, 0x6f, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x14, 0x0a, 0x05, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x50, - 0x0a, 0x14, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, - 0x22, 0x20, 0x0a, 0x06, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, - 0x74, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xa9, 0x01, - 0x0a, 0x15, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, - 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, - 0x32, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x69, 0x73, 0x73, - 0x75, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, - 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, - 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x1e, 0x0a, 0x0c, 0x4f, 0x72, 0x64, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x02, 0x0a, 0x0f, 0x4e, 0x65, - 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, - 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, - 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, - 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, - 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, - 0x03, 0x10, 0x04, 0x22, 0x89, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, - 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, - 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, - 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, - 0x7e, 0x0a, 0x18, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, - 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x6e, - 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x31, 0x0a, 0x09, - 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x09, 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, - 0x52, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, - 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x47, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x22, 0x6b, 0x0a, 0x17, - 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x12, - 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x03, + 0x52, 0x03, 0x6a, 0x77, 0x6b, 0x22, 0xdd, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, + 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x54, 0x0a, 0x14, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, - 0xd8, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, - 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4a, 0x04, - 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x3d, 0x0a, 0x0e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x06, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, 0x24, 0x0a, 0x10, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, - 0x22, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, 0x0a, 0x18, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x36, 0x0a, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, - 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, - 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x4a, 0x04, 0x08, 0x03, - 0x10, 0x04, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0xea, 0x02, 0x0a, 0x1c, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, - 0x70, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x74, 0x74, 0x65, - 0x6d, 0x70, 0x74, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x0f, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x3c, 0x0a, 0x0b, 0x61, - 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x61, 0x74, - 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, - 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xb8, 0x01, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x30, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x65, - 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, - 0x22, 0x24, 0x0a, 0x08, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, - 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, - 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, - 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, - 0x42, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x37, 0x0a, - 0x09, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x09, 0x69, 0x6e, - 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, - 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x09, 0x69, 0x6e, 0x63, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, 0x19, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x81, 0x01, 0x0a, 0x15, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, - 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, - 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x22, 0xac, 0x01, - 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x72, - 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, - 0x79, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, - 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x9f, 0x01, 0x0a, - 0x1b, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, - 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x48, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x05, - 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x73, 0x61, - 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x48, 0x00, 0x52, 0x05, 0x62, 0x61, - 0x74, 0x63, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x40, - 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, - 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x20, - 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, - 0x22, 0x35, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, - 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x18, 0x0a, - 0x07, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0e, 0x49, 0x6e, 0x63, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, + 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x20, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0xc8, 0x01, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x72, - 0x64, 0x65, 0x72, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6f, 0x72, 0x64, - 0x65, 0x72, 0x49, 0x44, 0x12, 0x42, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, - 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x6f, - 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xe1, - 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, - 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, - 0x6d, 0x65, 0x49, 0x44, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, - 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, - 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, - 0x64, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, - 0x64, 0x78, 0x22, 0x8e, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, - 0x44, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, - 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, - 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, - 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, - 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, - 0x64, 0x78, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, - 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, - 0x64, 0x49, 0x64, 0x78, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x04, 0x20, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, + 0x10, 0x05, 0x22, 0x7f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x65, + 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x65, 0x61, 0x72, 0x6c, 0x69, + 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0x57, 0x0a, 0x15, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, - 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, - 0xcf, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, - 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, - 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, - 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, - 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, + 0x02, 0x10, 0x03, 0x22, 0x1d, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x22, 0x4e, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, + 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x4a, 0x04, 0x08, 0x01, + 0x10, 0x02, 0x22, 0xa4, 0x01, 0x0a, 0x21, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, + 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x9f, 0x01, 0x0a, 0x14, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x4a, + 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x50, 0x0a, 0x14, 0x46, + 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x20, 0x0a, + 0x06, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, + 0xb8, 0x01, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x22, 0x41, 0x0a, 0x0b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, - 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, + 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xa9, 0x01, 0x0a, 0x15, 0x41, + 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, + 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, + 0x6d, 0x65, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, + 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x1e, 0x0a, 0x0c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, + 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x10, + 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, + 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, + 0x22, 0x89, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, + 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, + 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, + 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x7e, 0x0a, 0x18, + 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x61, 0x2e, + 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x08, 0x6e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x31, 0x0a, 0x09, 0x6e, 0x65, 0x77, + 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, + 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x09, 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, 0x52, 0x0a, 0x14, + 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, + 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x22, 0x47, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x22, 0x6b, 0x0a, 0x17, 0x47, 0x65, 0x74, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, - 0x22, 0x4e, 0x0a, 0x18, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x70, 0x61, - 0x75, 0x73, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, - 0x22, 0x58, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6a, 0x77, 0x6b, 0x22, 0xc8, 0x01, 0x0a, 0x11, 0x52, - 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x54, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2c, + 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0xd8, 0x01, 0x0a, + 0x18, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, + 0x74, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, + 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, + 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x3d, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, 0x22, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, 0x0a, 0x18, 0x52, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, + 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, + 0x49, 0x64, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, + 0x49, 0x64, 0x78, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, + 0xea, 0x02, 0x0a, 0x1c, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x1c, + 0x0a, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x11, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, + 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, + 0x73, 0x52, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x3c, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xb8, 0x01, 0x0a, + 0x14, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, + 0x30, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x61, 0x64, 0x64, 0x65, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, + 0x79, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x24, 0x0a, 0x08, 0x53, 0x50, 0x4b, 0x49, 0x48, + 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x22, 0xa4, 0x01, + 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, + 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x65, 0x6e, + 0x65, 0x77, 0x42, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x4a, 0x04, + 0x08, 0x04, 0x10, 0x05, 0x22, 0x37, 0x0a, 0x09, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x2a, 0x0a, 0x09, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x52, 0x09, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, + 0x19, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, + 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x22, 0x81, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, + 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x72, 0x65, 0x6e, + 0x65, 0x77, 0x42, 0x79, 0x22, 0xac, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, + 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, + 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, + 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x22, 0x9f, 0x01, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x48, 0x00, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x40, 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x35, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x22, 0xb4, + 0x01, 0x0a, 0x0e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x42, 0x0a, 0x0e, 0x6c, + 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x4a, + 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xe1, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x40, 0x0a, 0x0d, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, + 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, + 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, 0x8e, 0x01, 0x0a, 0x10, 0x52, 0x65, + 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, + 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0b, + 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x4c, + 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, + 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, + 0x6e, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, + 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, + 0x6d, 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x30, 0x0a, 0x05, 0x75, + 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0x57, 0x0a, + 0x15, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, + 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, + 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, 0xcf, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, + 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, + 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x0a, + 0x6e, 0x65, 0x78, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6e, 0x65, + 0x78, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x41, 0x0a, 0x0b, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, + 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0c, 0x50, + 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x4e, 0x0a, 0x18, 0x50, 0x61, 0x75, 0x73, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, + 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x22, 0x58, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, + 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6a, 0x77, + 0x6b, 0x22, 0xc8, 0x01, 0x0a, 0x11, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x0a, + 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x22, 0x66, 0x0a, 0x1b, + 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x22, 0x87, 0x01, 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, + 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x65, + 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x5c, + 0x0a, 0x1e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, + 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, - 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, - 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x62, 0x75, 0x72, 0x73, 0x74, 0x22, 0x66, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x5d, 0x0a, 0x1f, + 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, + 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x59, 0x0a, 0x1b, 0x47, + 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, + 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, 0xa2, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x74, 0x65, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x6f, - 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x87, 0x01, - 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, - 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, - 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x08, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x65, - 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x5c, 0x0a, 0x1e, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, - 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, - 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, - 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, - 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x5d, 0x0a, 0x1f, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, - 0x74, 0x4b, 0x65, 0x79, 0x22, 0x59, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, - 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, - 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, - 0xa2, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, - 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, - 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, - 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x32, 0xed, 0x0d, 0x0a, 0x18, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, - 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, - 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x73, - 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, 0x51, - 0x44, 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x46, - 0x6f, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, - 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x15, 0x47, 0x65, 0x74, - 0x4c, 0x69, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x38, 0x0a, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x32, 0xed, 0x0d, 0x0a, 0x18, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, 0x4e, + 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, + 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, + 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, + 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, + 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x31, 0x0a, + 0x0e, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, + 0x12, 0x38, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, + 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, - 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x10, - 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, - 0x3e, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, - 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, - 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, - 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, - 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, - 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, - 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, - 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, - 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, - 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, - 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x57, 0x0a, - 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, - 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x08, 0x47, 0x65, 0x74, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, 0x2e, + 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, + 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x16, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, + 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, 0x0a, + 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x12, + 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, + 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, + 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x4b, + 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, + 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, + 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, + 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, - 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, - 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, - 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, - 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, - 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, - 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, - 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, - 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, - 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, - 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, - 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, - 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, - 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, - 0x58, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, - 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, - 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x51, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, + 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, 0x72, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, + 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, + 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x38, 0x0a, + 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, + 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, + 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, + 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, + 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, + 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, + 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, + 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, + 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, + 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x59, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x1c, 0x47, 0x65, 0x74, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, - 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, - 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x30, 0x01, 0x32, 0xb4, 0x1a, 0x0a, 0x10, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, - 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, - 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, - 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, - 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x31, - 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, - 0x00, 0x12, 0x38, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x63, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, - 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, - 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x08, 0x47, 0x65, - 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, - 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, - 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, - 0x61, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x4f, 0x0a, - 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, - 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, - 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, - 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, - 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, - 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, - 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, - 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, - 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, - 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, - 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x32, 0xb4, 0x1a, 0x0a, 0x10, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, + 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, + 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x73, 0x61, + 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, 0x51, 0x44, + 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x46, 0x6f, + 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x13, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, + 0x69, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x00, 0x12, 0x2b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x10, 0x2e, + 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3e, + 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, + 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, + 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, + 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, + 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, + 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x21, + 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, + 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, + 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, + 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x1c, + 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, - 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, - 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x51, - 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, - 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, - 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, - 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, - 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x38, - 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, - 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, - 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, - 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, - 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, - 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, - 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, - 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, - 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, + 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, + 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, + 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, + 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, + 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x63, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, + 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, + 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, + 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, + 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, + 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, + 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x73, + 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, + 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, 0x61, + 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x58, + 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, + 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, + 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x59, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, - 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, - 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, - 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0d, - 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x2e, - 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x00, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0d, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, + 0x64, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x50, - 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, - 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x65, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, + 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, - 0x4a, 0x0a, 0x18, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, - 0x32, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x16, 0x44, - 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, - 0x54, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, - 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, 0x4e, 0x65, - 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x12, - 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, - 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, - 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, - 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x11, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, - 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x48, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x09, 0x41, 0x64, 0x64, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, - 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x74, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x12, 0x53, - 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, - 0x67, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4f, 0x0a, - 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x52, - 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, - 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x18, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0d, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, - 0x61, 0x72, 0x64, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, - 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, - 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x19, 0x2e, 0x73, - 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, + 0x22, 0x00, 0x12, 0x42, 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, + 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, + 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x54, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, + 0x12, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, + 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x2e, + 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x44, 0x0a, 0x10, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, - 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, - 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x14, 0x41, 0x64, - 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, - 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, - 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x18, 0x44, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, - 0x69, 0x64, 0x65, 0x12, 0x23, 0x2e, 0x73, 0x61, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x00, 0x12, 0x40, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, + 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, + 0x12, 0x4b, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, + 0x0d, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, + 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x00, 0x12, 0x40, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x50, 0x72, + 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x2e, + 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0d, 0x4c, 0x65, 0x61, + 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, + 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, + 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, + 0x61, 0x72, 0x64, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, + 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x10, 0x50, 0x61, 0x75, 0x73, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x10, 0x2e, 0x73, + 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, + 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, + 0x00, 0x12, 0x5b, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, + 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x41, + 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x2e, + 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, + 0x0a, 0x18, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x23, 0x2e, 0x73, 0x61, 0x2e, + 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x17, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x12, 0x22, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x57, 0x0a, 0x17, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x22, 0x2e, - 0x73, 0x61, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x32, 0xe6, 0x01, 0x0a, 0x15, - 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, - 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x3b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, - 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x22, 0x00, 0x32, 0xe6, 0x01, 0x0a, 0x15, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x3b, 0x0a, 0x0e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x19, + 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, + 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x73, 0x61, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1f, + 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x12, - 0x53, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, - 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x54, 0x6f, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x28, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, - 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x73, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x73, 0x61, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -4094,289 +4001,287 @@ func file_sa_proto_rawDescGZIP() []byte { return file_sa_proto_rawDescData } -var file_sa_proto_msgTypes = make([]protoimpl.MessageInfo, 56) +var file_sa_proto_msgTypes = make([]protoimpl.MessageInfo, 54) var file_sa_proto_goTypes = []any{ (*RegistrationID)(nil), // 0: sa.RegistrationID (*JSONWebKey)(nil), // 1: sa.JSONWebKey - (*AuthorizationID)(nil), // 2: sa.AuthorizationID - (*GetValidAuthorizationsRequest)(nil), // 3: sa.GetValidAuthorizationsRequest - (*Serial)(nil), // 4: sa.Serial - (*SerialMetadata)(nil), // 5: sa.SerialMetadata - (*Range)(nil), // 6: sa.Range - (*Count)(nil), // 7: sa.Count - (*Timestamps)(nil), // 8: sa.Timestamps - (*CountInvalidAuthorizationsRequest)(nil), // 9: sa.CountInvalidAuthorizationsRequest - (*CountFQDNSetsRequest)(nil), // 10: sa.CountFQDNSetsRequest - (*FQDNSetExistsRequest)(nil), // 11: sa.FQDNSetExistsRequest - (*Exists)(nil), // 12: sa.Exists - (*AddSerialRequest)(nil), // 13: sa.AddSerialRequest - (*AddCertificateRequest)(nil), // 14: sa.AddCertificateRequest - (*OrderRequest)(nil), // 15: sa.OrderRequest - (*NewOrderRequest)(nil), // 16: sa.NewOrderRequest - (*NewAuthzRequest)(nil), // 17: sa.NewAuthzRequest - (*NewOrderAndAuthzsRequest)(nil), // 18: sa.NewOrderAndAuthzsRequest - (*SetOrderErrorRequest)(nil), // 19: sa.SetOrderErrorRequest - (*GetOrderAuthorizationsRequest)(nil), // 20: sa.GetOrderAuthorizationsRequest - (*GetOrderForNamesRequest)(nil), // 21: sa.GetOrderForNamesRequest - (*FinalizeOrderRequest)(nil), // 22: sa.FinalizeOrderRequest - (*GetAuthorizationsRequest)(nil), // 23: sa.GetAuthorizationsRequest - (*Authorizations)(nil), // 24: sa.Authorizations - (*AuthorizationIDs)(nil), // 25: sa.AuthorizationIDs - (*AuthorizationID2)(nil), // 26: sa.AuthorizationID2 - (*RevokeCertificateRequest)(nil), // 27: sa.RevokeCertificateRequest - (*FinalizeAuthorizationRequest)(nil), // 28: sa.FinalizeAuthorizationRequest - (*AddBlockedKeyRequest)(nil), // 29: sa.AddBlockedKeyRequest - (*SPKIHash)(nil), // 30: sa.SPKIHash - (*Incident)(nil), // 31: sa.Incident - (*Incidents)(nil), // 32: sa.Incidents - (*SerialsForIncidentRequest)(nil), // 33: sa.SerialsForIncidentRequest - (*CreateIncidentRequest)(nil), // 34: sa.CreateIncidentRequest - (*UpdateIncidentRequest)(nil), // 35: sa.UpdateIncidentRequest - (*AddSerialsToIncidentRequest)(nil), // 36: sa.AddSerialsToIncidentRequest - (*AddSerialsToIncidentMetadata)(nil), // 37: sa.AddSerialsToIncidentMetadata - (*AddSerialsToIncidentBatch)(nil), // 38: sa.AddSerialsToIncidentBatch - (*IncidentSerial)(nil), // 39: sa.IncidentSerial - (*GetRevokedCertsByShardRequest)(nil), // 40: sa.GetRevokedCertsByShardRequest - (*RevocationStatus)(nil), // 41: sa.RevocationStatus - (*LeaseCRLShardRequest)(nil), // 42: sa.LeaseCRLShardRequest - (*LeaseCRLShardResponse)(nil), // 43: sa.LeaseCRLShardResponse - (*UpdateCRLShardRequest)(nil), // 44: sa.UpdateCRLShardRequest - (*Identifiers)(nil), // 45: sa.Identifiers - (*PauseRequest)(nil), // 46: sa.PauseRequest - (*PauseIdentifiersResponse)(nil), // 47: sa.PauseIdentifiersResponse - (*UpdateRegistrationKeyRequest)(nil), // 48: sa.UpdateRegistrationKeyRequest - (*RateLimitOverride)(nil), // 49: sa.RateLimitOverride - (*AddRateLimitOverrideRequest)(nil), // 50: sa.AddRateLimitOverrideRequest - (*AddRateLimitOverrideResponse)(nil), // 51: sa.AddRateLimitOverrideResponse - (*EnableRateLimitOverrideRequest)(nil), // 52: sa.EnableRateLimitOverrideRequest - (*DisableRateLimitOverrideRequest)(nil), // 53: sa.DisableRateLimitOverrideRequest - (*GetRateLimitOverrideRequest)(nil), // 54: sa.GetRateLimitOverrideRequest - (*RateLimitOverrideResponse)(nil), // 55: sa.RateLimitOverrideResponse - (*proto.Identifier)(nil), // 56: core.Identifier - (*timestamppb.Timestamp)(nil), // 57: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 58: google.protobuf.Duration - (*proto.ProblemDetails)(nil), // 59: core.ProblemDetails - (*proto.Authorization)(nil), // 60: core.Authorization - (*proto.ValidationRecord)(nil), // 61: core.ValidationRecord - (*emptypb.Empty)(nil), // 62: google.protobuf.Empty - (*proto.Registration)(nil), // 63: core.Registration - (*proto.Certificate)(nil), // 64: core.Certificate - (*proto.CertificateStatus)(nil), // 65: core.CertificateStatus - (*proto.Order)(nil), // 66: core.Order - (*proto.CRLEntry)(nil), // 67: core.CRLEntry + (*GetValidAuthorizationsRequest)(nil), // 2: sa.GetValidAuthorizationsRequest + (*Serial)(nil), // 3: sa.Serial + (*SerialMetadata)(nil), // 4: sa.SerialMetadata + (*Range)(nil), // 5: sa.Range + (*Count)(nil), // 6: sa.Count + (*Timestamps)(nil), // 7: sa.Timestamps + (*CountInvalidAuthorizationsRequest)(nil), // 8: sa.CountInvalidAuthorizationsRequest + (*CountFQDNSetsRequest)(nil), // 9: sa.CountFQDNSetsRequest + (*FQDNSetExistsRequest)(nil), // 10: sa.FQDNSetExistsRequest + (*Exists)(nil), // 11: sa.Exists + (*AddSerialRequest)(nil), // 12: sa.AddSerialRequest + (*AddCertificateRequest)(nil), // 13: sa.AddCertificateRequest + (*OrderRequest)(nil), // 14: sa.OrderRequest + (*NewOrderRequest)(nil), // 15: sa.NewOrderRequest + (*NewAuthzRequest)(nil), // 16: sa.NewAuthzRequest + (*NewOrderAndAuthzsRequest)(nil), // 17: sa.NewOrderAndAuthzsRequest + (*SetOrderErrorRequest)(nil), // 18: sa.SetOrderErrorRequest + (*GetOrderAuthorizationsRequest)(nil), // 19: sa.GetOrderAuthorizationsRequest + (*GetOrderForNamesRequest)(nil), // 20: sa.GetOrderForNamesRequest + (*FinalizeOrderRequest)(nil), // 21: sa.FinalizeOrderRequest + (*GetAuthorizationsRequest)(nil), // 22: sa.GetAuthorizationsRequest + (*Authorizations)(nil), // 23: sa.Authorizations + (*AuthorizationID2)(nil), // 24: sa.AuthorizationID2 + (*RevokeCertificateRequest)(nil), // 25: sa.RevokeCertificateRequest + (*FinalizeAuthorizationRequest)(nil), // 26: sa.FinalizeAuthorizationRequest + (*AddBlockedKeyRequest)(nil), // 27: sa.AddBlockedKeyRequest + (*SPKIHash)(nil), // 28: sa.SPKIHash + (*Incident)(nil), // 29: sa.Incident + (*Incidents)(nil), // 30: sa.Incidents + (*SerialsForIncidentRequest)(nil), // 31: sa.SerialsForIncidentRequest + (*CreateIncidentRequest)(nil), // 32: sa.CreateIncidentRequest + (*UpdateIncidentRequest)(nil), // 33: sa.UpdateIncidentRequest + (*AddSerialsToIncidentRequest)(nil), // 34: sa.AddSerialsToIncidentRequest + (*AddSerialsToIncidentMetadata)(nil), // 35: sa.AddSerialsToIncidentMetadata + (*AddSerialsToIncidentBatch)(nil), // 36: sa.AddSerialsToIncidentBatch + (*IncidentSerial)(nil), // 37: sa.IncidentSerial + (*GetRevokedCertsByShardRequest)(nil), // 38: sa.GetRevokedCertsByShardRequest + (*RevocationStatus)(nil), // 39: sa.RevocationStatus + (*LeaseCRLShardRequest)(nil), // 40: sa.LeaseCRLShardRequest + (*LeaseCRLShardResponse)(nil), // 41: sa.LeaseCRLShardResponse + (*UpdateCRLShardRequest)(nil), // 42: sa.UpdateCRLShardRequest + (*Identifiers)(nil), // 43: sa.Identifiers + (*PauseRequest)(nil), // 44: sa.PauseRequest + (*PauseIdentifiersResponse)(nil), // 45: sa.PauseIdentifiersResponse + (*UpdateRegistrationKeyRequest)(nil), // 46: sa.UpdateRegistrationKeyRequest + (*RateLimitOverride)(nil), // 47: sa.RateLimitOverride + (*AddRateLimitOverrideRequest)(nil), // 48: sa.AddRateLimitOverrideRequest + (*AddRateLimitOverrideResponse)(nil), // 49: sa.AddRateLimitOverrideResponse + (*EnableRateLimitOverrideRequest)(nil), // 50: sa.EnableRateLimitOverrideRequest + (*DisableRateLimitOverrideRequest)(nil), // 51: sa.DisableRateLimitOverrideRequest + (*GetRateLimitOverrideRequest)(nil), // 52: sa.GetRateLimitOverrideRequest + (*RateLimitOverrideResponse)(nil), // 53: sa.RateLimitOverrideResponse + (*proto.Identifier)(nil), // 54: core.Identifier + (*timestamppb.Timestamp)(nil), // 55: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 56: google.protobuf.Duration + (*proto.ProblemDetails)(nil), // 57: core.ProblemDetails + (*proto.Authorization)(nil), // 58: core.Authorization + (*proto.ValidationRecord)(nil), // 59: core.ValidationRecord + (*emptypb.Empty)(nil), // 60: google.protobuf.Empty + (*proto.Registration)(nil), // 61: core.Registration + (*proto.Certificate)(nil), // 62: core.Certificate + (*proto.CertificateStatus)(nil), // 63: core.CertificateStatus + (*proto.Order)(nil), // 64: core.Order + (*proto.CRLEntry)(nil), // 65: core.CRLEntry } var file_sa_proto_depIdxs = []int32{ - 56, // 0: sa.GetValidAuthorizationsRequest.identifiers:type_name -> core.Identifier - 57, // 1: sa.GetValidAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp - 57, // 2: sa.SerialMetadata.created:type_name -> google.protobuf.Timestamp - 57, // 3: sa.SerialMetadata.expires:type_name -> google.protobuf.Timestamp - 57, // 4: sa.Range.earliest:type_name -> google.protobuf.Timestamp - 57, // 5: sa.Range.latest:type_name -> google.protobuf.Timestamp - 57, // 6: sa.Timestamps.timestamps:type_name -> google.protobuf.Timestamp - 56, // 7: sa.CountInvalidAuthorizationsRequest.identifier:type_name -> core.Identifier - 6, // 8: sa.CountInvalidAuthorizationsRequest.range:type_name -> sa.Range - 56, // 9: sa.CountFQDNSetsRequest.identifiers:type_name -> core.Identifier - 58, // 10: sa.CountFQDNSetsRequest.window:type_name -> google.protobuf.Duration - 56, // 11: sa.FQDNSetExistsRequest.identifiers:type_name -> core.Identifier - 57, // 12: sa.AddSerialRequest.created:type_name -> google.protobuf.Timestamp - 57, // 13: sa.AddSerialRequest.expires:type_name -> google.protobuf.Timestamp - 57, // 14: sa.AddCertificateRequest.issued:type_name -> google.protobuf.Timestamp - 57, // 15: sa.NewOrderRequest.expires:type_name -> google.protobuf.Timestamp - 56, // 16: sa.NewOrderRequest.identifiers:type_name -> core.Identifier - 56, // 17: sa.NewAuthzRequest.identifier:type_name -> core.Identifier - 57, // 18: sa.NewAuthzRequest.expires:type_name -> google.protobuf.Timestamp - 16, // 19: sa.NewOrderAndAuthzsRequest.newOrder:type_name -> sa.NewOrderRequest - 17, // 20: sa.NewOrderAndAuthzsRequest.newAuthzs:type_name -> sa.NewAuthzRequest - 59, // 21: sa.SetOrderErrorRequest.error:type_name -> core.ProblemDetails - 56, // 22: sa.GetOrderForNamesRequest.identifiers:type_name -> core.Identifier - 56, // 23: sa.GetAuthorizationsRequest.identifiers:type_name -> core.Identifier - 57, // 24: sa.GetAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp - 60, // 25: sa.Authorizations.authzs:type_name -> core.Authorization - 57, // 26: sa.RevokeCertificateRequest.date:type_name -> google.protobuf.Timestamp - 57, // 27: sa.RevokeCertificateRequest.backdate:type_name -> google.protobuf.Timestamp - 57, // 28: sa.FinalizeAuthorizationRequest.expires:type_name -> google.protobuf.Timestamp - 61, // 29: sa.FinalizeAuthorizationRequest.validationRecords:type_name -> core.ValidationRecord - 59, // 30: sa.FinalizeAuthorizationRequest.validationError:type_name -> core.ProblemDetails - 57, // 31: sa.FinalizeAuthorizationRequest.attemptedAt:type_name -> google.protobuf.Timestamp - 57, // 32: sa.AddBlockedKeyRequest.added:type_name -> google.protobuf.Timestamp - 57, // 33: sa.Incident.renewBy:type_name -> google.protobuf.Timestamp - 31, // 34: sa.Incidents.incidents:type_name -> sa.Incident - 57, // 35: sa.CreateIncidentRequest.renewBy:type_name -> google.protobuf.Timestamp - 57, // 36: sa.UpdateIncidentRequest.renewBy:type_name -> google.protobuf.Timestamp - 37, // 37: sa.AddSerialsToIncidentRequest.metadata:type_name -> sa.AddSerialsToIncidentMetadata - 38, // 38: sa.AddSerialsToIncidentRequest.batch:type_name -> sa.AddSerialsToIncidentBatch - 57, // 39: sa.IncidentSerial.lastNoticeSent:type_name -> google.protobuf.Timestamp - 57, // 40: sa.GetRevokedCertsByShardRequest.revokedBefore:type_name -> google.protobuf.Timestamp - 57, // 41: sa.GetRevokedCertsByShardRequest.expiresAfter:type_name -> google.protobuf.Timestamp - 57, // 42: sa.RevocationStatus.revokedDate:type_name -> google.protobuf.Timestamp - 57, // 43: sa.LeaseCRLShardRequest.until:type_name -> google.protobuf.Timestamp - 57, // 44: sa.UpdateCRLShardRequest.thisUpdate:type_name -> google.protobuf.Timestamp - 57, // 45: sa.UpdateCRLShardRequest.nextUpdate:type_name -> google.protobuf.Timestamp - 56, // 46: sa.Identifiers.identifiers:type_name -> core.Identifier - 56, // 47: sa.PauseRequest.identifiers:type_name -> core.Identifier - 58, // 48: sa.RateLimitOverride.period:type_name -> google.protobuf.Duration - 49, // 49: sa.AddRateLimitOverrideRequest.override:type_name -> sa.RateLimitOverride - 49, // 50: sa.AddRateLimitOverrideResponse.existing:type_name -> sa.RateLimitOverride - 49, // 51: sa.RateLimitOverrideResponse.override:type_name -> sa.RateLimitOverride - 57, // 52: sa.RateLimitOverrideResponse.updatedAt:type_name -> google.protobuf.Timestamp - 11, // 53: sa.StorageAuthorityReadOnly.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest - 10, // 54: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest - 26, // 55: sa.StorageAuthorityReadOnly.GetAuthorization2:input_type -> sa.AuthorizationID2 - 4, // 56: sa.StorageAuthorityReadOnly.GetCertificate:input_type -> sa.Serial - 4, // 57: sa.StorageAuthorityReadOnly.GetLintPrecertificate:input_type -> sa.Serial - 4, // 58: sa.StorageAuthorityReadOnly.GetCertificateStatus:input_type -> sa.Serial - 15, // 59: sa.StorageAuthorityReadOnly.GetOrder:input_type -> sa.OrderRequest - 21, // 60: sa.StorageAuthorityReadOnly.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest + 54, // 0: sa.GetValidAuthorizationsRequest.identifiers:type_name -> core.Identifier + 55, // 1: sa.GetValidAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp + 55, // 2: sa.SerialMetadata.created:type_name -> google.protobuf.Timestamp + 55, // 3: sa.SerialMetadata.expires:type_name -> google.protobuf.Timestamp + 55, // 4: sa.Range.earliest:type_name -> google.protobuf.Timestamp + 55, // 5: sa.Range.latest:type_name -> google.protobuf.Timestamp + 55, // 6: sa.Timestamps.timestamps:type_name -> google.protobuf.Timestamp + 54, // 7: sa.CountInvalidAuthorizationsRequest.identifier:type_name -> core.Identifier + 5, // 8: sa.CountInvalidAuthorizationsRequest.range:type_name -> sa.Range + 54, // 9: sa.CountFQDNSetsRequest.identifiers:type_name -> core.Identifier + 56, // 10: sa.CountFQDNSetsRequest.window:type_name -> google.protobuf.Duration + 54, // 11: sa.FQDNSetExistsRequest.identifiers:type_name -> core.Identifier + 55, // 12: sa.AddSerialRequest.created:type_name -> google.protobuf.Timestamp + 55, // 13: sa.AddSerialRequest.expires:type_name -> google.protobuf.Timestamp + 55, // 14: sa.AddCertificateRequest.issued:type_name -> google.protobuf.Timestamp + 55, // 15: sa.NewOrderRequest.expires:type_name -> google.protobuf.Timestamp + 54, // 16: sa.NewOrderRequest.identifiers:type_name -> core.Identifier + 54, // 17: sa.NewAuthzRequest.identifier:type_name -> core.Identifier + 55, // 18: sa.NewAuthzRequest.expires:type_name -> google.protobuf.Timestamp + 15, // 19: sa.NewOrderAndAuthzsRequest.newOrder:type_name -> sa.NewOrderRequest + 16, // 20: sa.NewOrderAndAuthzsRequest.newAuthzs:type_name -> sa.NewAuthzRequest + 57, // 21: sa.SetOrderErrorRequest.error:type_name -> core.ProblemDetails + 54, // 22: sa.GetOrderForNamesRequest.identifiers:type_name -> core.Identifier + 54, // 23: sa.GetAuthorizationsRequest.identifiers:type_name -> core.Identifier + 55, // 24: sa.GetAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp + 58, // 25: sa.Authorizations.authzs:type_name -> core.Authorization + 55, // 26: sa.RevokeCertificateRequest.date:type_name -> google.protobuf.Timestamp + 55, // 27: sa.RevokeCertificateRequest.backdate:type_name -> google.protobuf.Timestamp + 55, // 28: sa.FinalizeAuthorizationRequest.expires:type_name -> google.protobuf.Timestamp + 59, // 29: sa.FinalizeAuthorizationRequest.validationRecords:type_name -> core.ValidationRecord + 57, // 30: sa.FinalizeAuthorizationRequest.validationError:type_name -> core.ProblemDetails + 55, // 31: sa.FinalizeAuthorizationRequest.attemptedAt:type_name -> google.protobuf.Timestamp + 55, // 32: sa.AddBlockedKeyRequest.added:type_name -> google.protobuf.Timestamp + 55, // 33: sa.Incident.renewBy:type_name -> google.protobuf.Timestamp + 29, // 34: sa.Incidents.incidents:type_name -> sa.Incident + 55, // 35: sa.CreateIncidentRequest.renewBy:type_name -> google.protobuf.Timestamp + 55, // 36: sa.UpdateIncidentRequest.renewBy:type_name -> google.protobuf.Timestamp + 35, // 37: sa.AddSerialsToIncidentRequest.metadata:type_name -> sa.AddSerialsToIncidentMetadata + 36, // 38: sa.AddSerialsToIncidentRequest.batch:type_name -> sa.AddSerialsToIncidentBatch + 55, // 39: sa.IncidentSerial.lastNoticeSent:type_name -> google.protobuf.Timestamp + 55, // 40: sa.GetRevokedCertsByShardRequest.revokedBefore:type_name -> google.protobuf.Timestamp + 55, // 41: sa.GetRevokedCertsByShardRequest.expiresAfter:type_name -> google.protobuf.Timestamp + 55, // 42: sa.RevocationStatus.revokedDate:type_name -> google.protobuf.Timestamp + 55, // 43: sa.LeaseCRLShardRequest.until:type_name -> google.protobuf.Timestamp + 55, // 44: sa.UpdateCRLShardRequest.thisUpdate:type_name -> google.protobuf.Timestamp + 55, // 45: sa.UpdateCRLShardRequest.nextUpdate:type_name -> google.protobuf.Timestamp + 54, // 46: sa.Identifiers.identifiers:type_name -> core.Identifier + 54, // 47: sa.PauseRequest.identifiers:type_name -> core.Identifier + 56, // 48: sa.RateLimitOverride.period:type_name -> google.protobuf.Duration + 47, // 49: sa.AddRateLimitOverrideRequest.override:type_name -> sa.RateLimitOverride + 47, // 50: sa.AddRateLimitOverrideResponse.existing:type_name -> sa.RateLimitOverride + 47, // 51: sa.RateLimitOverrideResponse.override:type_name -> sa.RateLimitOverride + 55, // 52: sa.RateLimitOverrideResponse.updatedAt:type_name -> google.protobuf.Timestamp + 10, // 53: sa.StorageAuthorityReadOnly.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest + 9, // 54: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest + 24, // 55: sa.StorageAuthorityReadOnly.GetAuthorization2:input_type -> sa.AuthorizationID2 + 3, // 56: sa.StorageAuthorityReadOnly.GetCertificate:input_type -> sa.Serial + 3, // 57: sa.StorageAuthorityReadOnly.GetLintPrecertificate:input_type -> sa.Serial + 3, // 58: sa.StorageAuthorityReadOnly.GetCertificateStatus:input_type -> sa.Serial + 14, // 59: sa.StorageAuthorityReadOnly.GetOrder:input_type -> sa.OrderRequest + 20, // 60: sa.StorageAuthorityReadOnly.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest 0, // 61: sa.StorageAuthorityReadOnly.GetRegistration:input_type -> sa.RegistrationID 1, // 62: sa.StorageAuthorityReadOnly.GetRegistrationByKey:input_type -> sa.JSONWebKey - 4, // 63: sa.StorageAuthorityReadOnly.GetRevocationStatus:input_type -> sa.Serial - 40, // 64: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest - 4, // 65: sa.StorageAuthorityReadOnly.GetSerialMetadata:input_type -> sa.Serial + 3, // 63: sa.StorageAuthorityReadOnly.GetRevocationStatus:input_type -> sa.Serial + 38, // 64: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest + 3, // 65: sa.StorageAuthorityReadOnly.GetSerialMetadata:input_type -> sa.Serial 0, // 66: sa.StorageAuthorityReadOnly.GetSerialsByAccount:input_type -> sa.RegistrationID - 30, // 67: sa.StorageAuthorityReadOnly.GetSerialsByKey:input_type -> sa.SPKIHash - 3, // 68: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest - 20, // 69: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:input_type -> sa.GetOrderAuthorizationsRequest - 20, // 70: sa.StorageAuthorityReadOnly.GetOrderAuthorizations:input_type -> sa.GetOrderAuthorizationsRequest - 4, // 71: sa.StorageAuthorityReadOnly.IncidentsForSerial:input_type -> sa.Serial - 30, // 72: sa.StorageAuthorityReadOnly.KeyBlocked:input_type -> sa.SPKIHash - 62, // 73: sa.StorageAuthorityReadOnly.ListIncidents:input_type -> google.protobuf.Empty - 4, // 74: sa.StorageAuthorityReadOnly.ReplacementOrderExists:input_type -> sa.Serial - 33, // 75: sa.StorageAuthorityReadOnly.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest - 46, // 76: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:input_type -> sa.PauseRequest + 28, // 67: sa.StorageAuthorityReadOnly.GetSerialsByKey:input_type -> sa.SPKIHash + 2, // 68: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest + 19, // 69: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:input_type -> sa.GetOrderAuthorizationsRequest + 19, // 70: sa.StorageAuthorityReadOnly.GetOrderAuthorizations:input_type -> sa.GetOrderAuthorizationsRequest + 3, // 71: sa.StorageAuthorityReadOnly.IncidentsForSerial:input_type -> sa.Serial + 28, // 72: sa.StorageAuthorityReadOnly.KeyBlocked:input_type -> sa.SPKIHash + 60, // 73: sa.StorageAuthorityReadOnly.ListIncidents:input_type -> google.protobuf.Empty + 3, // 74: sa.StorageAuthorityReadOnly.ReplacementOrderExists:input_type -> sa.Serial + 31, // 75: sa.StorageAuthorityReadOnly.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest + 44, // 76: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:input_type -> sa.PauseRequest 0, // 77: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:input_type -> sa.RegistrationID - 54, // 78: sa.StorageAuthorityReadOnly.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest - 62, // 79: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty - 11, // 80: sa.StorageAuthority.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest - 10, // 81: sa.StorageAuthority.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest - 26, // 82: sa.StorageAuthority.GetAuthorization2:input_type -> sa.AuthorizationID2 - 4, // 83: sa.StorageAuthority.GetCertificate:input_type -> sa.Serial - 4, // 84: sa.StorageAuthority.GetLintPrecertificate:input_type -> sa.Serial - 4, // 85: sa.StorageAuthority.GetCertificateStatus:input_type -> sa.Serial - 15, // 86: sa.StorageAuthority.GetOrder:input_type -> sa.OrderRequest - 21, // 87: sa.StorageAuthority.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest + 52, // 78: sa.StorageAuthorityReadOnly.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest + 60, // 79: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty + 10, // 80: sa.StorageAuthority.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest + 9, // 81: sa.StorageAuthority.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest + 24, // 82: sa.StorageAuthority.GetAuthorization2:input_type -> sa.AuthorizationID2 + 3, // 83: sa.StorageAuthority.GetCertificate:input_type -> sa.Serial + 3, // 84: sa.StorageAuthority.GetLintPrecertificate:input_type -> sa.Serial + 3, // 85: sa.StorageAuthority.GetCertificateStatus:input_type -> sa.Serial + 14, // 86: sa.StorageAuthority.GetOrder:input_type -> sa.OrderRequest + 20, // 87: sa.StorageAuthority.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest 0, // 88: sa.StorageAuthority.GetRegistration:input_type -> sa.RegistrationID 1, // 89: sa.StorageAuthority.GetRegistrationByKey:input_type -> sa.JSONWebKey - 4, // 90: sa.StorageAuthority.GetRevocationStatus:input_type -> sa.Serial - 40, // 91: sa.StorageAuthority.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest - 4, // 92: sa.StorageAuthority.GetSerialMetadata:input_type -> sa.Serial + 3, // 90: sa.StorageAuthority.GetRevocationStatus:input_type -> sa.Serial + 38, // 91: sa.StorageAuthority.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest + 3, // 92: sa.StorageAuthority.GetSerialMetadata:input_type -> sa.Serial 0, // 93: sa.StorageAuthority.GetSerialsByAccount:input_type -> sa.RegistrationID - 30, // 94: sa.StorageAuthority.GetSerialsByKey:input_type -> sa.SPKIHash - 3, // 95: sa.StorageAuthority.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest - 20, // 96: sa.StorageAuthority.GetValidOrderAuthorizations2:input_type -> sa.GetOrderAuthorizationsRequest - 20, // 97: sa.StorageAuthority.GetOrderAuthorizations:input_type -> sa.GetOrderAuthorizationsRequest - 4, // 98: sa.StorageAuthority.IncidentsForSerial:input_type -> sa.Serial - 30, // 99: sa.StorageAuthority.KeyBlocked:input_type -> sa.SPKIHash - 62, // 100: sa.StorageAuthority.ListIncidents:input_type -> google.protobuf.Empty - 4, // 101: sa.StorageAuthority.ReplacementOrderExists:input_type -> sa.Serial - 33, // 102: sa.StorageAuthority.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest - 46, // 103: sa.StorageAuthority.CheckIdentifiersPaused:input_type -> sa.PauseRequest + 28, // 94: sa.StorageAuthority.GetSerialsByKey:input_type -> sa.SPKIHash + 2, // 95: sa.StorageAuthority.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest + 19, // 96: sa.StorageAuthority.GetValidOrderAuthorizations2:input_type -> sa.GetOrderAuthorizationsRequest + 19, // 97: sa.StorageAuthority.GetOrderAuthorizations:input_type -> sa.GetOrderAuthorizationsRequest + 3, // 98: sa.StorageAuthority.IncidentsForSerial:input_type -> sa.Serial + 28, // 99: sa.StorageAuthority.KeyBlocked:input_type -> sa.SPKIHash + 60, // 100: sa.StorageAuthority.ListIncidents:input_type -> google.protobuf.Empty + 3, // 101: sa.StorageAuthority.ReplacementOrderExists:input_type -> sa.Serial + 31, // 102: sa.StorageAuthority.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest + 44, // 103: sa.StorageAuthority.CheckIdentifiersPaused:input_type -> sa.PauseRequest 0, // 104: sa.StorageAuthority.GetPausedIdentifiers:input_type -> sa.RegistrationID - 54, // 105: sa.StorageAuthority.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest - 62, // 106: sa.StorageAuthority.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty - 29, // 107: sa.StorageAuthority.AddBlockedKey:input_type -> sa.AddBlockedKeyRequest - 14, // 108: sa.StorageAuthority.AddCertificate:input_type -> sa.AddCertificateRequest - 14, // 109: sa.StorageAuthority.AddPrecertificate:input_type -> sa.AddCertificateRequest - 13, // 110: sa.StorageAuthority.AddSerial:input_type -> sa.AddSerialRequest - 26, // 111: sa.StorageAuthority.DeactivateAuthorization2:input_type -> sa.AuthorizationID2 + 52, // 105: sa.StorageAuthority.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest + 60, // 106: sa.StorageAuthority.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty + 27, // 107: sa.StorageAuthority.AddBlockedKey:input_type -> sa.AddBlockedKeyRequest + 13, // 108: sa.StorageAuthority.AddCertificate:input_type -> sa.AddCertificateRequest + 13, // 109: sa.StorageAuthority.AddPrecertificate:input_type -> sa.AddCertificateRequest + 12, // 110: sa.StorageAuthority.AddSerial:input_type -> sa.AddSerialRequest + 24, // 111: sa.StorageAuthority.DeactivateAuthorization2:input_type -> sa.AuthorizationID2 0, // 112: sa.StorageAuthority.DeactivateRegistration:input_type -> sa.RegistrationID - 28, // 113: sa.StorageAuthority.FinalizeAuthorization2:input_type -> sa.FinalizeAuthorizationRequest - 22, // 114: sa.StorageAuthority.FinalizeOrder:input_type -> sa.FinalizeOrderRequest - 18, // 115: sa.StorageAuthority.NewOrderAndAuthzs:input_type -> sa.NewOrderAndAuthzsRequest - 63, // 116: sa.StorageAuthority.NewRegistration:input_type -> core.Registration - 27, // 117: sa.StorageAuthority.RevokeCertificate:input_type -> sa.RevokeCertificateRequest - 19, // 118: sa.StorageAuthority.SetOrderError:input_type -> sa.SetOrderErrorRequest - 15, // 119: sa.StorageAuthority.SetOrderProcessing:input_type -> sa.OrderRequest - 48, // 120: sa.StorageAuthority.UpdateRegistrationKey:input_type -> sa.UpdateRegistrationKeyRequest - 27, // 121: sa.StorageAuthority.UpdateRevokedCertificate:input_type -> sa.RevokeCertificateRequest - 42, // 122: sa.StorageAuthority.LeaseCRLShard:input_type -> sa.LeaseCRLShardRequest - 44, // 123: sa.StorageAuthority.UpdateCRLShard:input_type -> sa.UpdateCRLShardRequest - 46, // 124: sa.StorageAuthority.PauseIdentifiers:input_type -> sa.PauseRequest + 26, // 113: sa.StorageAuthority.FinalizeAuthorization2:input_type -> sa.FinalizeAuthorizationRequest + 21, // 114: sa.StorageAuthority.FinalizeOrder:input_type -> sa.FinalizeOrderRequest + 17, // 115: sa.StorageAuthority.NewOrderAndAuthzs:input_type -> sa.NewOrderAndAuthzsRequest + 61, // 116: sa.StorageAuthority.NewRegistration:input_type -> core.Registration + 25, // 117: sa.StorageAuthority.RevokeCertificate:input_type -> sa.RevokeCertificateRequest + 18, // 118: sa.StorageAuthority.SetOrderError:input_type -> sa.SetOrderErrorRequest + 14, // 119: sa.StorageAuthority.SetOrderProcessing:input_type -> sa.OrderRequest + 46, // 120: sa.StorageAuthority.UpdateRegistrationKey:input_type -> sa.UpdateRegistrationKeyRequest + 25, // 121: sa.StorageAuthority.UpdateRevokedCertificate:input_type -> sa.RevokeCertificateRequest + 40, // 122: sa.StorageAuthority.LeaseCRLShard:input_type -> sa.LeaseCRLShardRequest + 42, // 123: sa.StorageAuthority.UpdateCRLShard:input_type -> sa.UpdateCRLShardRequest + 44, // 124: sa.StorageAuthority.PauseIdentifiers:input_type -> sa.PauseRequest 0, // 125: sa.StorageAuthority.UnpauseAccount:input_type -> sa.RegistrationID - 50, // 126: sa.StorageAuthority.AddRateLimitOverride:input_type -> sa.AddRateLimitOverrideRequest - 53, // 127: sa.StorageAuthority.DisableRateLimitOverride:input_type -> sa.DisableRateLimitOverrideRequest - 52, // 128: sa.StorageAuthority.EnableRateLimitOverride:input_type -> sa.EnableRateLimitOverrideRequest - 34, // 129: sa.StorageAuthorityAdmin.CreateIncident:input_type -> sa.CreateIncidentRequest - 35, // 130: sa.StorageAuthorityAdmin.UpdateIncident:input_type -> sa.UpdateIncidentRequest - 36, // 131: sa.StorageAuthorityAdmin.AddSerialsToIncident:input_type -> sa.AddSerialsToIncidentRequest - 12, // 132: sa.StorageAuthorityReadOnly.FQDNSetExists:output_type -> sa.Exists - 8, // 133: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps - 60, // 134: sa.StorageAuthorityReadOnly.GetAuthorization2:output_type -> core.Authorization - 64, // 135: sa.StorageAuthorityReadOnly.GetCertificate:output_type -> core.Certificate - 64, // 136: sa.StorageAuthorityReadOnly.GetLintPrecertificate:output_type -> core.Certificate - 65, // 137: sa.StorageAuthorityReadOnly.GetCertificateStatus:output_type -> core.CertificateStatus - 66, // 138: sa.StorageAuthorityReadOnly.GetOrder:output_type -> core.Order - 66, // 139: sa.StorageAuthorityReadOnly.GetOrderForNames:output_type -> core.Order - 63, // 140: sa.StorageAuthorityReadOnly.GetRegistration:output_type -> core.Registration - 63, // 141: sa.StorageAuthorityReadOnly.GetRegistrationByKey:output_type -> core.Registration - 41, // 142: sa.StorageAuthorityReadOnly.GetRevocationStatus:output_type -> sa.RevocationStatus - 67, // 143: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:output_type -> core.CRLEntry - 5, // 144: sa.StorageAuthorityReadOnly.GetSerialMetadata:output_type -> sa.SerialMetadata - 4, // 145: sa.StorageAuthorityReadOnly.GetSerialsByAccount:output_type -> sa.Serial - 4, // 146: sa.StorageAuthorityReadOnly.GetSerialsByKey:output_type -> sa.Serial - 24, // 147: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:output_type -> sa.Authorizations - 24, // 148: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:output_type -> sa.Authorizations - 24, // 149: sa.StorageAuthorityReadOnly.GetOrderAuthorizations:output_type -> sa.Authorizations - 32, // 150: sa.StorageAuthorityReadOnly.IncidentsForSerial:output_type -> sa.Incidents - 12, // 151: sa.StorageAuthorityReadOnly.KeyBlocked:output_type -> sa.Exists - 32, // 152: sa.StorageAuthorityReadOnly.ListIncidents:output_type -> sa.Incidents - 12, // 153: sa.StorageAuthorityReadOnly.ReplacementOrderExists:output_type -> sa.Exists - 39, // 154: sa.StorageAuthorityReadOnly.SerialsForIncident:output_type -> sa.IncidentSerial - 45, // 155: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:output_type -> sa.Identifiers - 45, // 156: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:output_type -> sa.Identifiers - 55, // 157: sa.StorageAuthorityReadOnly.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse - 55, // 158: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverrideResponse - 12, // 159: sa.StorageAuthority.FQDNSetExists:output_type -> sa.Exists - 8, // 160: sa.StorageAuthority.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps - 60, // 161: sa.StorageAuthority.GetAuthorization2:output_type -> core.Authorization - 64, // 162: sa.StorageAuthority.GetCertificate:output_type -> core.Certificate - 64, // 163: sa.StorageAuthority.GetLintPrecertificate:output_type -> core.Certificate - 65, // 164: sa.StorageAuthority.GetCertificateStatus:output_type -> core.CertificateStatus - 66, // 165: sa.StorageAuthority.GetOrder:output_type -> core.Order - 66, // 166: sa.StorageAuthority.GetOrderForNames:output_type -> core.Order - 63, // 167: sa.StorageAuthority.GetRegistration:output_type -> core.Registration - 63, // 168: sa.StorageAuthority.GetRegistrationByKey:output_type -> core.Registration - 41, // 169: sa.StorageAuthority.GetRevocationStatus:output_type -> sa.RevocationStatus - 67, // 170: sa.StorageAuthority.GetRevokedCertsByShard:output_type -> core.CRLEntry - 5, // 171: sa.StorageAuthority.GetSerialMetadata:output_type -> sa.SerialMetadata - 4, // 172: sa.StorageAuthority.GetSerialsByAccount:output_type -> sa.Serial - 4, // 173: sa.StorageAuthority.GetSerialsByKey:output_type -> sa.Serial - 24, // 174: sa.StorageAuthority.GetValidAuthorizations2:output_type -> sa.Authorizations - 24, // 175: sa.StorageAuthority.GetValidOrderAuthorizations2:output_type -> sa.Authorizations - 24, // 176: sa.StorageAuthority.GetOrderAuthorizations:output_type -> sa.Authorizations - 32, // 177: sa.StorageAuthority.IncidentsForSerial:output_type -> sa.Incidents - 12, // 178: sa.StorageAuthority.KeyBlocked:output_type -> sa.Exists - 32, // 179: sa.StorageAuthority.ListIncidents:output_type -> sa.Incidents - 12, // 180: sa.StorageAuthority.ReplacementOrderExists:output_type -> sa.Exists - 39, // 181: sa.StorageAuthority.SerialsForIncident:output_type -> sa.IncidentSerial - 45, // 182: sa.StorageAuthority.CheckIdentifiersPaused:output_type -> sa.Identifiers - 45, // 183: sa.StorageAuthority.GetPausedIdentifiers:output_type -> sa.Identifiers - 55, // 184: sa.StorageAuthority.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse - 55, // 185: sa.StorageAuthority.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverrideResponse - 62, // 186: sa.StorageAuthority.AddBlockedKey:output_type -> google.protobuf.Empty - 62, // 187: sa.StorageAuthority.AddCertificate:output_type -> google.protobuf.Empty - 62, // 188: sa.StorageAuthority.AddPrecertificate:output_type -> google.protobuf.Empty - 62, // 189: sa.StorageAuthority.AddSerial:output_type -> google.protobuf.Empty - 62, // 190: sa.StorageAuthority.DeactivateAuthorization2:output_type -> google.protobuf.Empty - 63, // 191: sa.StorageAuthority.DeactivateRegistration:output_type -> core.Registration - 62, // 192: sa.StorageAuthority.FinalizeAuthorization2:output_type -> google.protobuf.Empty - 62, // 193: sa.StorageAuthority.FinalizeOrder:output_type -> google.protobuf.Empty - 66, // 194: sa.StorageAuthority.NewOrderAndAuthzs:output_type -> core.Order - 63, // 195: sa.StorageAuthority.NewRegistration:output_type -> core.Registration - 62, // 196: sa.StorageAuthority.RevokeCertificate:output_type -> google.protobuf.Empty - 62, // 197: sa.StorageAuthority.SetOrderError:output_type -> google.protobuf.Empty - 62, // 198: sa.StorageAuthority.SetOrderProcessing:output_type -> google.protobuf.Empty - 63, // 199: sa.StorageAuthority.UpdateRegistrationKey:output_type -> core.Registration - 62, // 200: sa.StorageAuthority.UpdateRevokedCertificate:output_type -> google.protobuf.Empty - 43, // 201: sa.StorageAuthority.LeaseCRLShard:output_type -> sa.LeaseCRLShardResponse - 62, // 202: sa.StorageAuthority.UpdateCRLShard:output_type -> google.protobuf.Empty - 47, // 203: sa.StorageAuthority.PauseIdentifiers:output_type -> sa.PauseIdentifiersResponse - 7, // 204: sa.StorageAuthority.UnpauseAccount:output_type -> sa.Count - 51, // 205: sa.StorageAuthority.AddRateLimitOverride:output_type -> sa.AddRateLimitOverrideResponse - 62, // 206: sa.StorageAuthority.DisableRateLimitOverride:output_type -> google.protobuf.Empty - 62, // 207: sa.StorageAuthority.EnableRateLimitOverride:output_type -> google.protobuf.Empty - 31, // 208: sa.StorageAuthorityAdmin.CreateIncident:output_type -> sa.Incident - 31, // 209: sa.StorageAuthorityAdmin.UpdateIncident:output_type -> sa.Incident - 62, // 210: sa.StorageAuthorityAdmin.AddSerialsToIncident:output_type -> google.protobuf.Empty + 48, // 126: sa.StorageAuthority.AddRateLimitOverride:input_type -> sa.AddRateLimitOverrideRequest + 51, // 127: sa.StorageAuthority.DisableRateLimitOverride:input_type -> sa.DisableRateLimitOverrideRequest + 50, // 128: sa.StorageAuthority.EnableRateLimitOverride:input_type -> sa.EnableRateLimitOverrideRequest + 32, // 129: sa.StorageAuthorityAdmin.CreateIncident:input_type -> sa.CreateIncidentRequest + 33, // 130: sa.StorageAuthorityAdmin.UpdateIncident:input_type -> sa.UpdateIncidentRequest + 34, // 131: sa.StorageAuthorityAdmin.AddSerialsToIncident:input_type -> sa.AddSerialsToIncidentRequest + 11, // 132: sa.StorageAuthorityReadOnly.FQDNSetExists:output_type -> sa.Exists + 7, // 133: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps + 58, // 134: sa.StorageAuthorityReadOnly.GetAuthorization2:output_type -> core.Authorization + 62, // 135: sa.StorageAuthorityReadOnly.GetCertificate:output_type -> core.Certificate + 62, // 136: sa.StorageAuthorityReadOnly.GetLintPrecertificate:output_type -> core.Certificate + 63, // 137: sa.StorageAuthorityReadOnly.GetCertificateStatus:output_type -> core.CertificateStatus + 64, // 138: sa.StorageAuthorityReadOnly.GetOrder:output_type -> core.Order + 64, // 139: sa.StorageAuthorityReadOnly.GetOrderForNames:output_type -> core.Order + 61, // 140: sa.StorageAuthorityReadOnly.GetRegistration:output_type -> core.Registration + 61, // 141: sa.StorageAuthorityReadOnly.GetRegistrationByKey:output_type -> core.Registration + 39, // 142: sa.StorageAuthorityReadOnly.GetRevocationStatus:output_type -> sa.RevocationStatus + 65, // 143: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:output_type -> core.CRLEntry + 4, // 144: sa.StorageAuthorityReadOnly.GetSerialMetadata:output_type -> sa.SerialMetadata + 3, // 145: sa.StorageAuthorityReadOnly.GetSerialsByAccount:output_type -> sa.Serial + 3, // 146: sa.StorageAuthorityReadOnly.GetSerialsByKey:output_type -> sa.Serial + 23, // 147: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:output_type -> sa.Authorizations + 23, // 148: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:output_type -> sa.Authorizations + 23, // 149: sa.StorageAuthorityReadOnly.GetOrderAuthorizations:output_type -> sa.Authorizations + 30, // 150: sa.StorageAuthorityReadOnly.IncidentsForSerial:output_type -> sa.Incidents + 11, // 151: sa.StorageAuthorityReadOnly.KeyBlocked:output_type -> sa.Exists + 30, // 152: sa.StorageAuthorityReadOnly.ListIncidents:output_type -> sa.Incidents + 11, // 153: sa.StorageAuthorityReadOnly.ReplacementOrderExists:output_type -> sa.Exists + 37, // 154: sa.StorageAuthorityReadOnly.SerialsForIncident:output_type -> sa.IncidentSerial + 43, // 155: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:output_type -> sa.Identifiers + 43, // 156: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:output_type -> sa.Identifiers + 53, // 157: sa.StorageAuthorityReadOnly.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse + 53, // 158: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverrideResponse + 11, // 159: sa.StorageAuthority.FQDNSetExists:output_type -> sa.Exists + 7, // 160: sa.StorageAuthority.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps + 58, // 161: sa.StorageAuthority.GetAuthorization2:output_type -> core.Authorization + 62, // 162: sa.StorageAuthority.GetCertificate:output_type -> core.Certificate + 62, // 163: sa.StorageAuthority.GetLintPrecertificate:output_type -> core.Certificate + 63, // 164: sa.StorageAuthority.GetCertificateStatus:output_type -> core.CertificateStatus + 64, // 165: sa.StorageAuthority.GetOrder:output_type -> core.Order + 64, // 166: sa.StorageAuthority.GetOrderForNames:output_type -> core.Order + 61, // 167: sa.StorageAuthority.GetRegistration:output_type -> core.Registration + 61, // 168: sa.StorageAuthority.GetRegistrationByKey:output_type -> core.Registration + 39, // 169: sa.StorageAuthority.GetRevocationStatus:output_type -> sa.RevocationStatus + 65, // 170: sa.StorageAuthority.GetRevokedCertsByShard:output_type -> core.CRLEntry + 4, // 171: sa.StorageAuthority.GetSerialMetadata:output_type -> sa.SerialMetadata + 3, // 172: sa.StorageAuthority.GetSerialsByAccount:output_type -> sa.Serial + 3, // 173: sa.StorageAuthority.GetSerialsByKey:output_type -> sa.Serial + 23, // 174: sa.StorageAuthority.GetValidAuthorizations2:output_type -> sa.Authorizations + 23, // 175: sa.StorageAuthority.GetValidOrderAuthorizations2:output_type -> sa.Authorizations + 23, // 176: sa.StorageAuthority.GetOrderAuthorizations:output_type -> sa.Authorizations + 30, // 177: sa.StorageAuthority.IncidentsForSerial:output_type -> sa.Incidents + 11, // 178: sa.StorageAuthority.KeyBlocked:output_type -> sa.Exists + 30, // 179: sa.StorageAuthority.ListIncidents:output_type -> sa.Incidents + 11, // 180: sa.StorageAuthority.ReplacementOrderExists:output_type -> sa.Exists + 37, // 181: sa.StorageAuthority.SerialsForIncident:output_type -> sa.IncidentSerial + 43, // 182: sa.StorageAuthority.CheckIdentifiersPaused:output_type -> sa.Identifiers + 43, // 183: sa.StorageAuthority.GetPausedIdentifiers:output_type -> sa.Identifiers + 53, // 184: sa.StorageAuthority.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse + 53, // 185: sa.StorageAuthority.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverrideResponse + 60, // 186: sa.StorageAuthority.AddBlockedKey:output_type -> google.protobuf.Empty + 60, // 187: sa.StorageAuthority.AddCertificate:output_type -> google.protobuf.Empty + 60, // 188: sa.StorageAuthority.AddPrecertificate:output_type -> google.protobuf.Empty + 60, // 189: sa.StorageAuthority.AddSerial:output_type -> google.protobuf.Empty + 60, // 190: sa.StorageAuthority.DeactivateAuthorization2:output_type -> google.protobuf.Empty + 61, // 191: sa.StorageAuthority.DeactivateRegistration:output_type -> core.Registration + 60, // 192: sa.StorageAuthority.FinalizeAuthorization2:output_type -> google.protobuf.Empty + 60, // 193: sa.StorageAuthority.FinalizeOrder:output_type -> google.protobuf.Empty + 64, // 194: sa.StorageAuthority.NewOrderAndAuthzs:output_type -> core.Order + 61, // 195: sa.StorageAuthority.NewRegistration:output_type -> core.Registration + 60, // 196: sa.StorageAuthority.RevokeCertificate:output_type -> google.protobuf.Empty + 60, // 197: sa.StorageAuthority.SetOrderError:output_type -> google.protobuf.Empty + 60, // 198: sa.StorageAuthority.SetOrderProcessing:output_type -> google.protobuf.Empty + 61, // 199: sa.StorageAuthority.UpdateRegistrationKey:output_type -> core.Registration + 60, // 200: sa.StorageAuthority.UpdateRevokedCertificate:output_type -> google.protobuf.Empty + 41, // 201: sa.StorageAuthority.LeaseCRLShard:output_type -> sa.LeaseCRLShardResponse + 60, // 202: sa.StorageAuthority.UpdateCRLShard:output_type -> google.protobuf.Empty + 45, // 203: sa.StorageAuthority.PauseIdentifiers:output_type -> sa.PauseIdentifiersResponse + 6, // 204: sa.StorageAuthority.UnpauseAccount:output_type -> sa.Count + 49, // 205: sa.StorageAuthority.AddRateLimitOverride:output_type -> sa.AddRateLimitOverrideResponse + 60, // 206: sa.StorageAuthority.DisableRateLimitOverride:output_type -> google.protobuf.Empty + 60, // 207: sa.StorageAuthority.EnableRateLimitOverride:output_type -> google.protobuf.Empty + 29, // 208: sa.StorageAuthorityAdmin.CreateIncident:output_type -> sa.Incident + 29, // 209: sa.StorageAuthorityAdmin.UpdateIncident:output_type -> sa.Incident + 60, // 210: sa.StorageAuthorityAdmin.AddSerialsToIncident:output_type -> google.protobuf.Empty 132, // [132:211] is the sub-list for method output_type 53, // [53:132] is the sub-list for method input_type 53, // [53:53] is the sub-list for extension type_name @@ -4389,8 +4294,8 @@ func file_sa_proto_init() { if File_sa_proto != nil { return } - file_sa_proto_msgTypes[35].OneofWrappers = []any{} - file_sa_proto_msgTypes[36].OneofWrappers = []any{ + file_sa_proto_msgTypes[33].OneofWrappers = []any{} + file_sa_proto_msgTypes[34].OneofWrappers = []any{ (*AddSerialsToIncidentRequest_Metadata)(nil), (*AddSerialsToIncidentRequest_Batch)(nil), } @@ -4400,7 +4305,7 @@ func file_sa_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sa_proto_rawDesc), len(file_sa_proto_rawDesc)), NumEnums: 0, - NumMessages: 56, + NumMessages: 54, NumExtensions: 0, NumServices: 3, }, diff --git a/sa/proto/sa.proto b/sa/proto/sa.proto index fa502978795..3122b98fb02 100644 --- a/sa/proto/sa.proto +++ b/sa/proto/sa.proto @@ -110,10 +110,6 @@ message JSONWebKey { bytes jwk = 1; } -message AuthorizationID { - string id = 1; -} - message GetValidAuthorizationsRequest { // Next unused field number: 7 int64 registrationID = 1; @@ -293,10 +289,6 @@ message Authorizations { repeated core.Authorization authzs = 2; } -message AuthorizationIDs { - repeated string ids = 1; -} - message AuthorizationID2 { int64 id = 1; } diff --git a/sa/sa.go b/sa/sa.go index 5d941030183..f58ccd817bf 100644 --- a/sa/sa.go +++ b/sa/sa.go @@ -19,13 +19,13 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/unpause" @@ -370,7 +370,7 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.Ad // but don't return an error from AddCertificate. if fqdnTransactionErr != nil { ssa.rateLimitWriteErrors.Inc() - ssa.log.Errf("failed AddCertificate FQDN sets insert transaction: %v", fqdnTransactionErr) + ssa.log.Error(ctx, "failed AddCertificate FQDN sets insert transaction", fqdnTransactionErr) } return &emptypb.Empty{}, nil diff --git a/sa/sa_test.go b/sa/sa_test.go index d9040e1de70..3c19459ce1a 100644 --- a/sa/sa_test.go +++ b/sa/sa_test.go @@ -35,6 +35,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" @@ -42,7 +43,6 @@ import ( "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/revocation" @@ -51,7 +51,6 @@ import ( "github.com/letsencrypt/boulder/test/vars" ) -var log = blog.UseMock() var ctx = context.Background() var ( @@ -107,7 +106,7 @@ func initSA(t testing.TB) (*SQLStorageAuthority, clock.FakeClock) { fc := clock.NewFake() fc.Set(mustTime("2015-03-04 05:00")) - saro, err := NewSQLStorageAuthorityRO(dbMap, dbIncidentsMap, metrics.NoopRegisterer, 0, fc, log) + saro, err := NewSQLStorageAuthorityRO(dbMap, dbIncidentsMap, metrics.NoopRegisterer, 0, fc, blog.NewMock()) if err != nil { t.Fatalf("Failed to create SA: %s", err) } diff --git a/sa/saa.go b/sa/saa.go index 5c1fa7e8250..b4182fc347d 100644 --- a/sa/saa.go +++ b/sa/saa.go @@ -5,15 +5,16 @@ import ( "errors" "fmt" "io" + "log/slog" "slices" "strings" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -193,7 +194,7 @@ func (ssa *SQLStorageAuthorityAdmin) AddSerialsToIncident(stream sapb.StorageAut if err != nil { return fmt.Errorf("inserting batch into %q: %w", incidentTable, err) } - ssa.log.Infof("AddSerialsToIncident %q: batch of %d, %d inserted", incidentTable, len(serials), inserted) + ssa.log.Info(ctx, "Added serials to incident", slog.String("incident", incidentTable), slog.Int("count", len(serials)), slog.Int64("inserted", inserted)) return nil } diff --git a/sa/saa_test.go b/sa/saa_test.go index 2bf7074f7c1..0bbabe898a2 100644 --- a/sa/saa_test.go +++ b/sa/saa_test.go @@ -13,6 +13,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" sapb "github.com/letsencrypt/boulder/sa/proto" @@ -78,7 +79,7 @@ func initSAAdmin(t *testing.T) (*SQLStorageAuthorityAdmin, *db.WrappedMap, *db.W t.Fatalf("Failed to create dbIncidentsAdminMap: %s", err) } - saa, err := NewSQLStorageAuthorityAdmin(dbMap, dbIncidentsAdminMap, log) + saa, err := NewSQLStorageAuthorityAdmin(dbMap, dbIncidentsAdminMap, blog.NewMock()) if err != nil { t.Fatalf("Failed to create SA admin impl: %s", err) } diff --git a/sa/saro.go b/sa/saro.go index 8ce156920d7..c1d4bc952f3 100644 --- a/sa/saro.go +++ b/sa/saro.go @@ -18,12 +18,12 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -903,7 +903,10 @@ func (ssa *SQLStorageAuthorityRO) ReplacementOrderExists(ctx context.Context, re if errors.Is(err, berrors.NotFound) { // The existing replacement order has been deleted. This should // never happen. - ssa.log.Errf("replacement order %d for serial %q not found", replacement.OrderID, req.Serial) + ssa.log.Error(ctx, "replacement order not found", err, + blog.Order(replacement.OrderID), + blog.Serial(req.Serial), + ) return &sapb.Exists{Exists: false}, nil } } diff --git a/salesforce/exporter.go b/salesforce/exporter.go index bb69b156106..6c0aec2362a 100644 --- a/salesforce/exporter.go +++ b/salesforce/exporter.go @@ -10,9 +10,9 @@ import ( "golang.org/x/time/rate" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" emailpb "github.com/letsencrypt/boulder/salesforce/email/proto" ) @@ -117,10 +117,10 @@ func (impl *ExporterImpl) SendContacts(ctx context.Context, req *emailpb.SendCon } // Start begins asynchronous processing of the email queue. When the parent -// daemonCtx is cancelled the queue will be drained and the workers will exit. -func (impl *ExporterImpl) Start(daemonCtx context.Context) { +// context is cancelled the queue will be drained and the workers will exit. +func (impl *ExporterImpl) Start(ctx context.Context) { go func() { - <-daemonCtx.Done() + <-ctx.Done() // Wake waiting workers to exit. impl.wake.Broadcast() }() @@ -130,12 +130,12 @@ func (impl *ExporterImpl) Start(daemonCtx context.Context) { for { impl.Lock() - for len(impl.toSend) == 0 && daemonCtx.Err() == nil { + for len(impl.toSend) == 0 && ctx.Err() == nil { // Wait for the queue to be updated or the daemon to exit. impl.wake.Wait() } - if len(impl.toSend) == 0 && daemonCtx.Err() != nil { + if len(impl.toSend) == 0 && ctx.Err() != nil { // No more emails to process, exit. impl.Unlock() return @@ -152,9 +152,9 @@ func (impl *ExporterImpl) Start(daemonCtx context.Context) { continue } - err := impl.limiter.Wait(daemonCtx) + err := impl.limiter.Wait(ctx) if err != nil && !errors.Is(err, context.Canceled) { - impl.log.Errf("Unexpected limiter.Wait() error: %s", err) + impl.log.Error(ctx, "Unexpected limiter.Wait() error", err) continue } @@ -162,7 +162,7 @@ func (impl *ExporterImpl) Start(daemonCtx context.Context) { if err != nil { impl.emailCache.Remove(email) impl.pardotErrorCounter.Inc() - impl.log.Errf("Sending Contact to Pardot: %s", err) + impl.log.Error(ctx, "Sending Contact to Pardot", err) } else { impl.emailsHandledCounter.Inc() } diff --git a/salesforce/exporter_test.go b/salesforce/exporter_test.go index 4cdf7993aba..bd427179c2e 100644 --- a/salesforce/exporter_test.go +++ b/salesforce/exporter_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/metrics" emailpb "github.com/letsencrypt/boulder/salesforce/email/proto" "github.com/letsencrypt/boulder/test" diff --git a/sfe/overrides.go b/sfe/overrides.go index 5292ea4f7d7..518a505e709 100644 --- a/sfe/overrides.go +++ b/sfe/overrides.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "html/template" + "log/slog" "net/http" "net/url" "slices" @@ -433,7 +434,7 @@ func setOverrideRequestFormHeaders(w http.ResponseWriter) { // method that allows it to be used as an http.HandlerFunc. func (sfe *SelfServiceFrontEndImpl) makeOverrideRequestFormHandler(formHTML template.HTML, rateLimit, displayRateLimit string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - sfe.overrideRequestHandler(w, formHTML, rateLimit, displayRateLimit) + sfe.overrideRequestHandler(r.Context(), w, formHTML, rateLimit, displayRateLimit) } } @@ -443,9 +444,9 @@ func (sfe *SelfServiceFrontEndImpl) makeOverrideRequestFormHandler(formHTML temp // is the limit that will be displayed to the user in the form. These are // typically the same, but can differ in cases where multiple forms are used for // the same rate limit. -func (sfe *SelfServiceFrontEndImpl) overrideRequestHandler(w http.ResponseWriter, formHTML template.HTML, rateLimit, displayRateLimit string) { +func (sfe *SelfServiceFrontEndImpl) overrideRequestHandler(ctx context.Context, w http.ResponseWriter, formHTML template.HTML, rateLimit, displayRateLimit string) { setOverrideRequestFormHeaders(w) - sfe.renderTemplate(w, "overrideForm.html", map[string]any{ + sfe.renderTemplate(ctx, w, "overrideForm.html", map[string]any{ "FormHTML": formHTML, "RateLimit": rateLimit, "DisplayRateLimit": displayRateLimit, @@ -475,7 +476,7 @@ func (sfe *SelfServiceFrontEndImpl) validateOverrideFieldHandler(w http.Response var req validationRequest err := json.NewDecoder(http.MaxBytesReader(w, r.Body, validateOverrideFieldBodyLimit)).Decode(&req) if err != nil { - sfe.log.Errf("failed to decode validation request: %s", err) + sfe.log.Error(r.Context(), "failed to decode validation request", err) http.Error(w, "bad request", http.StatusBadRequest) return } @@ -494,7 +495,7 @@ func (sfe *SelfServiceFrontEndImpl) validateOverrideFieldHandler(w http.Response Error: message, }) if err != nil { - sfe.log.Errf("failed to encode validation response: %s", err) + sfe.log.Error(r.Context(), "failed to encode validation response", err) http.Error(w, "failed to encode validation response", http.StatusInternalServerError) return } @@ -503,14 +504,14 @@ func (sfe *SelfServiceFrontEndImpl) validateOverrideFieldHandler(w http.Response // overrideAutoApprovedSuccessHandler renders the success page after a // successful override request submission which was automatically approved. func (sfe *SelfServiceFrontEndImpl) overrideAutoApprovedSuccessHandler(w http.ResponseWriter, r *http.Request) { - sfe.renderTemplate(w, "overrideAutoApprovedSuccess.html", nil) + sfe.renderTemplate(r.Context(), w, "overrideAutoApprovedSuccess.html", nil) } // overrideRequestSubmittedSuccessHandler renders the success page after a // successful override request submission created a Zendesk ticket for manual // review. func (sfe *SelfServiceFrontEndImpl) overrideRequestSubmittedSuccessHandler(w http.ResponseWriter, r *http.Request) { - sfe.renderTemplate(w, "overrideRequestSubmittedSuccess.html", nil) + sfe.renderTemplate(r.Context(), w, "overrideRequestSubmittedSuccess.html", nil) } type overrideRequest struct { @@ -532,25 +533,26 @@ type overrideRequest struct { // either the form logic is flawed or the requester has bypassed the form and // submitting (malformed) requests directly to this endpoint. func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() var refundLimits func() if sfe.limiter != nil && sfe.txnBuilder != nil { requesterIP, err := web.ExtractRequesterIP(r) if err != nil { - sfe.log.Errf("failed to determine requester IP address: %s", err) + sfe.log.Error(ctx, "failed to determine requester IP address", err) http.Error(w, "failed to determine the IP address of the requester", http.StatusInternalServerError) return } txns, err := sfe.txnBuilder.LimitOverrideRequestsPerIPAddressTransaction(requesterIP) if err != nil { - sfe.log.Errf("failed to build transaction for override request form limits: %s", err) + sfe.log.Error(ctx, "failed to build transaction for override request form limits", err) http.Error(w, "failed to build transaction for override request form limits", http.StatusInternalServerError) return } - d, err := sfe.limiter.Spend(r.Context(), txns) + d, err := sfe.limiter.Spend(ctx, txns) if err != nil { - sfe.log.Errf("failed to spend transaction for override request form limits: %s", err) + sfe.log.Error(ctx, "failed to spend transaction for override request form limits", err) http.Error(w, "failed to spend transaction for override request form limits", http.StatusInternalServerError) return } @@ -562,15 +564,15 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, bErr.Detail, http.StatusTooManyRequests) return } - sfe.log.Errf("failed to determine result of override request form limits transaction: %s", err) + sfe.log.Error(ctx, "failed to determine result of override request form limits transaction", err) http.Error(w, "failed to determine result of override request form limits transaction", http.StatusInternalServerError) return } refundLimits = func() { - _, err := sfe.limiter.Refund(r.Context(), txns) + _, err := sfe.limiter.Refund(ctx, txns) if err != nil { - sfe.log.Errf("failed to refund transaction for override request form limits: %s", err) + sfe.log.Error(ctx, "failed to refund transaction for override request form limits", err) } } } @@ -584,7 +586,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response var req overrideRequest err := json.NewDecoder(http.MaxBytesReader(w, r.Body, submitOverrideRequestBodyLimit)).Decode(&req) if err != nil { - sfe.log.Errf("failed to decode override request: %s", err) + sfe.log.Error(ctx, "failed to decode override request", err) http.Error(w, "bad request", http.StatusBadRequest) return } @@ -628,12 +630,12 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response } req, _, err := makeAddOverrideRequest(rateLimitFieldValue, fields) if err != nil { - sfe.log.Errf("failed to create automatically approved override request: %s", err) + sfe.log.Error(ctx, "failed to create automatically approved override request", err) return false, nil } resp, err := sfe.ra.AddRateLimitOverride(ctx, req) if err != nil { - sfe.log.Errf("failed to create automatically approved override request: %s", err) + sfe.log.Error(ctx, "failed to create automatically approved override request", err) return false, nil } return resp.Enabled, resp.Existing @@ -650,7 +652,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response validFields[AccountURIFieldName] = accountURI if validFields[TierFieldName] == newOrdersPerAccountTierOptions[0] { - requestHandled, existingOverride = autoApproveOverride(r.Context(), req.RateLimit, validFields) + requestHandled, existingOverride = autoApproveOverride(ctx, req.RateLimit, validFields) } case rl.CertificatesPerDomainPerAccount.String(): @@ -662,7 +664,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response validFields[AccountURIFieldName] = accountURI if validFields[TierFieldName] == certificatesPerDomainPerAccountTierOptions[0] { - requestHandled, existingOverride = autoApproveOverride(r.Context(), req.RateLimit, validFields) + requestHandled, existingOverride = autoApproveOverride(ctx, req.RateLimit, validFields) } case rl.CertificatesPerDomain.String() + perDNSNameSuffix: @@ -674,7 +676,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response validFields[RegisteredDomainFieldName] = registeredDomain if validFields[TierFieldName] == certificatesPerDomainTierOptions[0] { - requestHandled, existingOverride = autoApproveOverride(r.Context(), req.RateLimit, validFields) + requestHandled, existingOverride = autoApproveOverride(ctx, req.RateLimit, validFields) } case rl.CertificatesPerDomain.String() + perIPSuffix: @@ -686,7 +688,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response validFields[IPAddressFieldName] = ipAddress if validFields[TierFieldName] == certificatesPerDomainTierOptions[0] { - requestHandled, existingOverride = autoApproveOverride(r.Context(), req.RateLimit, validFields) + requestHandled, existingOverride = autoApproveOverride(ctx, req.RateLimit, validFields) } default: @@ -695,14 +697,14 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response } if sfe.ee != nil && validFields[mailingListFieldName] == "true" { - _, err := sfe.ee.SendContacts(r.Context(), &emailpb.SendContactsRequest{Emails: []string{validFields[emailAddressFieldName]}}) + _, err := sfe.ee.SendContacts(ctx, &emailpb.SendContactsRequest{Emails: []string{validFields[emailAddressFieldName]}}) if err != nil { - sfe.log.Errf("failed to send contact to email-exporter: %s", err) + sfe.log.Error(ctx, "failed to send contact to email-exporter", err) } } if requestHandled { - sfe.log.Infof("automatically approved override request for %s", validFields[OrganizationFieldName]) + sfe.log.Info(ctx, "automatically approved override request", slog.String("organization", validFields[OrganizationFieldName])) w.WriteHeader(http.StatusCreated) return } @@ -722,7 +724,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response validFields[IPAddressFieldName], ) if err != nil { - sfe.log.Errf("failed to create override request Zendesk ticket: %s", err) + sfe.log.Error(ctx, "failed to create override request Zendesk ticket", err) http.Error(w, "failed to create support ticket", http.StatusInternalServerError) return } @@ -751,13 +753,13 @@ Requester-provided email: %s`, ) err := sfe.zendeskClient.AddComment(ticketID, privateBody, false) if err != nil { - sfe.log.Errf("failed to add Zendesk comment to ticket %d: %s", ticketID, err) + sfe.log.Error(ctx, "failed to add Zendesk comment to ticket", err, slog.Int64("ticketID", ticketID)) } } // If we got here the request has either been auto-approved or a Zendesk // ticket has been created for manual review, so a refund is not needed. requestHandled = true - sfe.log.Infof("created override request Zendesk ticket %d", ticketID) + sfe.log.Info(ctx, "created override request Zendesk ticket", slog.Int64("ticketID", ticketID)) w.WriteHeader(http.StatusAccepted) } diff --git a/sfe/overrides_test.go b/sfe/overrides_test.go index e14a40d97eb..bbf08b4baef 100644 --- a/sfe/overrides_test.go +++ b/sfe/overrides_test.go @@ -11,12 +11,13 @@ import ( "strings" "testing" + "google.golang.org/grpc" + "github.com/letsencrypt/boulder/mocks" rapb "github.com/letsencrypt/boulder/ra/proto" rl "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/sfe/zendesk" "github.com/letsencrypt/boulder/test/zendeskfake" - "google.golang.org/grpc" ) const ( diff --git a/sfe/overridesimporter.go b/sfe/overridesimporter.go index 1678774328d..6542e844998 100644 --- a/sfe/overridesimporter.go +++ b/sfe/overridesimporter.go @@ -3,6 +3,7 @@ package sfe import ( "context" "fmt" + "log/slog" "net/netip" "net/url" "strconv" @@ -12,8 +13,8 @@ import ( "github.com/jmhodges/clock" "google.golang.org/protobuf/types/known/durationpb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" rapb "github.com/letsencrypt/boulder/ra/proto" rl "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/sfe/zendesk" @@ -79,7 +80,7 @@ func (im *OverridesImporter) Start(ctx context.Context) { func (im *OverridesImporter) tick(ctx context.Context) { tickets, err := im.zendesk.FindTickets(map[string]string{ReviewStatusFieldName: reviewStatusApproved}, "open") if err != nil { - im.log.Errf("while searching zendesk for solved and approved tickets: %s", err) + im.log.Error(ctx, "while searching zendesk for solved and approved tickets", err) return } @@ -99,13 +100,16 @@ func (im *OverridesImporter) tick(ctx context.Context) { err = im.processTicket(ctx, id, fields) if err != nil { - im.log.Errf("while processing ticket %d: %s", id, err) + im.log.Error(ctx, "while processing ticket", err, slog.Int64("ticket", id)) failures++ continue } processed++ } - im.log.Infof("overrides importer processed %d tickets with %d failures", processed, failures) + im.log.Info(ctx, "overrides importer tick complete", + slog.Int("processed", processed), + slog.Int("failures", failures), + ) } func accountURIToID(s string) (int64, error) { @@ -120,7 +124,7 @@ func accountURIToID(s string) (int64, error) { // transitionToPendingWithComment sets the status of the given ticket to // "pending" and adds a private comment with the given cause. If updating the // ticket fails, the error is logged. -func (im *OverridesImporter) transitionToPendingWithComment(ticketID int64, cause string) { +func (im *OverridesImporter) transitionToPendingWithComment(ctx context.Context, ticketID int64, cause string) { privateBody := fmt.Sprintf( "A failure occurred while importing this override:\n\n%s\n\n"+ "This ticket's status has been set to pending.\n\n"+ @@ -129,7 +133,7 @@ func (im *OverridesImporter) transitionToPendingWithComment(ticketID int64, caus ) err := im.zendesk.UpdateTicketStatus(ticketID, "pending", privateBody, false) if err != nil { - im.log.Errf("failed to update ticket %d: %s", ticketID, err) + im.log.Error(ctx, "failed to update ticket", err, slog.Int64("ticket", ticketID)) } } @@ -246,7 +250,7 @@ func (im *OverridesImporter) processTicket(ctx context.Context, ticketID int64, req, accountDomainOrIP, err := makeAddOverrideRequest(fields[RateLimitFieldName], fields) if err != nil { // Move to "pending" so the next tick won't comment again. - im.transitionToPendingWithComment(ticketID, err.Error()) + im.transitionToPendingWithComment(ctx, ticketID, err.Error()) return fmt.Errorf("preparing override request: %w", err) } @@ -277,12 +281,12 @@ ignore the request entirely.`, resp.Existing.Period.AsDuration(), resp.Existing.Comment, ) - im.transitionToPendingWithComment(ticketID, privateBody) + im.transitionToPendingWithComment(ctx, ticketID, privateBody) return fmt.Errorf("override for rate limit %s and account/domain/IP: %s is lower than existing override", rateLimit, accountDomainOrIP) } // Move to "pending" so the next tick won't comment again. - im.transitionToPendingWithComment(ticketID, "An existing override for this limit and requester is currently administratively disabled.") + im.transitionToPendingWithComment(ctx, ticketID, "An existing override for this limit and requester is currently administratively disabled.") return fmt.Errorf("override for rate limit %s and account/domain/IP: %s is administratively disabled", rateLimit, accountDomainOrIP) } diff --git a/sfe/overridesimporter_test.go b/sfe/overridesimporter_test.go index b612da76669..d1547171419 100644 --- a/sfe/overridesimporter_test.go +++ b/sfe/overridesimporter_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" rapb "github.com/letsencrypt/boulder/ra/proto" rl "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/sfe/zendesk" diff --git a/sfe/sfe.go b/sfe/sfe.go index fd2c8e618ca..8b00360ec8d 100644 --- a/sfe/sfe.go +++ b/sfe/sfe.go @@ -1,11 +1,13 @@ package sfe import ( + "context" "embed" "errors" "fmt" "html/template" "io/fs" + "log/slog" "net/http" "net/url" "strconv" @@ -17,8 +19,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics/measured_http" rapb "github.com/letsencrypt/boulder/ra/proto" rl "github.com/letsencrypt/boulder/ratelimits" @@ -188,7 +190,7 @@ func (sfe *SelfServiceFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTT // renderTemplate takes the name of an HTML template and optional dynamicData // which are rendered and served back to the client via the response writer. -func (sfe *SelfServiceFrontEndImpl) renderTemplate(w http.ResponseWriter, filename string, dynamicData any) { +func (sfe *SelfServiceFrontEndImpl) renderTemplate(ctx context.Context, w http.ResponseWriter, filename string, dynamicData any) { if len(filename) == 0 { http.Error(w, "Template page does not exist", http.StatusInternalServerError) return @@ -197,14 +199,14 @@ func (sfe *SelfServiceFrontEndImpl) renderTemplate(w http.ResponseWriter, filena w.Header().Set("Content-Type", "text/html; charset=utf-8") err := sfe.templatePages.ExecuteTemplate(w, filename, dynamicData) if err != nil { - sfe.log.Warningf("template %q execute failed: %s", filename, err) + sfe.log.Error(ctx, "Failed to execute template", err, slog.String("template", filename)) http.Error(w, err.Error(), http.StatusInternalServerError) } } // Index is the homepage of the SFE func (sfe *SelfServiceFrontEndImpl) Index(response http.ResponseWriter, request *http.Request) { - sfe.renderTemplate(response, "index.html", nil) + sfe.renderTemplate(request.Context(), response, "index.html", nil) } // BuildID tells the requester what boulder build version is running. @@ -213,7 +215,7 @@ func (sfe *SelfServiceFrontEndImpl) BuildID(response http.ResponseWriter, reques response.WriteHeader(http.StatusOK) detailsString := fmt.Sprintf("Boulder=(%s %s)", core.GetBuildID(), core.GetBuildTime()) if _, err := fmt.Fprintln(response, detailsString); err != nil { - sfe.log.Warningf("Could not write response: %s", err) + sfe.log.Error(request.Context(), "Could not write response", err) } } @@ -228,16 +230,16 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseForm(response http.ResponseWriter, re if err != nil { if errors.Is(err, jwt.ErrExpired) { // JWT expired before the Subscriber visited the unpause page. - sfe.unpauseTokenExpired(response) + sfe.unpauseTokenExpired(request.Context(), response) return } if errors.Is(err, unpause.ErrMalformedJWT) { // JWT is malformed. This could happen if the Subscriber failed to // copy the entire URL from their logs. - sfe.unpauseRequestMalformed(response) + sfe.unpauseRequestMalformed(request.Context(), response) return } - sfe.unpauseFailed(response) + sfe.unpauseFailed(request.Context(), response) return } @@ -251,7 +253,7 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseForm(response http.ResponseWriter, re } // Present the unpause form to the Subscriber. - sfe.renderTemplate(response, "unpause-form.html", tmplData{unpausePostForm, incomingJWT, accountID, idents}) + sfe.renderTemplate(request.Context(), response, "unpause-form.html", tmplData{unpausePostForm, incomingJWT, accountID, idents}) } // UnpauseSubmit serves a page showing the result of the unpause form submission. @@ -264,16 +266,16 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseSubmit(response http.ResponseWriter, if err != nil { if errors.Is(err, jwt.ErrExpired) { // JWT expired before the Subscriber could click the unpause button. - sfe.unpauseTokenExpired(response) + sfe.unpauseTokenExpired(request.Context(), response) return } if errors.Is(err, unpause.ErrMalformedJWT) { // JWT is malformed. This should never happen if the request came // from our form. - sfe.unpauseRequestMalformed(response) + sfe.unpauseRequestMalformed(request.Context(), response) return } - sfe.unpauseFailed(response) + sfe.unpauseFailed(request.Context(), response) return } @@ -281,7 +283,7 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseSubmit(response http.ResponseWriter, RegistrationID: accountID, }) if err != nil { - sfe.unpauseFailed(response) + sfe.unpauseFailed(request.Context(), response) return } @@ -292,12 +294,12 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseSubmit(response http.ResponseWriter, http.Redirect(response, request, unpauseStatus+"?"+params.Encode(), http.StatusFound) } -func (sfe *SelfServiceFrontEndImpl) unpauseRequestMalformed(response http.ResponseWriter) { - sfe.renderTemplate(response, "unpause-invalid-request.html", nil) +func (sfe *SelfServiceFrontEndImpl) unpauseRequestMalformed(ctx context.Context, response http.ResponseWriter) { + sfe.renderTemplate(ctx, response, "unpause-invalid-request.html", nil) } -func (sfe *SelfServiceFrontEndImpl) unpauseTokenExpired(response http.ResponseWriter) { - sfe.renderTemplate(response, "unpause-expired.html", nil) +func (sfe *SelfServiceFrontEndImpl) unpauseTokenExpired(ctx context.Context, response http.ResponseWriter) { + sfe.renderTemplate(ctx, response, "unpause-expired.html", nil) } type unpauseStatusTemplate struct { @@ -306,12 +308,12 @@ type unpauseStatusTemplate struct { Count int64 } -func (sfe *SelfServiceFrontEndImpl) unpauseFailed(response http.ResponseWriter) { - sfe.renderTemplate(response, "unpause-status.html", unpauseStatusTemplate{Successful: false}) +func (sfe *SelfServiceFrontEndImpl) unpauseFailed(ctx context.Context, response http.ResponseWriter) { + sfe.renderTemplate(ctx, response, "unpause-status.html", unpauseStatusTemplate{Successful: false}) } -func (sfe *SelfServiceFrontEndImpl) unpauseSuccessful(response http.ResponseWriter, count int64) { - sfe.renderTemplate(response, "unpause-status.html", unpauseStatusTemplate{ +func (sfe *SelfServiceFrontEndImpl) unpauseSuccessful(ctx context.Context, response http.ResponseWriter, count int64) { + sfe.renderTemplate(ctx, response, "unpause-status.html", unpauseStatusTemplate{ Successful: true, Limit: unpause.RequestLimit, Count: count}, @@ -329,11 +331,11 @@ func (sfe *SelfServiceFrontEndImpl) UnpauseStatus(response http.ResponseWriter, count, err := strconv.ParseInt(request.URL.Query().Get("count"), 10, 64) if err != nil || count < 0 { - sfe.unpauseFailed(response) + sfe.unpauseFailed(request.Context(), response) return } - sfe.unpauseSuccessful(response, count) + sfe.unpauseSuccessful(request.Context(), response, count) } // parseUnpauseJWT extracts and returns the subscriber's registration ID and a diff --git a/sfe/sfe_test.go b/sfe/sfe_test.go index 6ce02a77ff7..41cb5707ea9 100644 --- a/sfe/sfe_test.go +++ b/sfe/sfe_test.go @@ -12,9 +12,9 @@ import ( "github.com/jmhodges/clock" "google.golang.org/grpc" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/features" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/must" diff --git a/test/boulder-tools/Dockerfile b/test/boulder-tools/Dockerfile index 569fbf58c35..75de8e3521e 100644 --- a/test/boulder-tools/Dockerfile +++ b/test/boulder-tools/Dockerfile @@ -10,6 +10,12 @@ ENV PATH=/usr/local/go/bin:/usr/local/protoc/bin:$PATH ENV GOBIN=/usr/local/bin/ RUN curl "https://dl.google.com/go/go${GO_VERSION}.$(echo $TARGETPLATFORM | sed 's|\/|-|').tar.gz" |\ tar -C /usr/local -xz +# Regardless of GO_VERSION, also install gotip from the latest main. +# This is temporary until Go 1.27 is released, to give us access to the +# crypto/mldsa package. See startservers.py and test/certs/generate.sh +# for uses of gotip. +RUN /usr/local/go/bin/go install golang.org/dl/gotip@latest +RUN gotip download RUN go install github.com/rubenv/sql-migrate/sql-migrate@v1.1.2 RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 @@ -46,6 +52,8 @@ RUN sed -i '/$RepeatedMsgReduction on/s/^/#/' /etc/rsyslog.conf COPY --from=godeps /usr/local/bin/* /usr/local/bin/ COPY --from=godeps /usr/local/go/ /usr/local/go/ +# Downloaded packages from gotip +COPY --from=godeps /root/sdk/ /root/sdk/ COPY --from=rustdeps /usr/local/cargo/bin/typos /usr/local/bin/typos ENV PATH=/usr/local/go/bin:/usr/local/protoc/bin:$PATH diff --git a/test/boulder-tools/flushredis/main.go b/test/boulder-tools/flushredis/main.go index fafe29c984c..70dd8517b6b 100644 --- a/test/boulder-tools/flushredis/main.go +++ b/test/boulder-tools/flushredis/main.go @@ -5,8 +5,8 @@ import ( "fmt" "os" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" bredis "github.com/letsencrypt/boulder/redis" diff --git a/test/certs/generate.sh b/test/certs/generate.sh index 6c576cf2269..df91866237a 100755 --- a/test/certs/generate.sh +++ b/test/certs/generate.sh @@ -61,9 +61,7 @@ ipki() ( webpki() ( # Because it invokes the ceremony tool, webpki.go expects to be invoked with # the root of the boulder repo as the current working directory. - # This function executes in a subshell, so this cd does not affect the parent - # script. - make build + GOBIN=$PWD/bin/ go install ./cmd/ceremony mkdir -p ./test/certs/webpki go run ./test/certs/webpki.go ) @@ -81,5 +79,5 @@ fi if ! [ -d test/certs/mtpki ]; then echo "Generating mtpki/..." mkdir -p test/certs/mtpki - go run ./test/certs/genmtpki/genmtpki.go + gotip run ./test/certs/genmtpki/genmtpki.go -output-dir test/certs/mtpki fi diff --git a/test/certs/genmtpki/genmtpki.go b/test/certs/genmtpki/genmtpki.go index 4456496b9a5..a72ebf407ee 100644 --- a/test/certs/genmtpki/genmtpki.go +++ b/test/certs/genmtpki/genmtpki.go @@ -1,16 +1,20 @@ +//go:build go1.27 + package main import ( - "crypto/ecdsa" - "crypto/elliptic" + "crypto/mldsa" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" + "errors" + "flag" "log" "math/big" "os" + "path" "time" ) @@ -21,10 +25,19 @@ func main() { } } -const basename = "test/certs/mtpki/mtca1" +const basename = "mtca1" func main2() error { - key, err := ecdsa.GenerateKey(elliptic.P256(), nil) + outputDir := flag.String("output-dir", "", "Directory to write outputs to") + flag.Parse() + + if *outputDir == "" { + return errors.New("-output-dir flag required") + } + + basepath := path.Join(*outputDir, basename) + + key, err := mldsa.GenerateKey(mldsa.MLDSA44()) if err != nil { return err } @@ -34,7 +47,7 @@ func main2() error { return err } - keyFile, err := os.OpenFile(basename+".key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + keyFile, err := os.OpenFile(basepath+".key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } @@ -77,7 +90,7 @@ func main2() error { return err } - certFile, err := os.OpenFile(basename+".cert.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + certFile, err := os.OpenFile(basepath+".cert.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } @@ -122,7 +135,8 @@ func mtcaExtension() (pkix.Extension, error) { // Copied from https://cs.opensource.google/go/go/+/refs/tags/go1.26.3:src/crypto/x509/x509.go;l=345-350 oidSHA256 := asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} - oidSignatureECDSAWithSHA256 := asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} + // https://www.rfc-editor.org/info/rfc9881/ + oidSignatureMLDSA44 := asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 17} extnMarshaled, err := asn1.Marshal(struct { LogHash pkix.AlgorithmIdentifier @@ -130,7 +144,7 @@ func mtcaExtension() (pkix.Extension, error) { MinSerial int64 }{ LogHash: pkix.AlgorithmIdentifier{Algorithm: oidSHA256}, - SigAlg: pkix.AlgorithmIdentifier{Algorithm: oidSignatureECDSAWithSHA256}, + SigAlg: pkix.AlgorithmIdentifier{Algorithm: oidSignatureMLDSA44}, // Just for fun, exercise MinSerial functionality. MinSerial: 999, }) diff --git a/test/certs/webpki.go b/test/certs/webpki.go index 465d69fb680..b8495cda9f6 100644 --- a/test/certs/webpki.go +++ b/test/certs/webpki.go @@ -11,7 +11,6 @@ import ( "text/template" "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" ) // createSlot initializes a SoftHSM slot and token. SoftHSM chooses the highest empty @@ -78,7 +77,6 @@ func runCeremony(path string) error { } func main() { - _ = blog.Set(blog.StdoutLogger(6)) defer cmd.AuditPanic() // Create SoftHSM slots for the root signing keys diff --git a/test/config-next/blocked-accounts.yaml b/test/config-next/blocked-accounts.yaml new file mode 100644 index 00000000000..4db006b7826 --- /dev/null +++ b/test/config-next/blocked-accounts.yaml @@ -0,0 +1,3 @@ +Message: "your account has been blocked due to excessive traffic" +BlockedAccountIDs: + - 1234 diff --git a/test/config-next/cert-checker.json b/test/config-next/cert-checker.json index 1afa679c7ef..8dccca4e23a 100644 --- a/test/config-next/cert-checker.json +++ b/test/config-next/cert-checker.json @@ -6,7 +6,6 @@ }, "hostnamePolicyFile": "test/ident-policy.yaml", "workers": 16, - "unexpiredOnly": true, "badResultsOnly": true, "checkPeriod": "72h", "acceptableValidityDurations": [ diff --git a/test/config-next/mtpublisher.json b/test/config-next/mtpublisher.json new file mode 100644 index 00000000000..defba8e85b3 --- /dev/null +++ b/test/config-next/mtpublisher.json @@ -0,0 +1,19 @@ +{ + "mtPublisher": { + "db": { + "dbConnectFile": "test/secrets/mtpublisher_dburl", + "maxOpenConns": 10 + }, + "pollInterval": "1s", + "mtcLogID": "44947.4.1.0.44", + "mirrorID": "32473.9" + }, + "syslog": { + "stdoutlevel": 6, + "sysloglevel": 6 + }, + "openTelemetry": { + "endpoint": "bjaeger:4317", + "sampleratio": 1 + } +} diff --git a/test/config-next/proxysql/mtpublisher_dburl b/test/config-next/proxysql/mtpublisher_dburl new file mode 100644 index 00000000000..0675bf58824 --- /dev/null +++ b/test/config-next/proxysql/mtpublisher_dburl @@ -0,0 +1 @@ +mtpublisher@tcp(boulder-proxysql:6033)/mtcmeta_44947_4_1_0_44?readTimeout=14s&writeTimeout=14s&timeout=1s diff --git a/test/config-next/ra.json b/test/config-next/ra.json index 9eaac0eb576..50813bf4713 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -64,6 +64,17 @@ "dns", "ip" ] + }, + "mtcshortlived": { + "mtc": true, + "pendingAuthzLifetime": "7h", + "validAuthzLifetime": "7h", + "orderLifetime": "7h", + "maxNames": 10, + "identifierTypes": [ + "dns", + "ip" + ] } }, "defaultProfileName": "legacy", @@ -72,6 +83,18 @@ "certFile": "test/certs/ipki/ra.boulder/cert.pem", "keyFile": "test/certs/ipki/ra.boulder/key.pem" }, + "profileToMTCA": { + "mtcshortlived": { + "dnsAuthority": "consul.service.consul", + "srvLookup": { + "service": "mtca", + "domain": "service.consul" + }, + "timeout": "20s", + "noWaitForReady": false, + "hostOverride": "mtca.boulder" + } + }, "vaService": { "dnsAuthority": "consul.service.consul", "srvLookup": { diff --git a/test/config-next/vitess/mtpublisher_dburl b/test/config-next/vitess/mtpublisher_dburl new file mode 100644 index 00000000000..79f0bec4bf7 --- /dev/null +++ b/test/config-next/vitess/mtpublisher_dburl @@ -0,0 +1 @@ +mtpublisher@tcp(boulder-vitess:33577)/mtcmeta_44947_4_1_0_44?readTimeout=14s&writeTimeout=14s&timeout=1s diff --git a/test/config-next/wfe2.json b/test/config-next/wfe2.json index d929140a618..ee4f90eb39c 100644 --- a/test/config-next/wfe2.json +++ b/test/config-next/wfe2.json @@ -14,6 +14,7 @@ "accountURIPrefix": "http://boulder.service.consul:4001/acme/acct/", "goodkey": {}, "maxContactsPerRegistration": 3, + "maxCumulativeIdentifierLength": 10000, "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/wfe.boulder/cert.pem", @@ -141,7 +142,8 @@ "certProfiles": { "legacy": "The normal profile you know and love", "modern": "Profile 2: Electric Boogaloo", - "shortlived": "Like modern, but smaller" + "shortlived": "Like modern, but smaller", + "mtcshortlived": "Like shortlived, but MTC" }, "unpause": { "hmacKey": { @@ -150,6 +152,7 @@ "jwtLifetime": "336h", "url": "https://boulder.service.consul:4003" }, + "blockedAccountsFile": "test/config-next/blocked-accounts.yaml", "blockedOnDemandLabels": [ "asdf" ] diff --git a/test/config/cert-checker.json b/test/config/cert-checker.json index 5ca23dc550b..4f51507f249 100644 --- a/test/config/cert-checker.json +++ b/test/config/cert-checker.json @@ -6,7 +6,6 @@ }, "hostnamePolicyFile": "test/ident-policy.yaml", "workers": 16, - "unexpiredOnly": true, "badResultsOnly": true, "checkPeriod": "72h", "acceptableValidityDurations": [ diff --git a/test/config/mtca.json b/test/config/mtca.json deleted file mode 100644 index 679125a63e8..00000000000 --- a/test/config/mtca.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "mtca": { - "tls": { - "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/mtca.boulder/cert.pem", - "keyFile": "test/certs/ipki/mtca.boulder/key.pem" - }, - "grpcMTCA": { - "maxConnectionAge": "30s", - "services": { - "mtca.MTCA": { - "clientNames": [ - "ra.boulder" - ] - }, - "grpc.health.v1.Health": { - "clientNames": [ - "health-checker.boulder" - ] - } - } - }, - "issuer": { - "crlShards": 10, - "issuerURL": "http://ignored.letsencrypt.org", - "crlURLBase": "http://ignored.letsencrypt.org/", - "location": { - "file": "test/certs/mtpki/mtca1.key.pem", - "certFile": "test/certs/mtpki/mtca1.cert.pem" - } - } - }, - "syslog": { - "stdoutlevel": 4, - "sysloglevel": -1 - }, - "openTelemetry": { - "endpoint": "bjaeger:4317", - "sampleratio": 1 - } -} diff --git a/test/config/mtpublisher.json b/test/config/mtpublisher.json new file mode 100644 index 00000000000..defba8e85b3 --- /dev/null +++ b/test/config/mtpublisher.json @@ -0,0 +1,19 @@ +{ + "mtPublisher": { + "db": { + "dbConnectFile": "test/secrets/mtpublisher_dburl", + "maxOpenConns": 10 + }, + "pollInterval": "1s", + "mtcLogID": "44947.4.1.0.44", + "mirrorID": "32473.9" + }, + "syslog": { + "stdoutlevel": 6, + "sysloglevel": 6 + }, + "openTelemetry": { + "endpoint": "bjaeger:4317", + "sampleratio": 1 + } +} diff --git a/test/config/proxysql/mtpublisher_dburl b/test/config/proxysql/mtpublisher_dburl new file mode 100644 index 00000000000..0675bf58824 --- /dev/null +++ b/test/config/proxysql/mtpublisher_dburl @@ -0,0 +1 @@ +mtpublisher@tcp(boulder-proxysql:6033)/mtcmeta_44947_4_1_0_44?readTimeout=14s&writeTimeout=14s&timeout=1s diff --git a/test/config/vitess/mtpublisher_dburl b/test/config/vitess/mtpublisher_dburl new file mode 100644 index 00000000000..79f0bec4bf7 --- /dev/null +++ b/test/config/vitess/mtpublisher_dburl @@ -0,0 +1 @@ +mtpublisher@tcp(boulder-vitess:33577)/mtcmeta_44947_4_1_0_44?readTimeout=14s&writeTimeout=14s&timeout=1s diff --git a/test/entrypoint.sh b/test/entrypoint.sh index bf5301e793f..741805f7b99 100755 --- a/test/entrypoint.sh +++ b/test/entrypoint.sh @@ -15,6 +15,7 @@ DB_URL_FILES=( cert_checker_dburl incidents_dburl incidents_admin_dburl + mtpublisher_dburl revoker_dburl sa_dburl sa_ro_dburl diff --git a/test/integration/issuance_test.go b/test/integration/issuance_test.go index a529953f93e..f931c0236b4 100644 --- a/test/integration/issuance_test.go +++ b/test/integration/issuance_test.go @@ -10,6 +10,7 @@ import ( "crypto/x509/pkix" "fmt" "net" + "os" "strings" "testing" @@ -171,6 +172,33 @@ func TestIssuanceProfiles(t *testing.T) { test.AssertEquals(t, len(modern.SubjectKeyId), 0) } +// TestIssuanceMTC issues from an MTC profile. +func TestIssuanceMTC(t *testing.T) { + t.Parallel() + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + t.Skip("MTC issuance only available in config-next") + } + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generating keypair: %s", err) + } + + idents := []acme.Identifier{{Type: "dns", Value: random_domain()}} + + _, err = authAndIssue(client, key, idents, true, "mtcshortlived") + // "finalized order timeout" is as far as we get for now. Once we are collecting + // signatures and constructing standalone certificates, we'll expect this to succeed. + if err == nil || !strings.Contains(err.Error(), "finalized order timeout") { + t.Fatalf("issuing certificate: expected 'finalized order timeout', got %q", err) + } +} + // TestIPShortLived verifies that we will allow IP address identifiers only in // orders that use the shortlived profile. func TestIPShortLived(t *testing.T) { diff --git a/test/integration/observer_test.go b/test/integration/observer_test.go index 9c3b72659ba..94c2c82bb2d 100644 --- a/test/integration/observer_test.go +++ b/test/integration/observer_test.go @@ -167,7 +167,7 @@ monitors: t.Fatalf("timed out before getting desired log line from boulder-observer") case line := <-output: t.Log(line) - if strings.Contains(line, "name=[integration.trust:8675]") && strings.Contains(line, "success=[true]") { + if strings.Contains(line, "name=integration.trust:8675") && strings.Contains(line, "success=true") { return } } diff --git a/test/integration/otel_test.go b/test/integration/otel_test.go index 066099a549e..bed380e3166 100644 --- a/test/integration/otel_test.go +++ b/test/integration/otel_test.go @@ -22,8 +22,8 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) @@ -262,10 +262,11 @@ func TestTraces(t *testing.T) { func traceIssuingTestCert(t *testing.T) trace.TraceID { // Configure this integration test to trace to jaeger:4317 like Boulder will + logger, _ := blog.New(blog.Config{StdoutLevel: 6}) shutdown := cmd.NewOpenTelemetry(cmd.OpenTelemetryConfig{ Endpoint: "bjaeger:4317", SampleRatio: 1, - }, blog.Get()) + }, logger) defer shutdown(context.Background()) tracer := otel.GetTracerProvider().Tracer("TraceTest") diff --git a/test/load-generator/acme/challenge.go b/test/load-generator/acme/challenge.go index 12aeb9aa284..c3097f1e236 100644 --- a/test/load-generator/acme/challenge.go +++ b/test/load-generator/acme/challenge.go @@ -92,7 +92,7 @@ func (strategy preferredTypeChallengeStrategy) PickChallenge(authz *core.Authori return &chall, nil } } - return nil, fmt.Errorf("authorization (ID %q) had no %q type challenge", + return nil, fmt.Errorf("authorization (ID %d) had no %q type challenge", authz.ID, strategy.preferredType) } diff --git a/test/load-generator/acme/challenge_test.go b/test/load-generator/acme/challenge_test.go index f21907c3f81..b9cab162d0a 100644 --- a/test/load-generator/acme/challenge_test.go +++ b/test/load-generator/acme/challenge_test.go @@ -66,7 +66,7 @@ func TestPickChallenge(t *testing.T) { Type: "dns-01", } exampleAuthz := &core.Authorization{ - ID: "1234", + ID: 1234, Challenges: []core.Challenge{ { Type: "arm-wrestling", @@ -111,7 +111,7 @@ func TestPickChallenge(t *testing.T) { Name: "Preferred type strategy, no challenge of type", StratName: "tls-alpn-01", InputAuthz: exampleAuthz, - ExpectedError: `authorization (ID "1234") had no "tls-alpn-01" type challenge`, + ExpectedError: `authorization (ID 1234) had no "tls-alpn-01" type challenge`, }, { Name: "Preferred type strategy, challenge of type present", diff --git a/test/load-generator/boulder-calls.go b/test/load-generator/boulder-calls.go index 78ab698e203..c395a6ee3d6 100644 --- a/test/load-generator/boulder-calls.go +++ b/test/load-generator/boulder-calls.go @@ -153,7 +153,7 @@ func randDomain(base string) string { // limits annoying! var bytes [3]byte _, _ = rand.Read(bytes[:]) - return hex.EncodeToString(bytes[:]) + base + return hex.EncodeToString(bytes[:]) + "." + base } // newOrder creates a new pending order object for a random set of domains using @@ -254,9 +254,6 @@ func getAuthorization(s *State, c *acmeCache, url string) (*core.Authorization, if err != nil { return nil, fmt.Errorf("%s response: %s", url, body) } - // The Authorization ID is not set in the response so we populate it using the - // URL - authz.ID = url return &authz, nil } @@ -264,7 +261,7 @@ func getAuthorization(s *State, c *acmeCache, url string) (*core.Authorization, // HTTP-01 challenge using the context's account and the state's challenge // server. Aftering POSTing the authorization's HTTP-01 challenge the // authorization will be polled waiting for a state change. -func completeAuthorization(authz *core.Authorization, s *State, c *acmeCache) error { +func completeAuthorization(authz *core.Authorization, url string, s *State, c *acmeCache) error { // Skip if the authz isn't pending if authz.Status != core.StatusPending { return nil @@ -332,7 +329,7 @@ func completeAuthorization(authz *core.Authorization, s *State, c *acmeCache) er // Poll the authorization waiting for the challenge response to be recorded in // a change of state. The polling may sleep and retry a few times if required - err = pollAuthorization(authz, s, c) + err = pollAuthorization(url, s, c) if err != nil { return err } @@ -346,17 +343,16 @@ func completeAuthorization(authz *core.Authorization, s *State, c *acmeCache) er // be valid. If the status is invalid, or if three GETs do not produce the // correct authorization state an error is returned. If no error is returned // then the authorization is valid and ready. -func pollAuthorization(authz *core.Authorization, s *State, c *acmeCache) error { - authzURL := authz.ID +func pollAuthorization(url string, s *State, c *acmeCache) error { for range 3 { // Fetch the authz by its URL - authz, err := getAuthorization(s, c, authzURL) + authz, err := getAuthorization(s, c, url) if err != nil { return nil } // If the authz is invalid, abort with an error if authz.Status == "invalid" { - return fmt.Errorf("Authorization %q failed challenge and is status invalid", authzURL) + return fmt.Errorf("Authorization %q failed challenge and is status invalid", url) } // If the authz is valid, return with no error - the authz is ready to go! if authz.Status == "valid" { @@ -365,7 +361,7 @@ func pollAuthorization(authz *core.Authorization, s *State, c *acmeCache) error // Otherwise sleep and try again time.Sleep(3 * time.Second) } - return fmt.Errorf("Timed out polling authorization %q", authzURL) + return fmt.Errorf("Timed out polling authorization %q", url) } // fulfillOrder processes a pending order from the context, completing each @@ -390,7 +386,7 @@ func fulfillOrder(s *State, c *acmeCache) error { } // Complete the authorization by solving a challenge - err = completeAuthorization(authz, s, c) + err = completeAuthorization(authz, url, s, c) if err != nil { return err } diff --git a/test/load-generator/config/integration-test-config.json b/test/load-generator/config/integration-test-config.json index 50d86856826..db3cd286213 100644 --- a/test/load-generator/config/integration-test-config.json +++ b/test/load-generator/config/integration-test-config.json @@ -17,6 +17,9 @@ "httpOneAddrs": [":80"], "tlsAlpnOneAddrs": [":443"], "dnsAddrs": [":8053", ":8054"], + "dohAddrs": [":8343", ":8443"], + "dohCert": "test/certs/ipki/10.77.77.77/cert.pem", + "dohCertKey": "test/certs/ipki/10.77.77.77/key.pem", "fakeDNS": "10.77.77.77", "regKeySize": 2048, "regEmail": "loadtesting@letsencrypt.org", diff --git a/test/load-generator/main.go b/test/load-generator/main.go index 1baed067388..b271c71b731 100644 --- a/test/load-generator/main.go +++ b/test/load-generator/main.go @@ -28,6 +28,9 @@ type Config struct { HTTPOneAddrs []string // addresses to listen for http-01 validation requests on TLSALPNOneAddrs []string // addresses to listen for tls-alpn-01 validation requests on DNSAddrs []string // addresses to listen for DNS requests on + DOHAddrs []string // addresses to listen for DOH DNS requests on + DOHCert string // path to DOH Cert file + DOHCertKey string // path to DOH Key file FakeDNS string // IPv6 address to use for all DNS A requests RealIP string // value of the Real-IP header to use when bypassing CDN RegEmail string // email to use in registrations @@ -127,6 +130,9 @@ func main() { config.HTTPOneAddrs, config.TLSALPNOneAddrs, config.DNSAddrs, + config.DOHAddrs, + config.DOHCert, + config.DOHCertKey, config.FakeDNS, Plan{ Runtime: runtime, diff --git a/test/load-generator/state.go b/test/load-generator/state.go index 12f34ae3239..6d075740726 100644 --- a/test/load-generator/state.go +++ b/test/load-generator/state.go @@ -349,6 +349,9 @@ func (s *State) Run( httpOneAddrs []string, tlsALPNOneAddrs []string, dnsAddrs []string, + dohAddrs []string, + dohCert string, + dohCertKey string, fakeDNS string, p Plan) error { // Create a new challenge server binding the requested addrs. @@ -356,6 +359,9 @@ func (s *State) Run( HTTPOneAddrs: httpOneAddrs, TLSALPNOneAddrs: tlsALPNOneAddrs, DNSAddrs: dnsAddrs, + DOHAddrs: dohAddrs, + DOHCert: dohCert, + DOHCertKey: dohCertKey, // Use a logger that has a load-generator prefix Log: log.New(os.Stdout, "load-generator challsrv - ", log.LstdFlags), }) diff --git a/test/proxysql/proxysql.cnf b/test/proxysql/proxysql.cnf index b31a14b0768..16001f98546 100644 --- a/test/proxysql/proxysql.cnf +++ b/test/proxysql/proxysql.cnf @@ -100,6 +100,9 @@ mysql_users = }, { username = "incidents_sa_admin"; + }, + { + username = "mtpublisher"; } ); mysql_query_rules = diff --git a/test/startservers.py b/test/startservers.py index f1de143d4ba..56d677a15d7 100644 --- a/test/startservers.py +++ b/test/startservers.py @@ -5,12 +5,12 @@ import socket import subprocess -from helpers import config_dir, waithealth, waitport +from helpers import config_dir, waithealth, waitport, CONFIG_NEXT Service = collections.namedtuple('Service', ('name', 'debug_port', 'grpc_port', 'host_override', 'cmd', 'deps')) # Keep these ports in sync with consul/config.hcl -SERVICES = ( +SERVICES = [ Service('remoteva-a', 8011, 9397, 'rva.boulder', ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-a.json'), '--addr', ':9397', '--debug-addr', ':8011'), @@ -53,10 +53,6 @@ 8104, 9492, 'va.boulder', ('./bin/boulder', 'boulder-va', '--config', os.path.join(config_dir, 'va.json'), '--addr', ':9492', '--debug-addr', ':8104'), ('remoteva-a', 'remoteva-b')), - Service('boulder-mtca-1', - 8010, 9396, 'mtca.boulder', - ('./bin/boulder', 'boulder-mtca', '--config', os.path.join(config_dir, 'mtca.json'), '--addr', ':9396', '--debug-addr', ':8010'), - None), Service('boulder-ca-1', 8001, 9393, 'ca.boulder', ('./bin/boulder', 'boulder-ca', '--config', os.path.join(config_dir, 'ca.json'), '--addr', ':9393', '--debug-addr', ':8001'), @@ -147,7 +143,19 @@ 8016, None, None, ('./bin/boulder', 'log-validator', '--config', os.path.join(config_dir, 'log-validator.json'), '--debug-addr', ':8016'), None), -) +] + +if CONFIG_NEXT: + SERVICES.extend([ + Service('boulder-mtca-1', + 8010, 9396, 'mtca.boulder', + ('./bin/boulder', 'boulder-mtca', '--config', os.path.join(config_dir, 'mtca.json'), '--addr', ':9396', '--debug-addr', ':8010'), + None), + Service('boulder-mtpublisher-1', + 8025, None, None, + ('./bin/boulder', 'boulder-mtpublisher', '--config', os.path.join(config_dir, 'mtpublisher.json'), '--debug-addr', ':8025'), + None), + ]) def _service_toposort(services): """Yields Service objects in topologically sorted order. @@ -182,14 +190,17 @@ def install(race_detection, coverage=False): # Pass empty BUILD_TIME and BUILD_ID flags to avoid constantly invalidating the # build cache with new BUILD_TIMEs, or invalidating it on merges with a new # BUILD_ID. - go_build_flags='' + go_build_flags='-tags "integration"' if race_detection: go_build_flags += ' -race' if coverage: go_build_flags += ' -cover' # https://go.dev/blog/integration-test-coverage - return subprocess.call(["/usr/bin/make", "GO_BUILD_FLAGS=%s" % go_build_flags]) == 0 + cmd = ["/usr/bin/make", "GO_BUILD_FLAGS=%s" % go_build_flags] + if CONFIG_NEXT: + cmd.append("GO=gotip") + return subprocess.call(cmd) == 0 def run(cmd, coverage_dir=None): e = os.environ.copy() diff --git a/test/vars/vars.go b/test/vars/vars.go index b3e73ae2983..cda59d839d3 100644 --- a/test/vars/vars.go +++ b/test/vars/vars.go @@ -30,4 +30,8 @@ var ( DBConnIncidentsAdmin = dsn("incidents_sa_admin", "incidents_sa") // DBConnIncidentsFullPerms is the incidents database connection with full perms. DBConnIncidentsFullPerms = dsn("test_setup", "incidents_sa") + // DBConnMTCMeta_44947_4_1_0_44FullPerms is the mtcmeta_44947_4_1_0_44 database + // connection with full perms. It builds the DSN directly because mtcmeta has + // no _next variant for dsn() to append. + DBConnMTCMeta_44947_4_1_0_44FullPerms = fmt.Sprintf("test_setup@tcp(%s)/mtcmeta_44947_4_1_0_44", os.Getenv("DB_ADDR")) ) diff --git a/test/vtcomboserver/run.sh b/test/vtcomboserver/run.sh index 428ff67c5e0..438fa2dfc9e 100755 --- a/test/vtcomboserver/run.sh +++ b/test/vtcomboserver/run.sh @@ -32,7 +32,7 @@ rm -vf "$VTDATAROOT"/"$tablet_dir"/{mysql.sock,mysql.sock.lock} VTSCHEMADIR=/vt/schema/ cp -r /boulder/sa/vtschema/ "${VTSCHEMADIR}" -for DB in boulder_sa boulder_sa_next incidents_sa incidents_sa_next ; do +for DB in boulder_sa boulder_sa_next incidents_sa incidents_sa_next mtcmeta_44947_4_1_0_44 ; do # In MariaDB land, we need a `USE` statement in the SQL. In Vitess, # it's disallowed. grep --ignore-case --invert-match '^USE ' \ diff --git a/trees/cosigned/message.go b/trees/cosigned/message.go new file mode 100644 index 00000000000..8b2277630de --- /dev/null +++ b/trees/cosigned/message.go @@ -0,0 +1,122 @@ +// Package cosigned implements CosignedMessage from +// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1. +package cosigned + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + + "golang.org/x/crypto/cryptobyte" +) + +// Message represents a CosignedMessage from +// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1. +type Message struct { + CosignerName string + Timestamp uint64 + LogOrigin string + Start uint64 + End uint64 + SubtreeHash [sha256.Size]byte +} + +const subtreeLabel = "subtree/v1\n\x00" + +// Marshal encodes the Message as bytes. +// +// It errors if cosigner_name or log_origin are too long or too short. It does not validate semantic constraints, +// like start < end. +// +// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1 +// opaque HashValue[HASH_SIZE]; +// +// struct { +// uint8 label[12] = "subtree/v1\n\0"; +// opaque cosigner_name<1..2^8-1>; +// uint64 timestamp; +// opaque log_origin<1..2^8-1>; +// uint64 start; +// uint64 end; +// HashValue subtree_hash; +// } CosignedMessage; +func (message *Message) Marshal() ([]byte, error) { + if len(message.CosignerName) < 1 || len(message.CosignerName) > 255 { + return nil, fmt.Errorf("invalid cosigner_name length %d", len(message.CosignerName)) + } + if len(message.LogOrigin) < 1 || len(message.LogOrigin) > 255 { + return nil, fmt.Errorf("invalid log_origin length %d", len(message.LogOrigin)) + } + + var b cryptobyte.Builder + b.AddBytes([]byte(subtreeLabel)) + b.AddUint8LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes([]byte(message.CosignerName)) + }) + b.AddUint64(message.Timestamp) + b.AddUint8LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes([]byte(message.LogOrigin)) + }) + b.AddUint64(message.Start) + b.AddUint64(message.End) + b.AddBytes(message.SubtreeHash[:]) + + return b.Bytes() +} + +// Unmarshal unmarshals the input bytes and returns a *Message. +func Unmarshal(input []byte) (*Message, error) { + var out Message + + s := cryptobyte.String(input) + var label []byte + if !s.ReadBytes(&label, len(subtreeLabel)) { + return nil, errors.New("invalid label") + } + if !bytes.Equal(label, []byte(subtreeLabel)) { + return nil, errors.New("label was not subtree/v1") + } + + var cosignerName cryptobyte.String + if !s.ReadUint8LengthPrefixed(&cosignerName) { + return nil, errors.New("invalid cosigner_name") + } + if len(cosignerName) < 1 { + return nil, errors.New("empty cosigner_name") + } + out.CosignerName = string(cosignerName) + + if !s.ReadUint64(&out.Timestamp) { + return nil, errors.New("invalid timestamp") + } + + var logOrigin cryptobyte.String + if !s.ReadUint8LengthPrefixed(&logOrigin) { + return nil, errors.New("invalid log_origin") + } + if len(logOrigin) < 1 { + return nil, errors.New("empty log_origin") + } + out.LogOrigin = string(logOrigin) + + if !s.ReadUint64(&out.Start) { + return nil, errors.New("invalid start") + } + + if !s.ReadUint64(&out.End) { + return nil, errors.New("invalid end") + } + + var subtreeHash []byte + if !s.ReadBytes(&subtreeHash, len(out.SubtreeHash)) { + return nil, errors.New("invalid subtree hash") + } + copy(out.SubtreeHash[:], subtreeHash) + + if !s.Empty() { + return nil, errors.New("trailing bytes") + } + + return &out, nil +} diff --git a/trees/cosigned/message_test.go b/trees/cosigned/message_test.go new file mode 100644 index 00000000000..b6503c21b5d --- /dev/null +++ b/trees/cosigned/message_test.go @@ -0,0 +1,126 @@ +package cosigned + +import ( + "encoding/hex" + "reflect" + "strings" + "testing" +) + +func TestMessageRoundtrip(t *testing.T) { + m := Message{ + CosignerName: "alpha", + Timestamp: 1234, + LogOrigin: "beta", + Start: 999, + End: 1000, + SubtreeHash: [32]byte{}, + } + + copy(m.SubtreeHash[:], []byte("0123456789abcdef0123456789abcdef")) + + out, err := m.Marshal() + if err != nil { + t.Fatalf("marshaling: %s", err) + } + + m2, err := Unmarshal(out) + if err != nil { + t.Fatalf("unmarshaling encoded message: %s", err) + } + + if !reflect.DeepEqual(m, *m2) { + t.Errorf("round-tripping message: got %#v, want %#v", m, *m2) + } +} + +func TestMarshalErrors(t *testing.T) { + m := Message{ + CosignerName: "Michigan", + Timestamp: 1337000, + LogOrigin: "Illinois", + Start: 9, + End: 87654321, + SubtreeHash: [32]byte{}, + } + + type testCase struct { + name, expected string + distorter func(target *Message) + } + + testCases := []testCase{ + {"short CosignerName", "invalid cosigner_name length 0", func(target *Message) { + target.CosignerName = "" + }}, + {"long CosignerName", "invalid cosigner_name length 256", func(target *Message) { + target.CosignerName = strings.Repeat("a", 256) + }}, + {"short LogOrigin", "invalid log_origin length 0", func(target *Message) { + target.LogOrigin = "" + }}, + {"long LogOrigin", "invalid log_origin length 256", func(target *Message) { + target.LogOrigin = strings.Repeat("a", 256) + }}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m2 := m + tc.distorter(&m2) + _, err := m2.Marshal() + if err == nil { + t.Fatalf("got no error, want %q", tc.expected) + } + if err.Error() != tc.expected { + t.Errorf("marshal with short name: got %q, want %q", err, tc.expected) + } + }) + } +} + +func TestUnmarshalErrors(t *testing.T) { + m := Message{ + CosignerName: "Debut", + Timestamp: 55555, + LogOrigin: "Post", + Start: 11, + End: 22, + SubtreeHash: [32]byte{}, + } + + out, err := m.Marshal() + if err != nil { + t.Fatalf("marshal: %s", err) + } + t.Logf("%x", out) + + _, err = Unmarshal(out[:len(out)-1]) + if err == nil { + t.Errorf("unmarshal with short input: got no error") + } + + long := append(out, byte('a')) + _, err = Unmarshal(long) + if err == nil { + t.Errorf("unmarshal with trailing bytes: got no error") + } + + emptyCosigner, err := hex.DecodeString("737562747265652f76310a0000000000000000d90304506f7374000000000000000b00000000000000160000000000000000000000000000000000000000000000000000000000000000") + if err != nil { + t.Errorf("decoding hex: %s", err) + } + _, err = Unmarshal(emptyCosigner) + if err == nil { + t.Errorf("unmarshal with empty cosigner_name: got no error") + } + + emptyLogOrigin, err := hex.DecodeString("737562747265652f76310a00054465627574000000000000d90300000000000000000b00000000000000160000000000000000000000000000000000000000000000000000000000000000") + if err != nil { + t.Errorf("decoding hex: %s", err) + } + _, err = Unmarshal(emptyLogOrigin) + if err == nil { + t.Errorf("unmarshal with empty log_origin: got no error") + } +} diff --git a/va/caa.go b/va/caa.go index a318caf6597..d126e72aa6e 100644 --- a/va/caa.go +++ b/va/caa.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - corepb "github.com/letsencrypt/boulder/core/proto" + "log/slog" "net/url" "regexp" + "strconv" "strings" "sync" "time" @@ -15,7 +16,9 @@ import ( "google.golang.org/protobuf/proto" "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" + corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" @@ -41,6 +44,20 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal return nil, berrors.InternalServerError("incomplete IsCAAValid request") } + // TODO(#8722): remove this whole thing when Authz IDs are int64-only + var authzIDInt int64 + if req.AuthzIDInt != 0 { + authzIDInt = req.AuthzIDInt + } else if req.AuthzID != "" { + parsed, err := strconv.ParseInt(req.AuthzID, 10, 64) + if err != nil { + return nil, berrors.MalformedError("Unable to parse Authz ID %q as integer: %v", req.AuthzID, err) + } + authzIDInt = parsed + } else { + return nil, berrors.MalformedError("No Authz ID value supplied in gRPC message") + } + ident := identifier.FromProto(req.Identifier) if ident.Type != identifier.TypeDNS { return nil, berrors.MalformedError("Identifier type for CAA check was not DNS") @@ -56,25 +73,33 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal validationMethod: challType, } - // Initialize variables and a deferred function to handle check latency - // metrics, log check errors, and log an MPIC summary. Avoid using := to - // redeclare `prob`, `localLatency`, or `summary` below this point. + // Set the log attributes that we want to appear on all subsequent log lines + ctx = blog.ContextWith(ctx, + blog.Acct(req.AccountURIID), + blog.Authz(authzIDInt), + blog.Idents(ident), + slog.String("method", string(challType)), + ) + + // Initialize variables and a deferred function to handle validation latency + // metrics, log validation errors, and log an MPIC summary. Avoid using := + // to redeclare any of these variables below this point. var prob *probs.ProblemDetails - var summary *mpicSummary var localLatency time.Duration + var logAttrs []slog.Attr start := va.clk.Now() - logEvent := validationLogEvent{ - AuthzID: req.AuthzID, - Requester: req.AccountURIID, - Identifier: ident, - } defer func() { + logAttrs = append(logAttrs, + slog.Duration("localLatency", localLatency), + slog.Duration("totalLatency", va.clk.Since(start).Round(time.Millisecond)), + ) + probType := "" outcome := fail if prob != nil { // CAA check failed. probType = string(prob.Type) - logEvent.Error = prob.String() + logAttrs = append(logAttrs, slog.String("error", prob.String())) } else { // CAA check passed. outcome = pass @@ -85,12 +110,9 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal if va.isPrimaryVA() { // Observe total check latency (primary+remote). va.observeLatency(opCAA, allPerspectives, string(challType), probType, outcome, va.clk.Since(start)) - logEvent.Summary = summary } - // Log the total check latency. - logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() - va.log.AuditInfo("CAA check result", logEvent) + va.log.AuditInfo(ctx, "CAA check result", logAttrs...) }() // Do the local checks. We do these before kicking off the remote checks to @@ -101,7 +123,7 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal localLatency = va.clk.Since(start) if err != nil { - logEvent.InternalError = err.Error() + logAttrs = append(logAttrs, blog.Error(err)) prob = detailedError(err) prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", ident.Value, prob.Detail) } @@ -134,7 +156,14 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal } return remoteva.DoCAA(ctx, checkRequest) } + var summary *mpicSummary summary, prob = va.doRemoteOperation(ctx, op, req) + logAttrs = append(logAttrs, slog.Group("mpic", + slog.Any("passed", summary.Passed), + slog.Any("failed", summary.Failed), + slog.Any("passedRIRs", summary.PassedRIRs), + slog.String("quorum", summary.QuorumResult), + )) } return bgrpc.CAAResultToPB(filterProblemDetails(prob), va.perspective, va.rir) @@ -155,15 +184,12 @@ func (va *ValidationAuthorityImpl) checkCAA( return berrors.DNSError("%s", err) } - va.log.AuditInfo("Checked CAA records", map[string]any{ - "identifier": ident.Value, - "present": foundAt != "", - "requester": params.accountURIID, - "challenge": params.validationMethod, - "valid": valid, - "foundAt": foundAt, - "response": response, - }) + va.log.AuditInfo(ctx, "Checked CAA records", + slog.Bool("present", foundAt != ""), + slog.String("foundAt", foundAt), + slog.Bool("valid", valid), + slog.Any("response", response), + ) if !valid { return berrors.CAAError("CAA record for %s prevents issuance", foundAt) } diff --git a/va/caa_test.go b/va/caa_test.go index bf0171b921d..e80412f7fcb 100644 --- a/va/caa_test.go +++ b/va/caa_test.go @@ -2,11 +2,9 @@ package va import ( "context" - "encoding/json" "errors" "fmt" "net/netip" - "regexp" "slices" "strings" "testing" @@ -21,7 +19,7 @@ import ( "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" vapb "github.com/letsencrypt/boulder/va/proto" ) @@ -441,55 +439,55 @@ func TestCAALogging(t *testing.T) { Domain: "reserved.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"reserved.com\",\"identifier\":\"reserved.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":false}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:reserved.com}]" method=http-01 present=true foundAt=reserved.com valid=false response=" MsgHdr"`, }, { Domain: "reserved.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeDNS01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"dns-01\",\"foundAt\":\"reserved.com\",\"identifier\":\"reserved.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":false}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:reserved.com}]" method=dns-01 present=true foundAt=reserved.com valid=false response=" MsgHdr"`, }, { Domain: "mixedcase.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"mixedcase.com\",\"identifier\":\"mixedcase.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":false}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:mixedcase.com}]" method=http-01 present=true foundAt=mixedcase.com valid=false response=" MsgHdr"`, }, { Domain: "critical.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"critical.com\",\"identifier\":\"critical.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":false}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:critical.com}]" method=http-01 present=true foundAt=critical.com valid=false response=" MsgHdr"`, }, { Domain: "present.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"present.com\",\"identifier\":\"present.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":true}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:present.com}]" method=http-01 present=true foundAt=present.com valid=true response=" MsgHdr"`, }, { Domain: "not.here.but.still.present.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"present.com\",\"identifier\":\"not.here.but.still.present.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":true}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:not.here.but.still.present.com}]" method=http-01 present=true foundAt=present.com valid=true response=" MsgHdr"`, }, { Domain: "multi-crit-present.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"multi-crit-present.com\",\"identifier\":\"multi-crit-present.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":true}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:multi-crit-present.com}]" method=http-01 present=true foundAt=multi-crit-present.com valid=true response=" MsgHdr"`, }, { Domain: "present-with-parameter.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"present-with-parameter.com\",\"identifier\":\"present-with-parameter.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":true}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:present-with-parameter.com}]" method=http-01 present=true foundAt=present-with-parameter.com valid=true response=" MsgHdr"`, }, { Domain: "satisfiable-wildcard-override.com", AccountURIID: 12345, ChallengeType: core.ChallengeTypeHTTP01, - ExpectedLogline: "INFO: [AUDIT] Checked CAA records JSON={\"challenge\":\"http-01\",\"foundAt\":\"satisfiable-wildcard-override.com\",\"identifier\":\"satisfiable-wildcard-override.com\",\"present\":true,\"requester\":12345,\"response\":\" MsgHdr\",\"valid\":false}", + ExpectedLogline: `level=INFO msg="Checked CAA records" acct=12345 authz=123 idents="[{Type:dns Value:satisfiable-wildcard-override.com}]" method=http-01 present=true foundAt=satisfiable-wildcard-override.com valid=false response=" MsgHdr"`, }, } @@ -498,11 +496,12 @@ func TestCAALogging(t *testing.T) { mockLog := va.log.(*blog.Mock) defer mockLog.Clear() - params := &caaParams{ - accountURIID: tc.AccountURIID, - validationMethod: tc.ChallengeType, - } - _ = va.checkCAA(ctx, identifier.NewDNS(tc.Domain), params) + _, _ = va.DoCAA(t.Context(), &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS(tc.Domain).ToProto(), + ValidationMethod: string(tc.ChallengeType), + AccountURIID: tc.AccountURIID, + AuthzID: "123", + }) caaLogLines := mockLog.GetAllMatching(`Checked CAA records`) if len(caaLogLines) != 1 { @@ -510,7 +509,7 @@ func TestCAALogging(t *testing.T) { strings.Join(mockLog.GetAll(), "\n"), tc.ExpectedLogline) } else { - test.AssertEquals(t, caaLogLines[0], tc.ExpectedLogline) + test.AssertContains(t, caaLogLines[0], tc.ExpectedLogline) } }) } @@ -527,8 +526,10 @@ func TestDoCAAErrMessage(t *testing.T) { domain := "caa-timeout.com" resp, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{ Identifier: identifier.NewDNS(domain).ToProto(), + AuthzID: "123", ValidationMethod: string(core.ChallengeTypeHTTP01), AccountURIID: 12345, + AuthzIDInt: 678910, }) // The lookup itself should not return an error @@ -552,6 +553,7 @@ func TestDoCAAParams(t *testing.T) { _, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{ Identifier: identifier.NewDNS("present.com").ToProto(), AccountURIID: 12345, + AuthzIDInt: 678910, }) test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod") @@ -560,6 +562,7 @@ func TestDoCAAParams(t *testing.T) { Identifier: identifier.NewDNS("present.com").ToProto(), ValidationMethod: "tls-sni-01", AccountURIID: 12345, + AuthzIDInt: 678910, }) test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod") @@ -567,6 +570,7 @@ func TestDoCAAParams(t *testing.T) { _, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{ Identifier: identifier.NewDNS("present.com").ToProto(), ValidationMethod: string(core.ChallengeTypeHTTP01), + AuthzIDInt: 678910, }) test.AssertError(t, err, "calling IsCAAValid without an AccountURIID") @@ -575,8 +579,17 @@ func TestDoCAAParams(t *testing.T) { Identifier: identifier.NewIP(netip.MustParseAddr("127.0.0.1")).ToProto(), ValidationMethod: string(core.ChallengeTypeHTTP01), AccountURIID: 12345, + AuthzIDInt: 678910, }) test.AssertError(t, err, "calling IsCAAValid with a non-DNS identifier type") + + // Calling IsCAAValid without an AuthzID should fail. + _, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS("present.com").ToProto(), + ValidationMethod: string(core.ChallengeTypeHTTP01), + AccountURIID: 12345, + }) + test.AssertError(t, err, "calling isCAAValid without an Authz ID") } var errCAABrokenDNSClient = errors.New("dnsClient is broken") @@ -624,25 +637,6 @@ func (b caaHijackedDNS) LookupCAA(_ context.Context, domain string) (*bdns.Resul return &bdns.Result[*dns.CAA]{Final: results}, "caaHijackedDNS", nil } -// parseValidationLogEvent extracts ... from JSON={ ... } in a ValidateChallenge -// audit log and returns it as a validationLogEvent struct. -func parseValidationLogEvent(t *testing.T, log []string) validationLogEvent { - re := regexp.MustCompile(`JSON=\{.*\}`) - var audit validationLogEvent - for _, line := range log { - match := re.FindString(line) - if match != "" { - jsonStr := match[len(`JSON=`):] - if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil { - t.Fatalf("Failed to parse JSON: %v", err) - } - return audit - } - } - t.Fatal("JSON not found in log") - return audit -} - func TestMultiCAARechecking(t *testing.T) { // The remote differential log order is non-deterministic, so let's use // the same UA for all applicable RVAs. @@ -1057,8 +1051,10 @@ func TestMultiCAARechecking(t *testing.T) { isValidRes, err := va.DoCAA(context.TODO(), &vapb.IsCAAValidRequest{ Identifier: tc.ident.ToProto(), + AuthzID: "123", ValidationMethod: string(core.ChallengeTypeDNS01), AccountURIID: 1, + AuthzIDInt: 3, }) test.AssertNotError(t, err, "Should not have errored, but did") @@ -1075,11 +1071,31 @@ func TestMultiCAARechecking(t *testing.T) { } if tc.expectedSummary != nil { - gotAuditLog := parseValidationLogEvent(t, mockLog.GetAllMatching("CAA check result JSON=.*")) + mpicLog := mockLog.GetAllMatching("mpic.quorum") + test.AssertEquals(t, len(mpicLog), 1) + slices.Sort(tc.expectedSummary.Passed) + if len(tc.expectedSummary.Passed) > 1 { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.passed=\"%v\"", tc.expectedSummary.Passed)) + } else { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.passed=%v", tc.expectedSummary.Passed)) + } + slices.Sort(tc.expectedSummary.Failed) + if len(tc.expectedSummary.Failed) > 1 { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.failed=\"%v\"", tc.expectedSummary.Failed)) + } else { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.failed=%v", tc.expectedSummary.Failed)) + } + slices.Sort(tc.expectedSummary.PassedRIRs) - test.AssertDeepEquals(t, gotAuditLog.Summary, tc.expectedSummary) + if len(tc.expectedSummary.PassedRIRs) > 1 { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.passedRIRs=\"%v\"", tc.expectedSummary.PassedRIRs)) + } else { + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.passedRIRs=%v", tc.expectedSummary.PassedRIRs)) + } + + test.AssertContains(t, mpicLog[0], fmt.Sprintf("mpic.quorum=%v", tc.expectedSummary.QuorumResult)) } gotAnyRemoteFailures := mockLog.GetAllMatching("CAA check failed due to remote failures:") diff --git a/va/dns.go b/va/dns.go index 0e3ffd41d7d..71ec1017616 100644 --- a/va/dns.go +++ b/va/dns.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "errors" "fmt" + "log/slog" "net/netip" "slices" "strings" @@ -84,7 +85,7 @@ func (va *ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string } addrs := append(addrsAAAA, addrsA...) - va.log.Debugf("Resolved addresses for %s: %s", hostname, addrs) + va.log.Debug(ctx, "Resolved addresses", slog.String("hostname", hostname), slog.Any("addrs", addrs)) return addrs, resolvers, nil } @@ -121,7 +122,9 @@ func (va *ValidationAuthorityImpl) validateDNSAccount01(ctx context.Context, ide // Construct the challenge prefix specific to DNS-ACCOUNT-01 challengePrefix := fmt.Sprintf("_%s.%s", prefixLabel, core.DNSPrefix) - va.log.Debugf("DNS-ACCOUNT-01: Querying TXT for %q (derived from account URI %q)", fmt.Sprintf("%s.%s", challengePrefix, ident.Value), accountURI) + va.log.Debug(ctx, "Querying TXT", + slog.String("accountURI", accountURI), + slog.String("validationDomainName", fmt.Sprintf("%s.%s", challengePrefix, ident.Value))) // Call the common validation logic records, err := va.validateDNS(ctx, ident, challengePrefix, keyAuthorization) diff --git a/va/http.go b/va/http.go index f6ec25fb565..7f3c3263abf 100644 --- a/va/http.go +++ b/va/http.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "net/http" "net/netip" @@ -516,7 +517,7 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( records := []core.ValidationRecord{baseRecord} numRedirects := 0 processRedirect := func(req *http.Request, via []*http.Request) error { - va.log.Debugf("processing a HTTP redirect from the server to %q", req.URL.String()) + va.log.Debug(ctx, "processing HTTP redirect", slog.String("to", req.URL.String())) // Only process up to maxRedirect redirects if numRedirects > maxRedirect { return berrors.ConnectionFailureError("Too many redirects") @@ -591,7 +592,7 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( return err } - va.log.Debugf("following redirect to host %q url %q", req.Host, req.URL.String()) + va.log.Debug(ctx, "following HTTP redirect", slog.String("to", req.URL.String())) // Replace the transport's DialContext with the new preresolvedDialer for // the redirect. transport.DialContext = redirDialer.DialContext @@ -669,7 +670,6 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, token string, keyAuthorization string) ([]core.ValidationRecord, error) { if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { - va.log.Errf("Identifier type for HTTP-01 challenge was not DNS or IP: %s", ident) return nil, berrors.MalformedError("Identifier type for HTTP-01 challenge was not DNS or IP") } @@ -684,7 +684,7 @@ func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident ide if payload != keyAuthorization { problem := berrors.UnauthorizedError("The key authorization file from the server did not match this challenge. Expected %q (got %q)", keyAuthorization, payload) - va.log.Infof("%s for %s", problem, ident) + va.log.Info(ctx, fmt.Sprintf("%s for %s", problem, ident)) return validationRecords, problem } diff --git a/va/http_test.go b/va/http_test.go index 417049348d0..63167fa023a 100644 --- a/va/http_test.go +++ b/va/http_test.go @@ -1455,19 +1455,15 @@ func TestHTTP(t *testing.T) { if err != nil { t.Fatalf("Failed to follow http.StatusMovedPermanently redirect") } - redirectValid := `following redirect to host "" url "http://localhost.com/.well-known/acme-challenge/` + pathValid + `"` - matchedValidRedirect := log.GetAllMatching(redirectValid) - test.AssertEquals(t, len(matchedValidRedirect), 1) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://localhost.com/.well-known/acme-challenge/`+pathValid)), 1) log.Clear() _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Failed to follow http.StatusFound redirect") } - redirectMoved := `following redirect to host "" url "http://localhost.com/.well-known/acme-challenge/` + pathMoved + `"` - matchedMovedRedirect := log.GetAllMatching(redirectMoved) - test.AssertEquals(t, len(matchedValidRedirect), 1) - test.AssertEquals(t, len(matchedMovedRedirect), 1) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://localhost.com/.well-known/acme-challenge/`+pathValid)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://localhost.com/.well-known/acme-challenge/`+pathMoved)), 1) _, err = va.validateHTTP01(ctx, identifier.NewDNS("always.invalid"), pathFound, ka(pathFound)) if err == nil { @@ -1527,25 +1523,21 @@ func TestHTTPRedirectLookup(t *testing.T) { if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, err) } - redirectValid := `following redirect to host "" url "http://localhost.com/.well-known/acme-challenge/` + pathValid + `"` - matchedValidRedirect := log.GetAllMatching(redirectValid) - test.AssertEquals(t, len(matchedValidRedirect), 1) - test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 2) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://localhost.com/.well-known/acme-challenge/`+pathValid)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses.*addrs=\[127.0.0.1\]`)), 2) log.Clear() _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, err) } - redirectMoved := `following redirect to host "" url "http://localhost.com/.well-known/acme-challenge/` + pathMoved + `"` - matchedMovedRedirect := log.GetAllMatching(redirectMoved) - test.AssertEquals(t, len(matchedMovedRedirect), 1) - test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 3) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://localhost.com/.well-known/acme-challenge/`+pathMoved)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses.*addrs=\[127.0.0.1\]`)), 3) log.Clear() _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathReLookupInvalid, ka(pathReLookupInvalid)) test.AssertError(t, err, "error for pathReLookupInvalid should not be nil") - test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses.*addrs=\[127.0.0.1\]`)), 1) prob := detailedError(err) test.AssertDeepEquals(t, prob, probs.Connection(`127.0.0.1: Fetching http://invalid.invalid/path: Invalid host in redirect target, must end in IANA registered TLD`)) @@ -1554,10 +1546,9 @@ func TestHTTPRedirectLookup(t *testing.T) { if err != nil { t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, err) } - redirectPattern := `following redirect to host "" url "http://other.valid.com:\d+/path"` - test.AssertEquals(t, len(log.GetAllMatching(redirectPattern)), 1) - test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 1) - test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid.com: \[127.0.0.1\]`)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses.*localhost.com.*addrs=\[127.0.0.1\]`)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`following HTTP redirect.*http://other.valid.com:\d+/path`)), 1) + test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses.*other.valid.com.*addrs=\[127.0.0.1\]`)), 1) log.Clear() _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathRedirectInvalidPort, ka(pathRedirectInvalidPort)) diff --git a/va/proto/va.pb.go b/va/proto/va.pb.go index b65fe526ad9..d7115d862e7 100644 --- a/va/proto/va.pb.go +++ b/va/proto/va.pb.go @@ -29,7 +29,8 @@ type IsCAAValidRequest struct { Identifier *proto.Identifier `protobuf:"bytes,5,opt,name=identifier,proto3" json:"identifier,omitempty"` ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` AccountURIID int64 `protobuf:"varint,3,opt,name=accountURIID,proto3" json:"accountURIID,omitempty"` - AuthzID string `protobuf:"bytes,4,opt,name=authzID,proto3" json:"authzID,omitempty"` + AuthzID string `protobuf:"bytes,4,opt,name=authzID,proto3" json:"authzID,omitempty"` // TODO(#8722): reserve + AuthzIDInt int64 `protobuf:"varint,6,opt,name=authzIDInt,proto3" json:"authzIDInt,omitempty"` // TODO(#8722): rename unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -92,6 +93,13 @@ func (x *IsCAAValidRequest) GetAuthzID() string { return "" } +func (x *IsCAAValidRequest) GetAuthzIDInt() int64 { + if x != nil { + return x.AuthzIDInt + } + return 0 +} + // If CAA is valid for the requested domain, the problem will be empty type IsCAAValidResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -222,9 +230,11 @@ func (x *PerformValidationRequest) GetExpectedKeyAuthorization() string { } type AuthzMeta struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - RegID int64 `protobuf:"varint,2,opt,name=regID,proto3" json:"regID,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 4 + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // TODO(#8722): reserve + RegID int64 `protobuf:"varint,2,opt,name=regID,proto3" json:"regID,omitempty"` + IdInt int64 `protobuf:"varint,3,opt,name=idInt,proto3" json:"idInt,omitempty"` // TODO(#8722): rename unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -273,6 +283,13 @@ func (x *AuthzMeta) GetRegID() int64 { return 0 } +func (x *AuthzMeta) GetIdInt() int64 { + if x != nil { + return x.IdInt + } + return 0 +} + type ValidationResult struct { state protoimpl.MessageState `protogen:"open.v1"` Records []*proto.ValidationRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` @@ -346,7 +363,7 @@ var File_va_proto protoreflect.FileDescriptor var file_va_proto_rawDesc = string([]byte{ 0x0a, 0x08, 0x76, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x61, 0x1a, 0x15, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x01, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd5, 0x01, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, @@ -357,7 +374,9 @@ var file_va_proto_rawDesc = string([]byte{ 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x78, 0x0a, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x49, 0x44, 0x49, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x49, 0x44, 0x49, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x78, 0x0a, 0x12, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, @@ -379,33 +398,34 @@ var file_va_proto_rawDesc = string([]byte{ 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x31, 0x0a, 0x09, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x47, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, - 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x22, - 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, - 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, - 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43, 0x0a, 0x02, 0x56, 0x41, - 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, - 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, - 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, 0x43, 0x41, 0x41, 0x12, - 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, - 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, - 0x65, 0x72, 0x2f, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x64, 0x49, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x69, 0x64, 0x49, 0x6e, 0x74, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, + 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, + 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, + 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, + 0x32, 0x43, 0x0a, 0x02, 0x56, 0x41, 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, + 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, + 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, + 0x44, 0x6f, 0x43, 0x41, 0x41, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, + 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/va/proto/va.proto b/va/proto/va.proto index 7fba73f6e3a..950d0b4a1f0 100644 --- a/va/proto/va.proto +++ b/va/proto/va.proto @@ -14,14 +14,15 @@ service CAA { } message IsCAAValidRequest { - // Next unused field number: 6 + // Next unused field number: 7 reserved 1; // Previously domain // NOTE: For DNS identifiers, the value may be a wildcard domain name (e.g. // `*.example.com`). core.Identifier identifier = 5; string validationMethod = 2; int64 accountURIID = 3; - string authzID = 4; + string authzID = 4; // TODO(#8722): reserve + int64 authzIDInt = 6; // TODO(#8722): rename } // If CAA is valid for the requested domain, the problem will be empty @@ -41,8 +42,10 @@ message PerformValidationRequest { } message AuthzMeta { - string id = 1; + // Next unused field number: 4 + string id = 1; // TODO(#8722): reserve int64 regID = 2; + int64 idInt = 3; // TODO(#8722): rename } message ValidationResult { diff --git a/va/tlsalpn.go b/va/tlsalpn.go index 8b150b16575..af889cfc9d8 100644 --- a/va/tlsalpn.go +++ b/va/tlsalpn.go @@ -11,6 +11,7 @@ import ( "encoding/hex" "errors" "fmt" + "log/slog" "net" "net/netip" "strconv" @@ -150,18 +151,16 @@ func (va *ValidationAuthorityImpl) getChallengeCert( case identifier.TypeIP: reverseIP, err := dns.ReverseAddr(ident.Value) if err != nil { - va.log.Infof("%s Failed to parse IP address %s.", core.ChallengeTypeTLSALPN01, ident.Value) return nil, nil, fmt.Errorf("failed to parse IP address") } serverName = reverseIP default: // This should never happen. The calling function should check the // identifier type. - va.log.Infof("%s Unknown identifier type '%s' for %s.", core.ChallengeTypeTLSALPN01, ident.Type, ident.Value) - return nil, nil, fmt.Errorf("unknown identifier type: %s", ident.Type) + return nil, nil, fmt.Errorf("unrecognized identifier type: %s", ident.Type) } - va.log.Infof("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, ident, hostPort, serverName) + va.log.Info(ctx, "attempting to validate", slog.String("hostPort", hostPort), slog.String("serverName", serverName)) dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout) defer cancel() @@ -190,7 +189,6 @@ func (va *ValidationAuthorityImpl) getChallengeCert( conn, err := dialer.DialContext(dialCtx, "tcp", hostPort) if err != nil { - va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, ident, err, err) if (hostIP != netip.Addr{}) { // Wrap the validation error and the IP of the remote host in an // IPError so we can display the IP in the problem details returned @@ -205,13 +203,9 @@ func (va *ValidationAuthorityImpl) getChallengeCert( cs := conn.(*tls.Conn).ConnectionState() certs := cs.PeerCertificates if len(certs) == 0 { - va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, ident.Value) return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01) } - for i, cert := range certs { - va.log.Infof("%s challenge for %s received certificate (%d of %d): cert=[%s]", - core.ChallengeTypeTLSALPN01, ident.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)) - } + va.log.Info(ctx, "received certificate", slog.String("cert", hex.EncodeToString(certs[0].Raw))) return certs[0], &cs, nil } @@ -295,7 +289,6 @@ func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.Object func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { - va.log.Infof("Identifier type for TLS-ALPN-01 challenge was not DNS or IP: %s", ident) return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 challenge was not DNS or IP") } diff --git a/va/va.go b/va/va.go index 971568d3ce8..c986d17b9ce 100644 --- a/va/va.go +++ b/va/va.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "errors" "fmt" + "log/slog" "maps" "math/rand/v2" "net" @@ -14,6 +15,7 @@ import ( "os" "regexp" "slices" + "strconv" "strings" "syscall" "time" @@ -24,13 +26,13 @@ import ( "google.golang.org/protobuf/proto" "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/probs" vapb "github.com/letsencrypt/boulder/va/proto" @@ -330,17 +332,17 @@ func (va *ValidationAuthorityImpl) runExperiment( } va.metrics.experimentConcurrence.WithLabelValues(operation, "false").Inc() - logArgs := map[string]any{ - "operation": operation, - "primaryProblem": primaryProblem, - "primaryValidationRecords": primaryValidationRecords, - "experimentProblem": experimentProblem, - "experimentValidationRecords": experimentValidationRecords, + expAttrs := []slog.Attr{ + slog.String("operation", operation), + slog.Any("primaryProblem", primaryProblem), + slog.Any("primaryValidationRecords", primaryValidationRecords), + slog.Any("experimentProblem", experimentProblem), + slog.Any("experimentValidationRecords", experimentValidationRecords), } if err != nil { - logArgs["experimentErr"] = err.Error() + expAttrs = append(expAttrs, blog.Error(err)) } - va.log.AuditInfo("Primary VA disagreed with experimental VA", logArgs) + va.log.AuditInfo(ctx, "Primary VA disagreed with experimental VA", expAttrs...) } // maxAllowedFailures returns the maximum number of allowed failures @@ -608,6 +610,8 @@ func summarizeMPIC(passed, failed []string, passedRIRSet map[string]struct{}) *m // Internal logic errors are logged. If the number of operation failures exceeds // va.maxRemoteFailures, the first encountered problem is returned as a // *probs.ProblemDetails. +// +// It always returns a non-nil summary, whether or not there's also a prob. func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) (*mpicSummary, *probs.ProblemDetails) { remoteVACount := len(va.remoteVAs) // - Mar 15, 2026: MUST implement using at least 3 perspectives @@ -616,7 +620,7 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem // See "Phased Implementation Timeline" in // https://github.com/cabforum/servercert/blob/main/docs/BR.md#3229-multi-perspective-issuance-corroboration if remoteVACount < 3 { - return nil, probs.ServerInternal("Insufficient remote perspectives: need at least 3") + return summarizeMPIC(nil, nil, nil), probs.ServerInternal("Insufficient remote perspectives: need at least 3") } type response struct { @@ -666,7 +670,7 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem if core.IsCanceled(resp.err) { currProb = probs.ServerInternal("Secondary validation RPC canceled") } else { - va.log.Errf("Operation on remote VA (%s) failed: %s", resp.addr, resp.err) + va.log.Error(ctx, "Operation on remote VA failed", resp.err, slog.String("addr", resp.addr)) currProb = probs.ServerInternal("Secondary validation RPC failed") } } else if resp.result.GetProblem() != nil { @@ -676,7 +680,7 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem var err error currProb, err = bgrpc.PBToProblemDetails(resp.result.GetProblem()) if err != nil { - va.log.Errf("Operation on Remote VA (%s) returned malformed problem: %s", resp.addr, err) + va.log.Error(ctx, "Operation on Remote VA returned malformed problem", err, slog.String("addr", resp.addr)) currProb = probs.ServerInternal("Secondary validation RPC returned malformed result") } } else { @@ -719,19 +723,6 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem return summarizeMPIC(passed, failed, passedRIRs), firstProb } -// validationLogEvent is a struct that contains the information needed to log -// the results of DoCAA and DoDCV. -type validationLogEvent struct { - AuthzID string - Requester int64 - Identifier identifier.ACMEIdentifier - Challenge core.Challenge - Error string `json:",omitempty"` - InternalError string `json:",omitempty"` - Latency float64 - Summary *mpicSummary `json:",omitempty"` -} - // DoDCV conducts a local Domain Control Validation (DCV) for the specified // challenge. When invoked on the primary Validation Authority (VA) and the // local validation succeeds, it also performs DCV validations using the @@ -747,6 +738,21 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV return nil, berrors.InternalServerError("Incomplete validation request") } + // TODO(#8722): remove this and return req.Authz.Id to isAnyNilOrZero check + // above when Authz IDs are int64-only + var authzIDInt int64 + if req.Authz.IdInt != 0 { + authzIDInt = req.Authz.IdInt + } else if req.Authz.Id != "" { + parsed, err := strconv.ParseInt(req.Authz.Id, 10, 64) + if err != nil { + return nil, berrors.MalformedError("Unable to parse Authz ID %q as integer: %v", req.Authz.Id, err) + } + authzIDInt = parsed + } else { + return nil, berrors.InternalServerError("incomplete validation request") + } + ident := identifier.FromProto(req.Identifier) chall, err := bgrpc.PBToChallenge(req.Challenge) @@ -759,30 +765,39 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV return nil, berrors.MalformedError("challenge failed consistency check: %s", err) } + // Set the log attributes that we want to appear on all subsequent log lines + ctx = blog.ContextWith(ctx, + blog.Acct(req.Authz.RegID), + blog.Authz(authzIDInt), + blog.Idents(ident), + slog.String("method", string(chall.Type)), + slog.String("token", chall.Token), + ) + // Initialize variables and a deferred function to handle validation latency // metrics, log validation errors, and log an MPIC summary. Avoid using := - // to redeclare `prob`, `localLatency`, or `summary` below this point. + // to redeclare any of these variables below this point. var prob *probs.ProblemDetails - var summary *mpicSummary var localLatency time.Duration + var logAttrs []slog.Attr start := va.clk.Now() - logEvent := validationLogEvent{ - AuthzID: req.Authz.Id, - Requester: req.Authz.RegID, - Identifier: ident, - Challenge: chall, - } defer func() { + logAttrs = append(logAttrs, + slog.Duration("localLatency", localLatency), + slog.Duration("totalLatency", va.clk.Since(start).Round(time.Millisecond)), + ) + probType := "" outcome := fail if prob != nil { probType = string(prob.Type) - logEvent.Error = prob.String() - logEvent.Challenge.Error = prob - logEvent.Challenge.Status = core.StatusInvalid + logAttrs = append(logAttrs, + slog.String("status", string(core.StatusInvalid)), + slog.String("error", prob.String()), + ) } else { - logEvent.Challenge.Status = core.StatusValid outcome = pass + logAttrs = append(logAttrs, slog.String("status", string(core.StatusValid))) } // Observe local validation latency (primary|remote). @@ -790,12 +805,9 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV if va.isPrimaryVA() { // Observe total validation latency (primary+remote). va.observeLatency(opDCV, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) - logEvent.Summary = summary } - // Log the total validation latency. - logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() - va.log.AuditInfo("Validation result", logEvent) + va.log.AuditInfo(ctx, "Validation result", logAttrs...) }() // For dns-account-01 and dns-persist-01 challenges, construct the account URI @@ -821,15 +833,16 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV // Stop the clock for local validation latency. localLatency = va.clk.Since(start) + logAttrs = append(logAttrs, slog.Any("validationRecords", records)) // Check for malformed ValidationRecords - logEvent.Challenge.ValidationRecord = records - if err == nil && !logEvent.Challenge.RecordsSane() { + chall.ValidationRecord = records + if err == nil && !chall.RecordsSane() { err = errors.New("records from local validation failed sanity check") } if err != nil { - logEvent.InternalError = err.Error() + logAttrs = append(logAttrs, slog.String("internalErr", err.Error())) prob = detailedError(err) } @@ -880,7 +893,14 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV } return remoteva.DoDCV(ctx, validationRequest) } + var summary *mpicSummary summary, prob = va.doRemoteOperation(ctx, op, req) + logAttrs = append(logAttrs, slog.Group("mpic", + slog.Any("passed", summary.Passed), + slog.Any("failed", summary.Failed), + slog.Any("passedRIRs", summary.PassedRIRs), + slog.String("quorum", summary.QuorumResult), + )) } return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) diff --git a/va/va_test.go b/va/va_test.go index 54f3f29a599..eb4d425077c 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -24,12 +24,12 @@ import ( "google.golang.org/grpc" "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/iana" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test" @@ -93,8 +93,8 @@ func createValidationRequest(ident identifier.ACMEIdentifier, challengeType core Validationrecords: nil, }, Authz: &vapb.AuthzMeta{ - Id: "", RegID: 1, + IdInt: 1, }, ExpectedKeyAuthorization: expectedKeyAuthorization, } @@ -339,6 +339,43 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") } +// TODO(#8722): Remove this whole function when Authz IDs are int-only +func TestPerformValidationWithAuthzIDMatrix(t *testing.T) { + t.Parallel() + + va, _ := setup(nil, "", nil, &txtFakeDNS{}) + + // create a challenge with well known token + req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + // manipulate Authz ID for this validation attempt + req.Authz.Id = "" + req.Authz.IdInt = 0 + _, err := va.DoDCV(context.Background(), req) + test.AssertError(t, err, "expected error upon validation request with empty authz ID fields") + + // repeat + req = createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + req.Authz.Id = "1" + req.Authz.IdInt = 0 + res, err := va.DoDCV(context.Background(), req) + test.AssertNotError(t, err, "domain validation request failed") + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + + req = createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + req.Authz.Id = "" + req.Authz.IdInt = 1 + res, err = va.DoDCV(context.Background(), req) + test.AssertNotError(t, err, "domain validation request failed") + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + + req = createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + req.Authz.Id = "1" + req.Authz.IdInt = 1 + res, err = va.DoDCV(context.Background(), req) + test.AssertNotError(t, err, "domain validation request failed") + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) +} + func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { t.Parallel() @@ -423,7 +460,7 @@ func TestExperimentalVAConcurrence(t *testing.T) { // The addressesResolved and addressUsed fields are checked here to make sure they are not accidentally // base64-encoded (which can happen if we log the protobuf `corepb.ValidationRecord` instead of the nicely // JSON-serializable struct `core.ValidationRecord`) - expectLog: `Primary VA disagreed with experimental VA.*"addressesResolved":\["127.0.0.1"\],"addressUsed":"127.0.0.1"`, + expectLog: `Primary VA disagreed with experimental VA.*AddressesResolved:\[127.0.0.1\] AddressUsed:127.0.0.1`, }, { name: "both fail", @@ -535,13 +572,12 @@ func TestInternalErrorLogged(t *testing.T) { va, mockLog := setup(nil, "", nil, &ipFakeDNS{}) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() req := createValidationRequest(identifier.NewDNS("nonexistent.com"), core.ChallengeTypeHTTP01) _, err := va.DoDCV(ctx, req) test.AssertNotError(t, err, "failed validation should not be an error") - matchingLogs := mockLog.GetAllMatching( - `Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) + matchingLogs := mockLog.GetAllMatching(`Validation result.*internalErr=".*: connection refused"`) test.AssertEquals(t, len(matchingLogs), 1) } @@ -566,7 +602,7 @@ func TestPerformValidationValid(t *testing.T) { t.Fatalf("Wrong number of matching lines for 'Validation result'") } - if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"good-dns01.com"}`) { + if !strings.Contains(resultLog[0], `{Type:dns Value:good-dns01.com}`) { t.Error("PerformValidation didn't log validation identifier.") } } @@ -596,12 +632,12 @@ func TestPerformValidationWildcard(t *testing.T) { } // We expect that the top level Identifier reflect the wildcard name - if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"*.good-dns01.com"}`) { + if !strings.Contains(resultLog[0], `{Type:dns Value:*.good-dns01.com}`) { t.Errorf("PerformValidation didn't log correct validation identifier.") } // We expect that the ValidationRecord contain the correct non-wildcard // hostname that was validated - if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) { + if !strings.Contains(resultLog[0], `Hostname:good-dns01.com`) { t.Errorf("PerformValidation didn't log correct validation record hostname.") } } diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go index 236f2869f5e..e589f615619 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.go @@ -3,4 +3,4 @@ package aws // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.41.6" +const goModuleVersion = "1.41.7" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md index 12664aad38e..4eebedc3bf1 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.7.10 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. + # v1.7.9 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go index 8ef877434e7..84a7e483fd4 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.go @@ -3,4 +3,4 @@ package eventstream // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.7.9" +const goModuleVersion = "1.7.10" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md index 91225aeb845..6f932e910a8 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.32.17 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.32.16 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go index 9fce44721ac..fdbfa78e45b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.go @@ -3,4 +3,4 @@ package config // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.32.16" +const goModuleVersion = "1.32.17" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md index edf9b457d9f..0b215e6b831 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.19.16 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.19.15 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go index 86a94b5b223..5abad90cd98 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.go @@ -3,4 +3,4 @@ package credentials // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.19.15" +const goModuleVersion = "1.19.16" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md index 93671df136b..e17294549fc 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.18.23 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.18.22 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go index 0479eaf656e..7f59387edc3 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.go @@ -3,4 +3,4 @@ package imds // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.18.22" +const goModuleVersion = "1.18.23" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md index 9aa4e19e646..0990a4143a7 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.4.23 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.4.22 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go index cd7837e2fa5..05a8d3e7bc1 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.go @@ -3,4 +3,4 @@ package configsources // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.4.22" +const goModuleVersion = "1.4.23" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md index abb379a4da9..49577e3e94c 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.md @@ -1,3 +1,8 @@ +# v2.7.23 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v2.7.22 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go index e295061a31e..1e92900a1e8 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.go @@ -3,4 +3,4 @@ package endpoints // goModuleVersion is the tagged release for this module -const goModuleVersion = "2.7.22" +const goModuleVersion = "2.7.23" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md index 1ba4bf347fd..e1e3c23a740 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.4.24 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.4.23 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go index d4e42ece666..455cb74e1a5 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/internal/v4a/go_module_metadata.go @@ -3,4 +3,4 @@ package v4a // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.4.23" +const goModuleVersion = "1.4.24" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md index fb6a52e0c8d..cf6c5e09116 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.13.9 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. + # v1.13.8 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go index 2ecad3bc626..e145070706f 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.go @@ -3,4 +3,4 @@ package acceptencoding // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.13.8" +const goModuleVersion = "1.13.9" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md index 005004d0c10..f3442de4b75 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.9.15 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.9.14 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go index 3b7c30d1f25..7fa714cfb95 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/checksum/go_module_metadata.go @@ -3,4 +3,4 @@ package checksum // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.9.14" +const goModuleVersion = "1.9.15" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md index 9296c663296..96adad52610 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.13.23 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.13.22 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go index e70562dbd06..5737e9c0c1b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.go @@ -3,4 +3,4 @@ package presignedurl // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.13.22" +const goModuleVersion = "1.13.23" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md index d4fc36de96b..329570ad12b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.19.23 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.19.22 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go index 38f0f9cba73..6652be7f295 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/internal/s3shared/go_module_metadata.go @@ -3,4 +3,4 @@ package s3shared // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.19.22" +const goModuleVersion = "1.19.23" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md index 2780d0de2e1..8183fc352c7 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.101.0 (2026-05-06) + +* **Feature**: Validate outpost access point resource name + +# v1.100.1 (2026-04-29) + +* **Bug Fix**: Fix a memory leak in the credential cache used for S3 Express session credentials. +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + +# v1.100.0 (2026-04-22) + +* **Feature**: This release adds five additional checksum algorithms for S3 data integrity (MD5, SHA-512, XXHash3, XXHash64, XXHash128) and support for S3 Inventory on directory buckets (S3 Express One Zone). + # v1.99.1 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_CompleteMultipartUpload.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_CompleteMultipartUpload.go index 9109addaa7e..f70ecc8982d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_CompleteMultipartUpload.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_CompleteMultipartUpload.go @@ -238,6 +238,13 @@ type CompleteMultipartUploadInput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit MD5 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // This header can be used as a data integrity check to verify that the data // received is the same data that was originally sent. This header specifies the // Base64 encoded, 160-bit SHA1 digest of the object. For more information, see [Checking object integrity] @@ -254,6 +261,13 @@ type CompleteMultipartUploadInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 512-bit SHA512 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // This header specifies the checksum type of the object, which determines how // part-level checksums are combined to create an object-level checksum for // multipart objects. You can use this header as a data integrity check to verify @@ -264,6 +278,30 @@ type CompleteMultipartUploadInput struct { // Guide. ChecksumType types.ChecksumType + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit XXHASH128 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH3 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH64 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // The account ID of the expected bucket owner. If the account ID that you provide // does not match the actual owner of the bucket, the request fails with the HTTP // status code 403 Forbidden (access denied). @@ -402,6 +440,12 @@ type CompleteMultipartUploadOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide] + // . + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. When you use the API // operation on an object that was uploaded using multipart uploads, this value may @@ -424,6 +468,12 @@ type CompleteMultipartUploadOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // The checksum type, which determines how part-level checksums are combined to // create an object-level checksum for multipart objects. You can use this header // as a data integrity check to verify that the checksum type that is received is @@ -433,6 +483,24 @@ type CompleteMultipartUploadOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType types.ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag that identifies the newly created object's data. Objects with // different object data will have different entity tags. The entity tag is an // opaque string. The entity tag may or may not be an MD5 digest of the object diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteBucketInventoryConfiguration.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteBucketInventoryConfiguration.go index a502ae6a3bf..af1c47aba0f 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteBucketInventoryConfiguration.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteBucketInventoryConfiguration.go @@ -13,16 +13,33 @@ import ( smithyhttp "github.com/aws/smithy-go/transport/http" ) -// This operation is not supported for directory buckets. -// // Deletes an S3 Inventory configuration (identified by the inventory ID) from the // bucket. // -// To use this operation, you must have permissions to perform the +// Directory buckets - For directory buckets, you must make requests for this API +// operation to the Regional endpoint. These endpoints support path-style requests +// in the format https://s3express-control.region-code.amazonaws.com/bucket-name . +// Virtual-hosted-style requests aren't supported. For more information about +// endpoints in Availability Zones, see [Regional and Zonal endpoints for directory buckets in Availability Zones]in the Amazon S3 User Guide. For more +// information about endpoints in Local Zones, see [Concepts for directory buckets in Local Zones]in the Amazon S3 User Guide. +// +// Permissions To use this operation, you must have permissions to perform the // s3:PutInventoryConfiguration action. The bucket owner has this permission by // default. The bucket owner can grant this permission to others. For more // information about permissions, see [Permissions Related to Bucket Subresource Operations]and [Managing Access Permissions to Your Amazon S3 Resources]. // +// - General purpose bucket permissions - The s3:PutInventoryConfiguration +// permission is required in a policy. For more information about general purpose +// buckets permissions, see [Using Bucket Policies and User Policies]in the Amazon S3 User Guide. +// +// - Directory bucket permissions - To grant access to this API operation, you +// must have the s3express:PutInventoryConfiguration permission in an IAM +// identity-based policy instead of a bucket policy. For more information about +// directory bucket policies and permissions, see [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]in the Amazon S3 User Guide. +// +// HTTP Host header syntax Directory buckets - The HTTP Host header syntax is +// s3express-control.region-code.amazonaws.com . +// // For information about the Amazon S3 inventory feature, see [Amazon S3 Inventory]. // // After deleting a configuration, Amazon S3 might still deliver one additional @@ -41,11 +58,15 @@ import ( // if your header value is my file.txt , containing two spaces after my , you must // URL encode this value to my%20%20file.txt . // +// [Concepts for directory buckets in Local Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-lzs-for-directory-buckets.html // [Amazon S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html // [ListBucketInventoryConfigurations]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBucketInventoryConfigurations.html // [Permissions Related to Bucket Subresource Operations]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-actions.html#using-with-s3-actions-related-to-bucket-subresources +// [Using Bucket Policies and User Policies]: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html // [Managing Access Permissions to Your Amazon S3 Resources]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html // [PutBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketInventoryConfiguration.html +// [Regional and Zonal endpoints for directory buckets in Availability Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/endpoint-directory-buckets-AZ.html +// [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-security-iam.html // [GetBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketInventoryConfiguration.html func (c *Client) DeleteBucketInventoryConfiguration(ctx context.Context, params *DeleteBucketInventoryConfigurationInput, optFns ...func(*Options)) (*DeleteBucketInventoryConfigurationOutput, error) { if params == nil { @@ -66,6 +87,17 @@ type DeleteBucketInventoryConfigurationInput struct { // The name of the bucket containing the inventory configuration to delete. // + // Directory buckets - When you use this operation with a directory bucket, you + // must use path-style requests in the format + // https://s3express-control.region-code.amazonaws.com/bucket-name . + // Virtual-hosted-style requests aren't supported. Directory bucket names must be + // unique in the chosen Zone (Availability Zone or Local Zone). Bucket names must + // also follow the format bucket-base-name--zone-id--x-s3 (for example, + // DOC-EXAMPLE-BUCKET--usw2-az1--x-s3 ). For information about bucket naming + // restrictions, see [Directory bucket naming rules]in the Amazon S3 User Guide + // + // [Directory bucket naming rules]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html + // // This member is required. Bucket *string @@ -77,6 +109,10 @@ type DeleteBucketInventoryConfigurationInput struct { // The account ID of the expected bucket owner. If the account ID that you provide // does not match the actual owner of the bucket, the request fails with the HTTP // status code 403 Forbidden (access denied). + // + // For directory buckets, this header is not supported in this API operation. If + // you specify this header, the request fails with the HTTP status code 501 Not + // Implemented . ExpectedBucketOwner *string noSmithyDocumentSerde diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteObjects.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteObjects.go index 8211cf7eef4..0fa0d66bca9 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteObjects.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_DeleteObjects.go @@ -205,10 +205,20 @@ type DeleteObjectsInput struct { // // - CRC64NVME // + // - MD5 + // // - SHA1 // // - SHA256 // + // - SHA512 + // + // - XXHASH3 + // + // - XXHASH64 + // + // - XXHASH128 + // // For more information, see [Checking object integrity] in the Amazon S3 User Guide. // // If the individual checksum value you provide through x-amz-checksum-algorithm diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetBucketInventoryConfiguration.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetBucketInventoryConfiguration.go index d88552dec80..592c4dd5ac5 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetBucketInventoryConfiguration.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetBucketInventoryConfiguration.go @@ -14,15 +14,32 @@ import ( smithyhttp "github.com/aws/smithy-go/transport/http" ) -// This operation is not supported for directory buckets. -// // Returns an S3 Inventory configuration (identified by the inventory // configuration ID) from the bucket. // -// To use this operation, you must have permissions to perform the +// Directory buckets - For directory buckets, you must make requests for this API +// operation to the Regional endpoint. These endpoints support path-style requests +// in the format https://s3express-control.region-code.amazonaws.com/bucket-name . +// Virtual-hosted-style requests aren't supported. For more information about +// endpoints in Availability Zones, see [Regional and Zonal endpoints for directory buckets in Availability Zones]in the Amazon S3 User Guide. For more +// information about endpoints in Local Zones, see [Concepts for directory buckets in Local Zones]in the Amazon S3 User Guide. +// +// Permissions To use this operation, you must have permissions to perform the // s3:GetInventoryConfiguration action. The bucket owner has this permission by -// default and can grant this permission to others. For more information about -// permissions, see [Permissions Related to Bucket Subresource Operations]and [Managing Access Permissions to Your Amazon S3 Resources]. +// default. The bucket owner can grant this permission to others. For more +// information about permissions, see [Permissions Related to Bucket Subresource Operations]and [Managing Access Permissions to Your Amazon S3 Resources]. +// +// - General purpose bucket permissions - The s3:GetInventoryConfiguration +// permission is required in a policy. For more information about general purpose +// buckets permissions, see [Using Bucket Policies and User Policies]in the Amazon S3 User Guide. +// +// - Directory bucket permissions - To grant access to this API operation, you +// must have the s3express:GetInventoryConfiguration permission in an IAM +// identity-based policy instead of a bucket policy. For more information about +// directory bucket policies and permissions, see [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]in the Amazon S3 User Guide. +// +// HTTP Host header syntax Directory buckets - The HTTP Host header syntax is +// s3express-control.region-code.amazonaws.com . // // For information about the Amazon S3 inventory feature, see [Amazon S3 Inventory]. // @@ -38,12 +55,16 @@ import ( // if your header value is my file.txt , containing two spaces after my , you must // URL encode this value to my%20%20file.txt . // +// [Concepts for directory buckets in Local Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-lzs-for-directory-buckets.html // [Amazon S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html // [ListBucketInventoryConfigurations]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBucketInventoryConfigurations.html // [Permissions Related to Bucket Subresource Operations]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-actions.html#using-with-s3-actions-related-to-bucket-subresources +// [Using Bucket Policies and User Policies]: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html // [DeleteBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketInventoryConfiguration.html // [Managing Access Permissions to Your Amazon S3 Resources]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html // [PutBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketInventoryConfiguration.html +// [Regional and Zonal endpoints for directory buckets in Availability Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/endpoint-directory-buckets-AZ.html +// [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-security-iam.html func (c *Client) GetBucketInventoryConfiguration(ctx context.Context, params *GetBucketInventoryConfigurationInput, optFns ...func(*Options)) (*GetBucketInventoryConfigurationOutput, error) { if params == nil { params = &GetBucketInventoryConfigurationInput{} @@ -63,6 +84,17 @@ type GetBucketInventoryConfigurationInput struct { // The name of the bucket containing the inventory configuration to retrieve. // + // Directory buckets - When you use this operation with a directory bucket, you + // must use path-style requests in the format + // https://s3express-control.region-code.amazonaws.com/bucket-name . + // Virtual-hosted-style requests aren't supported. Directory bucket names must be + // unique in the chosen Zone (Availability Zone or Local Zone). Bucket names must + // also follow the format bucket-base-name--zone-id--x-s3 (for example, + // DOC-EXAMPLE-BUCKET--usw2-az1--x-s3 ). For information about bucket naming + // restrictions, see [Directory bucket naming rules]in the Amazon S3 User Guide + // + // [Directory bucket naming rules]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html + // // This member is required. Bucket *string @@ -74,6 +106,10 @@ type GetBucketInventoryConfigurationInput struct { // The account ID of the expected bucket owner. If the account ID that you provide // does not match the actual owner of the bucket, the request fails with the HTTP // status code 403 Forbidden (access denied). + // + // For directory buckets, this header is not supported in this API operation. If + // you specify this header, the request fails with the HTTP status code 501 Not + // Implemented . ExpectedBucketOwner *string noSmithyDocumentSerde diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetObject.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetObject.go index c43d23916a5..07a1b5bc87f 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetObject.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_GetObject.go @@ -466,6 +466,12 @@ type GetObjectOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide] + // . + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. For more information, see [Checking object integrity] // in the Amazon S3 User Guide. @@ -480,6 +486,12 @@ type GetObjectOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // The checksum type, which determines how part-level checksums are combined to // create an object-level checksum for multipart objects. You can use this header // response to verify that the checksum type that is received is the same checksum @@ -489,6 +501,24 @@ type GetObjectOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType types.ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Specifies presentational information for the object. ContentDisposition *string @@ -827,7 +857,7 @@ func addGetObjectOutputChecksumMiddlewares(stack *middleware.Stack, options Opti GetValidationMode: getGetObjectRequestValidationModeMember, SetValidationMode: setGetObjectRequestValidationModeMember, ResponseChecksumValidation: options.ResponseChecksumValidation, - ValidationAlgorithms: []string{"CRC64NVME", "CRC32", "CRC32C", "SHA256", "SHA1"}, + ValidationAlgorithms: []string{"CRC64NVME", "CRC32", "CRC32C", "SHA256", "SHA1", "SHA512", "MD5", "XXHASH64", "XXHASH3", "XXHASH128"}, IgnoreMultipartValidation: true, LogValidationSkipped: !options.DisableLogOutputChecksumValidationSkipped, LogMultipartValidationSkipped: !options.DisableLogOutputChecksumValidationSkipped, diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_HeadObject.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_HeadObject.go index 4ef0eb9b3cf..876ba6ef671 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_HeadObject.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_HeadObject.go @@ -413,6 +413,12 @@ type HeadObjectOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide] + // . + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. When you use the API // operation on an object that was uploaded using multipart uploads, this value may @@ -435,6 +441,12 @@ type HeadObjectOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // The checksum type, which determines how part-level checksums are combined to // create an object-level checksum for multipart objects. You can use this header // response to verify that the checksum type that is received is the same checksum @@ -444,6 +456,24 @@ type HeadObjectOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType types.ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Specifies presentational information for the object. ContentDisposition *string diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_ListBucketInventoryConfigurations.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_ListBucketInventoryConfigurations.go index 59ebd6abf8d..29ea5932015 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_ListBucketInventoryConfigurations.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_ListBucketInventoryConfigurations.go @@ -14,8 +14,6 @@ import ( smithyhttp "github.com/aws/smithy-go/transport/http" ) -// This operation is not supported for directory buckets. -// // Returns a list of S3 Inventory configurations for the bucket. You can have up // to 1,000 inventory configurations per bucket. // @@ -27,11 +25,30 @@ import ( // continue the pagination of the list by passing the value in continuation-token // in the request to GET the next page. // -// To use this operation, you must have permissions to perform the +// Directory buckets - For directory buckets, you must make requests for this API +// operation to the Regional endpoint. These endpoints support path-style requests +// in the format https://s3express-control.region-code.amazonaws.com/bucket-name . +// Virtual-hosted-style requests aren't supported. For more information about +// endpoints in Availability Zones, see [Regional and Zonal endpoints for directory buckets in Availability Zones]in the Amazon S3 User Guide. For more +// information about endpoints in Local Zones, see [Concepts for directory buckets in Local Zones]in the Amazon S3 User Guide. +// +// Permissions To use this operation, you must have permissions to perform the // s3:GetInventoryConfiguration action. The bucket owner has this permission by // default. The bucket owner can grant this permission to others. For more // information about permissions, see [Permissions Related to Bucket Subresource Operations]and [Managing Access Permissions to Your Amazon S3 Resources]. // +// - General purpose bucket permissions - The s3:GetInventoryConfiguration +// permission is required in a policy. For more information about general purpose +// buckets permissions, see [Using Bucket Policies and User Policies]in the Amazon S3 User Guide. +// +// - Directory bucket permissions - To grant access to this API operation, you +// must have the s3express:GetInventoryConfiguration permission in an IAM +// identity-based policy instead of a bucket policy. For more information about +// directory bucket policies and permissions, see [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]in the Amazon S3 User Guide. +// +// HTTP Host header syntax Directory buckets - The HTTP Host header syntax is +// s3express-control.region-code.amazonaws.com . +// // For information about the Amazon S3 inventory feature, see [Amazon S3 Inventory] // // The following operations are related to ListBucketInventoryConfigurations : @@ -46,11 +63,15 @@ import ( // if your header value is my file.txt , containing two spaces after my , you must // URL encode this value to my%20%20file.txt . // +// [Concepts for directory buckets in Local Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-lzs-for-directory-buckets.html // [Amazon S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html // [Permissions Related to Bucket Subresource Operations]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-actions.html#using-with-s3-actions-related-to-bucket-subresources +// [Using Bucket Policies and User Policies]: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html // [DeleteBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketInventoryConfiguration.html // [Managing Access Permissions to Your Amazon S3 Resources]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html // [PutBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketInventoryConfiguration.html +// [Regional and Zonal endpoints for directory buckets in Availability Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/endpoint-directory-buckets-AZ.html +// [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-security-iam.html // [GetBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketInventoryConfiguration.html func (c *Client) ListBucketInventoryConfigurations(ctx context.Context, params *ListBucketInventoryConfigurationsInput, optFns ...func(*Options)) (*ListBucketInventoryConfigurationsOutput, error) { if params == nil { @@ -71,6 +92,17 @@ type ListBucketInventoryConfigurationsInput struct { // The name of the bucket containing the inventory configurations to retrieve. // + // Directory buckets - When you use this operation with a directory bucket, you + // must use path-style requests in the format + // https://s3express-control.region-code.amazonaws.com/bucket-name . + // Virtual-hosted-style requests aren't supported. Directory bucket names must be + // unique in the chosen Zone (Availability Zone or Local Zone). Bucket names must + // also follow the format bucket-base-name--zone-id--x-s3 (for example, + // DOC-EXAMPLE-BUCKET--usw2-az1--x-s3 ). For information about bucket naming + // restrictions, see [Directory bucket naming rules]in the Amazon S3 User Guide + // + // [Directory bucket naming rules]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html + // // This member is required. Bucket *string @@ -83,6 +115,10 @@ type ListBucketInventoryConfigurationsInput struct { // The account ID of the expected bucket owner. If the account ID that you provide // does not match the actual owner of the bucket, the request fails with the HTTP // status code 403 Forbidden (access denied). + // + // For directory buckets, this header is not supported in this API operation. If + // you specify this header, the request fails with the HTTP status code 501 Not + // Implemented . ExpectedBucketOwner *string noSmithyDocumentSerde diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketInventoryConfiguration.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketInventoryConfiguration.go index 26ce2cbf128..ce0438210b0 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketInventoryConfiguration.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketInventoryConfiguration.go @@ -14,8 +14,6 @@ import ( smithyhttp "github.com/aws/smithy-go/transport/http" ) -// This operation is not supported for directory buckets. -// // This implementation of the PUT action adds an S3 Inventory configuration // (identified by the inventory ID) to the bucket. You can have up to 1,000 // inventory configurations per bucket. @@ -36,6 +34,13 @@ import ( // to Amazon S3 to write objects to the bucket in the defined location. For an // example policy, see [Granting Permissions for Amazon S3 Inventory and Storage Class Analysis]. // +// Directory buckets - For directory buckets, you must make requests for this API +// operation to the Regional endpoint. These endpoints support path-style requests +// in the format https://s3express-control.region-code.amazonaws.com/bucket-name . +// Virtual-hosted-style requests aren't supported. For more information about +// endpoints in Availability Zones, see [Regional and Zonal endpoints for directory buckets in Availability Zones]in the Amazon S3 User Guide. For more +// information about endpoints in Local Zones, see [Concepts for directory buckets in Local Zones]in the Amazon S3 User Guide. +// // Permissions To use this operation, you must have permission to perform the // s3:PutInventoryConfiguration action. The bucket owner has this permission by // default and can grant this permission to others. @@ -46,11 +51,23 @@ import ( // the destination bucket can also access all object metadata fields that are // available in the inventory report. // +// - General purpose bucket permissions - The s3:PutInventoryConfiguration +// permission is required in a policy. For more information about general purpose +// buckets permissions, see [Using Bucket Policies and User Policies]in the Amazon S3 User Guide. +// +// - Directory bucket permissions - To grant access to this API operation, you +// must have the s3express:PutInventoryConfiguration permission in an IAM +// identity-based policy instead of a bucket policy. For more information about +// directory bucket policies and permissions, see [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]in the Amazon S3 User Guide. +// // To restrict access to an inventory report, see [Restricting access to an Amazon S3 Inventory report] in the Amazon S3 User Guide. // For more information about the metadata fields available in S3 Inventory, see [Amazon S3 Inventory lists] // in the Amazon S3 User Guide. For more information about permissions, see [Permissions related to bucket subresource operations]and [Identity and access management in Amazon S3] // in the Amazon S3 User Guide. // +// HTTP Host header syntax Directory buckets - The HTTP Host header syntax is +// s3express-control.region-code.amazonaws.com . +// // PutBucketInventoryConfiguration has the following special errors: // // HTTP 400 Bad Request Error Code: InvalidArgument @@ -79,15 +96,19 @@ import ( // URL encode this value to my%20%20file.txt . // // [Granting Permissions for Amazon S3 Inventory and Storage Class Analysis]: https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-9 -// [Amazon S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html +// [Concepts for directory buckets in Local Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-lzs-for-directory-buckets.html // [ListBucketInventoryConfigurations]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBucketInventoryConfigurations.html // [S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-inventory.html +// [Restricting access to an Amazon S3 Inventory report]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-s3-inventory +// [Amazon S3 Inventory lists]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-inventory.html#storage-inventory-contents +// [Amazon Web Services Identity and Access Management (IAM) for S3 Express One Zone]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-security-iam.html +// [GetBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketInventoryConfiguration.html +// [Amazon S3 Inventory]: https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html // [Permissions related to bucket subresource operations]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-actions.html#using-with-s3-actions-related-to-bucket-subresources +// [Using Bucket Policies and User Policies]: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html // [DeleteBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketInventoryConfiguration.html // [Identity and access management in Amazon S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html -// [Restricting access to an Amazon S3 Inventory report]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-use-case-10 -// [Amazon S3 Inventory lists]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-inventory.html#storage-inventory-contents -// [GetBucketInventoryConfiguration]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketInventoryConfiguration.html +// [Regional and Zonal endpoints for directory buckets in Availability Zones]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/endpoint-directory-buckets-AZ.html func (c *Client) PutBucketInventoryConfiguration(ctx context.Context, params *PutBucketInventoryConfigurationInput, optFns ...func(*Options)) (*PutBucketInventoryConfigurationOutput, error) { if params == nil { params = &PutBucketInventoryConfigurationInput{} @@ -107,6 +128,17 @@ type PutBucketInventoryConfigurationInput struct { // The name of the bucket where the inventory configuration will be stored. // + // Directory buckets - When you use this operation with a directory bucket, you + // must use path-style requests in the format + // https://s3express-control.region-code.amazonaws.com/bucket-name . + // Virtual-hosted-style requests aren't supported. Directory bucket names must be + // unique in the chosen Zone (Availability Zone or Local Zone). Bucket names must + // also follow the format bucket-base-name--zone-id--x-s3 (for example, + // DOC-EXAMPLE-BUCKET--usw2-az1--x-s3 ). For information about bucket naming + // restrictions, see [Directory bucket naming rules]in the Amazon S3 User Guide + // + // [Directory bucket naming rules]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html + // // This member is required. Bucket *string @@ -123,6 +155,10 @@ type PutBucketInventoryConfigurationInput struct { // The account ID of the expected bucket owner. If the account ID that you provide // does not match the actual owner of the bucket, the request fails with the HTTP // status code 403 Forbidden (access denied). + // + // For directory buckets, this header is not supported in this API operation. If + // you specify this header, the request fails with the HTTP status code 501 Not + // Implemented . ExpectedBucketOwner *string noSmithyDocumentSerde diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketPolicy.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketPolicy.go index 22ed5fdc279..9577fc7ca8b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketPolicy.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutBucketPolicy.go @@ -135,10 +135,20 @@ type PutBucketPolicyInput struct { // // - CRC64NVME // + // - MD5 + // // - SHA1 // // - SHA256 // + // - SHA512 + // + // - XXHASH3 + // + // - XXHASH64 + // + // - XXHASH128 + // // For more information, see [Checking object integrity] in the Amazon S3 User Guide. // // If the individual checksum value you provide through x-amz-checksum-algorithm diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutObject.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutObject.go index b781048d547..d0ad31e0d68 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutObject.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_PutObject.go @@ -290,10 +290,20 @@ type PutObjectInput struct { // // - CRC64NVME // + // - MD5 + // // - SHA1 // // - SHA256 // + // - SHA512 + // + // - XXHASH3 + // + // - XXHASH64 + // + // - XXHASH128 + // // For more information, see [Checking object integrity] in the Amazon S3 User Guide. // // If the individual checksum value you provide through x-amz-checksum-algorithm @@ -336,6 +346,13 @@ type PutObjectInput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit MD5 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // This header can be used as a data integrity check to verify that the data // received is the same data that was originally sent. This header specifies the // Base64 encoded, 160-bit SHA1 digest of the object. For more information, see [Checking object integrity] @@ -352,6 +369,37 @@ type PutObjectInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 512-bit SHA512 digest of the object. For more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit XXHASH128 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH3 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH64 checksum of the object. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Specifies presentational information for the object. For more information, see [https://www.rfc-editor.org/rfc/rfc6266#section-4]. // // [https://www.rfc-editor.org/rfc/rfc6266#section-4]: https://www.rfc-editor.org/rfc/rfc6266#section-4 @@ -724,6 +772,13 @@ type PutObjectOutput struct { // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. This header is present if + // the object was uploaded with the MD5 checksum algorithm. For more information, + // see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. When you use the API // operation on an object that was uploaded using multipart uploads, this value may @@ -746,6 +801,13 @@ type PutObjectOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. This header is present + // if the object was uploaded with the SHA512 checksum algorithm. For more + // information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // This header specifies the checksum type of the object, which determines how // part-level checksums are combined to create an object-level checksum for // multipart objects. For PutObject uploads, the checksum type is always @@ -756,6 +818,27 @@ type PutObjectOutput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType types.ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. This header is + // present if the object was uploaded with the XXHASH128 checksum algorithm. For + // more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. This header is + // present if the object was uploaded with the XXHASH3 checksum algorithm. For + // more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. This header is + // present if the object was uploaded with the XXHASH64 checksum algorithm. For + // more information, see [Checking object integrity in the Amazon S3 User Guide]. + // + // [Checking object integrity in the Amazon S3 User Guide]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag for the uploaded object. // // General purpose buckets - To ensure that data is not corrupted traversing the diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_UploadPart.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_UploadPart.go index dd7834e87c3..2c31671c480 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_UploadPart.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_UploadPart.go @@ -299,6 +299,14 @@ type UploadPartInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit MD5 digest of the part. For more information, see [Checking object integrity] in + // the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // This header can be used as a data integrity check to verify that the data // received is the same data that was originally sent. This header specifies the // Base64 encoded, 160-bit SHA1 digest of the object. For more information, see [Checking object integrity] @@ -315,6 +323,38 @@ type UploadPartInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 512-bit SHA512 digest of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit XXHASH128 checksum of the part. For more information, + // see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH3 checksum of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH64 checksum of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Size of the body in bytes. This parameter is useful when the size of the body // cannot be determined automatically. ContentLength *int64 @@ -380,58 +420,76 @@ type UploadPartOutput struct { // encryption with Key Management Service (KMS) keys (SSE-KMS). BucketKeyEnabled *bool - // The Base64 encoded, 32-bit CRC32 checksum of the object. This checksum is only - // present if the checksum was uploaded with the object. When you use an API - // operation on an object that was uploaded using multipart uploads, this value may - // not be a direct checksum value of the full object. Instead, it's a calculation - // based on the checksum values of each individual part. For more information about - // how checksums are calculated with multipart uploads, see [Checking object integrity]in the Amazon S3 User - // Guide. + // The Base64 encoded, 32-bit CRC32 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. // - // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC32 *string - // The Base64 encoded, 32-bit CRC32C checksum of the object. This checksum is only - // present if the checksum was uploaded with the object. When you use an API - // operation on an object that was uploaded using multipart uploads, this value may - // not be a direct checksum value of the full object. Instead, it's a calculation - // based on the checksum values of each individual part. For more information about - // how checksums are calculated with multipart uploads, see [Checking object integrity]in the Amazon S3 User - // Guide. + // The Base64 encoded, 32-bit CRC32C checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. // - // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC32C *string - // This header can be used as a data integrity check to verify that the data - // received is the same data that was originally sent. This header specifies the - // Base64 encoded, 64-bit CRC64NVME checksum of the part. For more information, - // see [Checking object integrity]in the Amazon S3 User Guide. + // The Base64 encoded, 64-bit CRC64NVME checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string - // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only - // present if the checksum was uploaded with the object. When you use the API - // operation on an object that was uploaded using multipart uploads, this value may - // not be a direct checksum value of the full object. Instead, it's a calculation - // based on the checksum values of each individual part. For more information about - // how checksums are calculated with multipart uploads, see [Checking object integrity]in the Amazon S3 User - // Guide. + // The Base64 encoded, 128-bit MD5 checksum of the part. This will only be present + // if the checksum was provided in the request. For more information, see [Checking object integrity]in the + // Amazon S3 User Guide. // - // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + + // The Base64 encoded, 160-bit SHA1 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA1 *string - // The Base64 encoded, 256-bit SHA256 digest of the object. This checksum is only - // present if the checksum was uploaded with the object. When you use an API - // operation on an object that was uploaded using multipart uploads, this value may - // not be a direct checksum value of the full object. Instead, it's a calculation - // based on the checksum values of each individual part. For more information about - // how checksums are calculated with multipart uploads, see [Checking object integrity]in the Amazon S3 User - // Guide. + // The Base64 encoded, 256-bit SHA256 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. // - // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // The Base64 encoded, 128-bit XXHASH128 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the part. This will only be + // present if the checksum was provided in the request. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag for the uploaded object. ETag *string diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_WriteGetObjectResponse.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_WriteGetObjectResponse.go index 26bcaf58474..770483d497e 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_WriteGetObjectResponse.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/api_op_WriteGetObjectResponse.go @@ -147,6 +147,14 @@ type WriteGetObjectResponseInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit MD5 digest of the part. For more information, see [Checking object integrity] in + // the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // This header can be used as a data integrity check to verify that the data // received is the same data that was originally sent. This specifies the Base64 // encoded, 160-bit SHA1 digest of the object returned by the Object Lambda @@ -175,6 +183,38 @@ type WriteGetObjectResponseInput struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 512-bit SHA512 digest of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 128-bit XXHASH128 checksum of the part. For more information, + // see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH3 checksum of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // This header can be used as a data integrity check to verify that the data + // received is the same data that was originally sent. This header specifies the + // Base64 encoded, 64-bit XXHASH64 checksum of the part. For more information, see [Checking object integrity] + // in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Specifies presentational information for the object. ContentDisposition *string diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/deserializers.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/deserializers.go index 40d843272aa..82efd40ca42 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/deserializers.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/deserializers.go @@ -354,6 +354,19 @@ func awsRestxml_deserializeOpDocumentCompleteMultipartUploadOutput(v **CompleteM sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -380,6 +393,19 @@ func awsRestxml_deserializeOpDocumentCompleteMultipartUploadOutput(v **CompleteM sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumType", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -393,6 +419,45 @@ func awsRestxml_deserializeOpDocumentCompleteMultipartUploadOutput(v **CompleteM sv.ChecksumType = types.ChecksumType(xtv) } + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + case strings.EqualFold("ETag", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -6516,6 +6581,11 @@ func awsRestxml_deserializeOpHttpBindingsGetObjectOutput(v *GetObjectOutput, res v.ChecksumCRC64NVME = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-md5"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumMD5 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-sha1"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumSHA1 = ptr.String(headerValues[0]) @@ -6526,11 +6596,31 @@ func awsRestxml_deserializeOpHttpBindingsGetObjectOutput(v *GetObjectOutput, res v.ChecksumSHA256 = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-sha512"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumSHA512 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-type"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumType = types.ChecksumType(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-xxhash128"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH128 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash3"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH3 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash64"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH64 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("Content-Disposition"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ContentDisposition = ptr.String(headerValues[0]) @@ -8215,6 +8305,11 @@ func awsRestxml_deserializeOpHttpBindingsHeadObjectOutput(v *HeadObjectOutput, r v.ChecksumCRC64NVME = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-md5"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumMD5 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-sha1"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumSHA1 = ptr.String(headerValues[0]) @@ -8225,11 +8320,31 @@ func awsRestxml_deserializeOpHttpBindingsHeadObjectOutput(v *HeadObjectOutput, r v.ChecksumSHA256 = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-sha512"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumSHA512 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-type"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumType = types.ChecksumType(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-xxhash128"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH128 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash3"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH3 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash64"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH64 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("Content-Disposition"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ContentDisposition = ptr.String(headerValues[0]) @@ -12674,6 +12789,11 @@ func awsRestxml_deserializeOpHttpBindingsPutObjectOutput(v *PutObjectOutput, res v.ChecksumCRC64NVME = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-md5"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumMD5 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-sha1"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumSHA1 = ptr.String(headerValues[0]) @@ -12684,11 +12804,31 @@ func awsRestxml_deserializeOpHttpBindingsPutObjectOutput(v *PutObjectOutput, res v.ChecksumSHA256 = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-sha512"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumSHA512 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-type"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumType = types.ChecksumType(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-xxhash128"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH128 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash3"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH3 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash64"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH64 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("ETag"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ETag = ptr.String(headerValues[0]) @@ -13909,6 +14049,11 @@ func awsRestxml_deserializeOpHttpBindingsUploadPartOutput(v *UploadPartOutput, r v.ChecksumCRC64NVME = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-md5"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumMD5 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("x-amz-checksum-sha1"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ChecksumSHA1 = ptr.String(headerValues[0]) @@ -13919,6 +14064,26 @@ func awsRestxml_deserializeOpHttpBindingsUploadPartOutput(v *UploadPartOutput, r v.ChecksumSHA256 = ptr.String(headerValues[0]) } + if headerValues := response.Header.Values("x-amz-checksum-sha512"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumSHA512 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash128"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH128 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash3"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH3 = ptr.String(headerValues[0]) + } + + if headerValues := response.Header.Values("x-amz-checksum-xxhash64"); len(headerValues) != 0 { + headerValues[0] = strings.TrimSpace(headerValues[0]) + v.ChecksumXXHASH64 = ptr.String(headerValues[0]) + } + if headerValues := response.Header.Values("ETag"); len(headerValues) != 0 { headerValues[0] = strings.TrimSpace(headerValues[0]) v.ETag = ptr.String(headerValues[0]) @@ -16416,6 +16581,19 @@ func awsRestxml_deserializeDocumentChecksum(v **types.Checksum, decoder smithyxm sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16442,6 +16620,19 @@ func awsRestxml_deserializeDocumentChecksum(v **types.Checksum, decoder smithyxm sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumType", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16455,6 +16646,45 @@ func awsRestxml_deserializeDocumentChecksum(v **types.Checksum, decoder smithyxm sv.ChecksumType = types.ChecksumType(xtv) } + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + default: // Do nothing and ignore the unexpected tag element err = decoder.Decoder.Skip() @@ -16789,6 +17019,19 @@ func awsRestxml_deserializeDocumentCopyObjectResult(v **types.CopyObjectResult, sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16815,6 +17058,19 @@ func awsRestxml_deserializeDocumentCopyObjectResult(v **types.CopyObjectResult, sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumType", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16828,6 +17084,45 @@ func awsRestxml_deserializeDocumentCopyObjectResult(v **types.CopyObjectResult, sv.ChecksumType = types.ChecksumType(xtv) } + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + case strings.EqualFold("ETag", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16933,6 +17228,19 @@ func awsRestxml_deserializeDocumentCopyPartResult(v **types.CopyPartResult, deco sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -16959,6 +17267,58 @@ func awsRestxml_deserializeDocumentCopyPartResult(v **types.CopyPartResult, deco sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + case strings.EqualFold("ETag", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -22696,6 +23056,19 @@ func awsRestxml_deserializeDocumentObjectPart(v **types.ObjectPart, decoder smit sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -22722,6 +23095,58 @@ func awsRestxml_deserializeDocumentObjectPart(v **types.ObjectPart, decoder smit sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + case strings.EqualFold("PartNumber", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -23289,6 +23714,19 @@ func awsRestxml_deserializeDocumentPart(v **types.Part, decoder smithyxml.NodeDe sv.ChecksumCRC64NVME = ptr.String(xtv) } + case strings.EqualFold("ChecksumMD5", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumMD5 = ptr.String(xtv) + } + case strings.EqualFold("ChecksumSHA1", t.Name.Local): val, err := decoder.Value() if err != nil { @@ -23315,6 +23753,58 @@ func awsRestxml_deserializeDocumentPart(v **types.Part, decoder smithyxml.NodeDe sv.ChecksumSHA256 = ptr.String(xtv) } + case strings.EqualFold("ChecksumSHA512", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumSHA512 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH128", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH128 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH3", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH3 = ptr.String(xtv) + } + + case strings.EqualFold("ChecksumXXHASH64", t.Name.Local): + val, err := decoder.Value() + if err != nil { + return err + } + if val == nil { + break + } + { + xtv := string(val) + sv.ChecksumXXHASH64 = ptr.String(xtv) + } + case strings.EqualFold("ETag", t.Name.Local): val, err := decoder.Value() if err != nil { diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/endpoints.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/endpoints.go index 2e0197ec9aa..e2622a3772b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/endpoints.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/endpoints.go @@ -7754,124 +7754,133 @@ func (r *resolver) ResolveEndpoint( _accessPointName := *exprVal _ = _accessPointName if _outpostType == "accesspoint" { - if exprVal := params.Endpoint; exprVal != nil { - _Endpoint := *exprVal - _ = _Endpoint - if exprVal := rulesfn.ParseURL(_Endpoint); exprVal != nil { - _url := *exprVal - _ = _url - uriString := func() string { - var out strings.Builder - out.WriteString("https://") - out.WriteString(_accessPointName) - out.WriteString("-") - out.WriteString(_bucketArn.AccountId) - out.WriteString(".") - out.WriteString(_outpostId) - out.WriteString(".") - out.WriteString(_url.Authority) - return out.String() - }() + if rulesfn.IsValidHostLabel(_accessPointName, false) { + if exprVal := params.Endpoint; exprVal != nil { + _Endpoint := *exprVal + _ = _Endpoint + if exprVal := rulesfn.ParseURL(_Endpoint); exprVal != nil { + _url := *exprVal + _ = _url + uriString := func() string { + var out strings.Builder + out.WriteString("https://") + out.WriteString(_accessPointName) + out.WriteString("-") + out.WriteString(_bucketArn.AccountId) + out.WriteString(".") + out.WriteString(_outpostId) + out.WriteString(".") + out.WriteString(_url.Authority) + return out.String() + }() - uri, err := url.Parse(uriString) - if err != nil { - return endpoint, fmt.Errorf("Failed to parse uri: %s", uriString) - } + uri, err := url.Parse(uriString) + if err != nil { + return endpoint, fmt.Errorf("Failed to parse uri: %s", uriString) + } - return smithyendpoints.Endpoint{ - URI: *uri, - Headers: http.Header{}, - Properties: func() smithy.Properties { - var out smithy.Properties - smithyauth.SetAuthOptions(&out, []*smithyauth.Option{ - { - SchemeID: "aws.auth#sigv4a", - SignerProperties: func() smithy.Properties { - var sp smithy.Properties - smithyhttp.SetDisableDoubleEncoding(&sp, true) + return smithyendpoints.Endpoint{ + URI: *uri, + Headers: http.Header{}, + Properties: func() smithy.Properties { + var out smithy.Properties + smithyauth.SetAuthOptions(&out, []*smithyauth.Option{ + { + SchemeID: "aws.auth#sigv4a", + SignerProperties: func() smithy.Properties { + var sp smithy.Properties + smithyhttp.SetDisableDoubleEncoding(&sp, true) - smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4ASigningRegions(&sp, []string{"*"}) - return sp - }(), - }, - { - SchemeID: "aws.auth#sigv4", - SignerProperties: func() smithy.Properties { - var sp smithy.Properties - smithyhttp.SetDisableDoubleEncoding(&sp, true) + smithyhttp.SetSigV4ASigningRegions(&sp, []string{"*"}) + return sp + }(), + }, + { + SchemeID: "aws.auth#sigv4", + SignerProperties: func() smithy.Properties { + var sp smithy.Properties + smithyhttp.SetDisableDoubleEncoding(&sp, true) - smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4SigningRegion(&sp, _bucketArn.Region) - return sp - }(), - }, - }) - return out - }(), - }, nil + smithyhttp.SetSigV4SigningRegion(&sp, _bucketArn.Region) + return sp + }(), + }, + }) + return out + }(), + }, nil + } } + uriString := func() string { + var out strings.Builder + out.WriteString("https://") + out.WriteString(_accessPointName) + out.WriteString("-") + out.WriteString(_bucketArn.AccountId) + out.WriteString(".") + out.WriteString(_outpostId) + out.WriteString(".s3-outposts.") + out.WriteString(_bucketArn.Region) + out.WriteString(".") + out.WriteString(_bucketPartition.DnsSuffix) + return out.String() + }() + + uri, err := url.Parse(uriString) + if err != nil { + return endpoint, fmt.Errorf("Failed to parse uri: %s", uriString) + } + + return smithyendpoints.Endpoint{ + URI: *uri, + Headers: http.Header{}, + Properties: func() smithy.Properties { + var out smithy.Properties + smithyauth.SetAuthOptions(&out, []*smithyauth.Option{ + { + SchemeID: "aws.auth#sigv4a", + SignerProperties: func() smithy.Properties { + var sp smithy.Properties + smithyhttp.SetDisableDoubleEncoding(&sp, true) + + smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") + + smithyhttp.SetSigV4ASigningRegions(&sp, []string{"*"}) + return sp + }(), + }, + { + SchemeID: "aws.auth#sigv4", + SignerProperties: func() smithy.Properties { + var sp smithy.Properties + smithyhttp.SetDisableDoubleEncoding(&sp, true) + + smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") + smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") + + smithyhttp.SetSigV4SigningRegion(&sp, _bucketArn.Region) + return sp + }(), + }, + }) + return out + }(), + }, nil } - uriString := func() string { + return endpoint, fmt.Errorf("endpoint rule error, %s", func() string { var out strings.Builder - out.WriteString("https://") + out.WriteString("Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `") out.WriteString(_accessPointName) - out.WriteString("-") - out.WriteString(_bucketArn.AccountId) - out.WriteString(".") - out.WriteString(_outpostId) - out.WriteString(".s3-outposts.") - out.WriteString(_bucketArn.Region) - out.WriteString(".") - out.WriteString(_bucketPartition.DnsSuffix) + out.WriteString("`") return out.String() - }() - - uri, err := url.Parse(uriString) - if err != nil { - return endpoint, fmt.Errorf("Failed to parse uri: %s", uriString) - } - - return smithyendpoints.Endpoint{ - URI: *uri, - Headers: http.Header{}, - Properties: func() smithy.Properties { - var out smithy.Properties - smithyauth.SetAuthOptions(&out, []*smithyauth.Option{ - { - SchemeID: "aws.auth#sigv4a", - SignerProperties: func() smithy.Properties { - var sp smithy.Properties - smithyhttp.SetDisableDoubleEncoding(&sp, true) - - smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") - - smithyhttp.SetSigV4ASigningRegions(&sp, []string{"*"}) - return sp - }(), - }, - { - SchemeID: "aws.auth#sigv4", - SignerProperties: func() smithy.Properties { - var sp smithy.Properties - smithyhttp.SetDisableDoubleEncoding(&sp, true) - - smithyhttp.SetSigV4SigningName(&sp, "s3-outposts") - smithyhttp.SetSigV4ASigningName(&sp, "s3-outposts") - - smithyhttp.SetSigV4SigningRegion(&sp, _bucketArn.Region) - return sp - }(), - }, - }) - return out - }(), - }, nil + }()) } return endpoint, fmt.Errorf("endpoint rule error, %s", func() string { var out strings.Builder @@ -7957,8 +7966,8 @@ func (r *resolver) ResolveEndpoint( } if _ForcePathStyle == true { if exprVal := awsrulesfn.ParseARN(_Bucket); exprVal != nil { - _var_487 := *exprVal - _ = _var_487 + _var_488 := *exprVal + _ = _var_488 return endpoint, fmt.Errorf("endpoint rule error, %s", "Path-style addressing cannot be used with ARN buckets") } } diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go index 1bb87d174fb..c1666ef0688 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/go_module_metadata.go @@ -3,4 +3,4 @@ package s3 // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.99.1" +const goModuleVersion = "1.101.0" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/serializers.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/serializers.go index 4a350051806..e08c677277d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/serializers.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/serializers.go @@ -207,6 +207,11 @@ func awsRestxml_serializeOpHttpBindingsCompleteMultipartUploadInput(v *CompleteM encoder.SetHeader(locationName).String(*v.ChecksumCRC64NVME) } + if v.ChecksumMD5 != nil { + locationName := "X-Amz-Checksum-Md5" + encoder.SetHeader(locationName).String(*v.ChecksumMD5) + } + if v.ChecksumSHA1 != nil { locationName := "X-Amz-Checksum-Sha1" encoder.SetHeader(locationName).String(*v.ChecksumSHA1) @@ -217,11 +222,31 @@ func awsRestxml_serializeOpHttpBindingsCompleteMultipartUploadInput(v *CompleteM encoder.SetHeader(locationName).String(*v.ChecksumSHA256) } + if v.ChecksumSHA512 != nil { + locationName := "X-Amz-Checksum-Sha512" + encoder.SetHeader(locationName).String(*v.ChecksumSHA512) + } + if len(v.ChecksumType) > 0 { locationName := "X-Amz-Checksum-Type" encoder.SetHeader(locationName).String(string(v.ChecksumType)) } + if v.ChecksumXXHASH128 != nil { + locationName := "X-Amz-Checksum-Xxhash128" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH128) + } + + if v.ChecksumXXHASH3 != nil { + locationName := "X-Amz-Checksum-Xxhash3" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH3) + } + + if v.ChecksumXXHASH64 != nil { + locationName := "X-Amz-Checksum-Xxhash64" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH64) + } + if v.ExpectedBucketOwner != nil { locationName := "X-Amz-Expected-Bucket-Owner" encoder.SetHeader(locationName).String(*v.ExpectedBucketOwner) @@ -8209,6 +8234,11 @@ func awsRestxml_serializeOpHttpBindingsPutObjectInput(v *PutObjectInput, encoder encoder.SetHeader(locationName).String(*v.ChecksumCRC64NVME) } + if v.ChecksumMD5 != nil { + locationName := "X-Amz-Checksum-Md5" + encoder.SetHeader(locationName).String(*v.ChecksumMD5) + } + if v.ChecksumSHA1 != nil { locationName := "X-Amz-Checksum-Sha1" encoder.SetHeader(locationName).String(*v.ChecksumSHA1) @@ -8219,6 +8249,26 @@ func awsRestxml_serializeOpHttpBindingsPutObjectInput(v *PutObjectInput, encoder encoder.SetHeader(locationName).String(*v.ChecksumSHA256) } + if v.ChecksumSHA512 != nil { + locationName := "X-Amz-Checksum-Sha512" + encoder.SetHeader(locationName).String(*v.ChecksumSHA512) + } + + if v.ChecksumXXHASH128 != nil { + locationName := "X-Amz-Checksum-Xxhash128" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH128) + } + + if v.ChecksumXXHASH3 != nil { + locationName := "X-Amz-Checksum-Xxhash3" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH3) + } + + if v.ChecksumXXHASH64 != nil { + locationName := "X-Amz-Checksum-Xxhash64" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH64) + } + if v.ContentDisposition != nil { locationName := "Content-Disposition" encoder.SetHeader(locationName).String(*v.ContentDisposition) @@ -9936,6 +9986,11 @@ func awsRestxml_serializeOpHttpBindingsUploadPartInput(v *UploadPartInput, encod encoder.SetHeader(locationName).String(*v.ChecksumCRC64NVME) } + if v.ChecksumMD5 != nil { + locationName := "X-Amz-Checksum-Md5" + encoder.SetHeader(locationName).String(*v.ChecksumMD5) + } + if v.ChecksumSHA1 != nil { locationName := "X-Amz-Checksum-Sha1" encoder.SetHeader(locationName).String(*v.ChecksumSHA1) @@ -9946,6 +10001,26 @@ func awsRestxml_serializeOpHttpBindingsUploadPartInput(v *UploadPartInput, encod encoder.SetHeader(locationName).String(*v.ChecksumSHA256) } + if v.ChecksumSHA512 != nil { + locationName := "X-Amz-Checksum-Sha512" + encoder.SetHeader(locationName).String(*v.ChecksumSHA512) + } + + if v.ChecksumXXHASH128 != nil { + locationName := "X-Amz-Checksum-Xxhash128" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH128) + } + + if v.ChecksumXXHASH3 != nil { + locationName := "X-Amz-Checksum-Xxhash3" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH3) + } + + if v.ChecksumXXHASH64 != nil { + locationName := "X-Amz-Checksum-Xxhash64" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH64) + } + if v.ContentLength != nil { locationName := "Content-Length" encoder.SetHeader(locationName).Long(*v.ContentLength) @@ -10256,6 +10331,11 @@ func awsRestxml_serializeOpHttpBindingsWriteGetObjectResponseInput(v *WriteGetOb encoder.SetHeader(locationName).String(*v.ChecksumCRC64NVME) } + if v.ChecksumMD5 != nil { + locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Md5" + encoder.SetHeader(locationName).String(*v.ChecksumMD5) + } + if v.ChecksumSHA1 != nil { locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Sha1" encoder.SetHeader(locationName).String(*v.ChecksumSHA1) @@ -10266,6 +10346,26 @@ func awsRestxml_serializeOpHttpBindingsWriteGetObjectResponseInput(v *WriteGetOb encoder.SetHeader(locationName).String(*v.ChecksumSHA256) } + if v.ChecksumSHA512 != nil { + locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Sha512" + encoder.SetHeader(locationName).String(*v.ChecksumSHA512) + } + + if v.ChecksumXXHASH128 != nil { + locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Xxhash128" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH128) + } + + if v.ChecksumXXHASH3 != nil { + locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Xxhash3" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH3) + } + + if v.ChecksumXXHASH64 != nil { + locationName := "X-Amz-Fwd-Header-X-Amz-Checksum-Xxhash64" + encoder.SetHeader(locationName).String(*v.ChecksumXXHASH64) + } + if v.ContentDisposition != nil { locationName := "X-Amz-Fwd-Header-Content-Disposition" encoder.SetHeader(locationName).String(*v.ContentDisposition) @@ -10884,6 +10984,17 @@ func awsRestxml_serializeDocumentCompletedPart(v *types.CompletedPart, value smi el := value.MemberElement(root) el.String(*v.ChecksumCRC64NVME) } + if v.ChecksumMD5 != nil { + rootAttr := []smithyxml.Attr{} + root := smithyxml.StartElement{ + Name: smithyxml.Name{ + Local: "ChecksumMD5", + }, + Attr: rootAttr, + } + el := value.MemberElement(root) + el.String(*v.ChecksumMD5) + } if v.ChecksumSHA1 != nil { rootAttr := []smithyxml.Attr{} root := smithyxml.StartElement{ @@ -10906,6 +11017,50 @@ func awsRestxml_serializeDocumentCompletedPart(v *types.CompletedPart, value smi el := value.MemberElement(root) el.String(*v.ChecksumSHA256) } + if v.ChecksumSHA512 != nil { + rootAttr := []smithyxml.Attr{} + root := smithyxml.StartElement{ + Name: smithyxml.Name{ + Local: "ChecksumSHA512", + }, + Attr: rootAttr, + } + el := value.MemberElement(root) + el.String(*v.ChecksumSHA512) + } + if v.ChecksumXXHASH128 != nil { + rootAttr := []smithyxml.Attr{} + root := smithyxml.StartElement{ + Name: smithyxml.Name{ + Local: "ChecksumXXHASH128", + }, + Attr: rootAttr, + } + el := value.MemberElement(root) + el.String(*v.ChecksumXXHASH128) + } + if v.ChecksumXXHASH3 != nil { + rootAttr := []smithyxml.Attr{} + root := smithyxml.StartElement{ + Name: smithyxml.Name{ + Local: "ChecksumXXHASH3", + }, + Attr: rootAttr, + } + el := value.MemberElement(root) + el.String(*v.ChecksumXXHASH3) + } + if v.ChecksumXXHASH64 != nil { + rootAttr := []smithyxml.Attr{} + root := smithyxml.StartElement{ + Name: smithyxml.Name{ + Local: "ChecksumXXHASH64", + }, + Attr: rootAttr, + } + el := value.MemberElement(root) + el.String(*v.ChecksumXXHASH64) + } if v.ETag != nil { rootAttr := []smithyxml.Attr{} root := smithyxml.StartElement{ diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/enums.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/enums.go index d0441734468..a84ece87b2b 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/enums.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/enums.go @@ -275,6 +275,11 @@ const ( ChecksumAlgorithmSha1 ChecksumAlgorithm = "SHA1" ChecksumAlgorithmSha256 ChecksumAlgorithm = "SHA256" ChecksumAlgorithmCrc64nvme ChecksumAlgorithm = "CRC64NVME" + ChecksumAlgorithmSha512 ChecksumAlgorithm = "SHA512" + ChecksumAlgorithmMd5 ChecksumAlgorithm = "MD5" + ChecksumAlgorithmXxhash64 ChecksumAlgorithm = "XXHASH64" + ChecksumAlgorithmXxhash3 ChecksumAlgorithm = "XXHASH3" + ChecksumAlgorithmXxhash128 ChecksumAlgorithm = "XXHASH128" ) // Values returns all known values for ChecksumAlgorithm. Note that this can be @@ -288,6 +293,11 @@ func (ChecksumAlgorithm) Values() []ChecksumAlgorithm { "SHA1", "SHA256", "CRC64NVME", + "SHA512", + "MD5", + "XXHASH64", + "XXHASH3", + "XXHASH128", } } diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/types.go b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/types.go index 3645fd84e6d..f047e26ef7e 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/types.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/s3/types/types.go @@ -338,6 +338,13 @@ type Checksum struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. This checksum is present + // if the object was uploaded with the MD5 checksum algorithm. For more + // information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. When you use the API // operation on an object that was uploaded using multipart uploads, this value may @@ -360,12 +367,40 @@ type Checksum struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html#large-object-checksums ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. This checksum is + // present if the object was uploaded with the SHA512 checksum algorithm. For more + // information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // The checksum type that is used to calculate the object’s checksum value. For // more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. This checksum is + // present if the object was uploaded with the XXHASH128 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. This checksum is + // present if the object was uploaded with the XXHASH3 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. This checksum is + // present if the object was uploaded with the XXHASH64 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + noSmithyDocumentSerde } @@ -413,12 +448,18 @@ type CompletedPart struct { // The Base64 encoded, 64-bit CRC64NVME checksum of the part. This checksum is // present if the multipart upload request was created with the CRC64NVME checksum - // algorithm to the uploaded object). For more information, see [Checking object integrity]in the Amazon S3 - // User Guide. + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the part. This checksum is present if + // the multipart upload request was created with the MD5 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 checksum of the part. This checksum is present // if the multipart upload request was created with the SHA1 checksum algorithm. // For more information, see [Checking object integrity]in the Amazon S3 User Guide. @@ -433,6 +474,34 @@ type CompletedPart struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA512 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // The Base64 encoded, 128-bit XXHASH128 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH128 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH3 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH64 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag returned when the part was uploaded. ETag *string @@ -513,6 +582,13 @@ type CopyObjectResult struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the object. This checksum is only + // present if the object was uploaded with the MD5 checksum algorithm. For more + // information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 digest of the object. This checksum is only // present if the checksum was uploaded with the object. For more information, see [Checking object integrity] // in the Amazon S3 User Guide. @@ -527,12 +603,40 @@ type CopyObjectResult struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the object. This checksum is only + // present if the object was uploaded with the SHA512 checksum algorithm. For more + // information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + // The checksum type that is used to calculate the object’s checksum value. For // more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumType ChecksumType + // The Base64 encoded, 128-bit XXHASH128 checksum of the object. This checksum is + // only present if the object was uploaded with the XXHASH128 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the object. This checksum is + // only present if the object was uploaded with the XXHASH3 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the object. This checksum is + // only present if the object was uploaded with the XXHASH64 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Returns the ETag of the new object. The ETag reflects only changes to the // contents of an object, not its metadata. ETag *string @@ -546,46 +650,76 @@ type CopyObjectResult struct { // Container for all response elements. type CopyPartResult struct { - // This header can be used as a data integrity check to verify that the data - // received is the same data that was originally sent. This header specifies the - // Base64 encoded, 32-bit CRC32 checksum of the part. For more information, see [Checking object integrity] - // in the Amazon S3 User Guide. + // The Base64 encoded, 32-bit CRC32 checksum of the part. This checksum is present + // if the multipart upload request was created with the CRC32 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC32 *string - // This header can be used as a data integrity check to verify that the data - // received is the same data that was originally sent. This header specifies the - // Base64 encoded, 32-bit CRC32C checksum of the part. For more information, see [Checking object integrity] - // in the Amazon S3 User Guide. + // The Base64 encoded, 32-bit CRC32C checksum of the part. This checksum is + // present if the multipart upload request was created with the CRC32C checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC32C *string // The Base64 encoded, 64-bit CRC64NVME checksum of the part. This checksum is // present if the multipart upload request was created with the CRC64NVME checksum - // algorithm to the uploaded object). For more information, see [Checking object integrity]in the Amazon S3 - // User Guide. + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string - // This header can be used as a data integrity check to verify that the data - // received is the same data that was originally sent. This header specifies the - // Base64 encoded, 160-bit SHA1 checksum of the part. For more information, see [Checking object integrity] - // in the Amazon S3 User Guide. + // The Base64 encoded, 128-bit MD5 digest of the part. This checksum is present if + // the multipart upload request was created with the MD5 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + + // The Base64 encoded, 160-bit SHA1 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA1 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA1 *string - // This header can be used as a data integrity check to verify that the data - // received is the same data that was originally sent. This header specifies the - // Base64 encoded, 256-bit SHA256 checksum of the part. For more information, see [Checking object integrity] - // in the Amazon S3 User Guide. + // The Base64 encoded, 256-bit SHA256 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA256 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. // // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA512 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // The Base64 encoded, 128-bit XXHASH128 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH128 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH3 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH64 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag of the object. ETag *string @@ -2360,6 +2494,11 @@ type InventoryConfiguration struct { Filter *InventoryFilter // Contains the optional fields that are included in the inventory results. + // + // The following optional fields are supported for directory buckets Size | + // LastModifiedDate | StorageClass | ETag | IsMultipartUploaded | EncryptionStatus + // | BucketKeyStatus | ChecksumAlgorithm | LifecycleExpirationDate. Throws + // MalformedXML error if unsupported optional field is provided. OptionalFields []InventoryOptionalField noSmithyDocumentSerde @@ -3451,6 +3590,13 @@ type ObjectPart struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the part. This checksum is present if + // the multipart upload request was created with the MD5 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 checksum of the part. This checksum is present // if the multipart upload request was created with the SHA1 checksum algorithm. // For more information, see [Checking object integrity]in the Amazon S3 User Guide. @@ -3465,6 +3611,34 @@ type ObjectPart struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA512 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // The Base64 encoded, 128-bit XXHASH128 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH128 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH3 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH64 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // The part number identifying the part. This value is a positive integer between // 1 and 10,000. PartNumber *int32 @@ -3633,6 +3807,13 @@ type Part struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumCRC64NVME *string + // The Base64 encoded, 128-bit MD5 digest of the part. This checksum is present if + // the multipart upload request was created with the MD5 checksum algorithm. For + // more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumMD5 *string + // The Base64 encoded, 160-bit SHA1 checksum of the part. This checksum is present // if the object was uploaded with the SHA1 checksum algorithm. For more // information, see [Checking object integrity]in the Amazon S3 User Guide. @@ -3647,6 +3828,34 @@ type Part struct { // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html ChecksumSHA256 *string + // The Base64 encoded, 512-bit SHA512 digest of the part. This checksum is present + // if the multipart upload request was created with the SHA512 checksum algorithm. + // For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumSHA512 *string + + // The Base64 encoded, 128-bit XXHASH128 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH128 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH128 *string + + // The Base64 encoded, 64-bit XXHASH3 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH3 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH3 *string + + // The Base64 encoded, 64-bit XXHASH64 checksum of the part. This checksum is + // present if the multipart upload request was created with the XXHASH64 checksum + // algorithm. For more information, see [Checking object integrity]in the Amazon S3 User Guide. + // + // [Checking object integrity]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html + ChecksumXXHASH64 *string + // Entity tag returned when the part was uploaded. ETag *string diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md index 5e35f50a649..253e0359678 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.0.11 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.0.10 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go index a1576278656..eba7ad77743 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/signin/go_module_metadata.go @@ -3,4 +3,4 @@ package signin // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.0.10" +const goModuleVersion = "1.0.11" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md index 87e04ee90d3..26c80a2c233 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.30.17 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.30.16 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go index fd4de83a8a7..9d12dd55bc3 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.go @@ -3,4 +3,4 @@ package sso // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.30.16" +const goModuleVersion = "1.30.17" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md index a96458855a5..e645209405d 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.35.21 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.35.20 (2026-04-17) * **Dependency Update**: Bump smithy-go to 1.25.0 to support endpointBdd trait diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go index c107608ce89..af00268dfc6 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.go @@ -3,4 +3,4 @@ package ssooidc // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.35.20" +const goModuleVersion = "1.35.21" diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md index fb91ca64de0..199f7a79ce8 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.42.1 (2026-04-29) + +* **Dependency Update**: Update to smithy-go v1.25.1. +* **Dependency Update**: Updated to the latest SDK module versions + # v1.42.0 (2026-04-17) * **Feature**: The STS client now supports configuring SigV4a through the auth scheme preference setting. SigV4a uses asymmetric cryptography, enabling customers using long-term IAM credentials to continue making STS API calls even when a region is isolated from the partition leader. diff --git a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go index 684a25787a0..bdd6a15d8f0 100644 --- a/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go +++ b/vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.go @@ -3,4 +3,4 @@ package sts // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.42.0" +const goModuleVersion = "1.42.1" diff --git a/vendor/github.com/aws/smithy-go/CHANGELOG.md b/vendor/github.com/aws/smithy-go/CHANGELOG.md index 2db174e021a..b9cd114ed16 100644 --- a/vendor/github.com/aws/smithy-go/CHANGELOG.md +++ b/vendor/github.com/aws/smithy-go/CHANGELOG.md @@ -1,3 +1,12 @@ +# Release (2026-04-23) + +## General Highlights +* **Dependency Update**: Updated to the latest SDK module versions + +## Module Highlights +* `github.com/aws/smithy-go`: v1.25.1 + * **Bug Fix**: Fixed a memory leak in the LRU cache implementation used by some AWS services. + # Release (2026-04-15) ## General Highlights diff --git a/vendor/github.com/aws/smithy-go/container/private/cache/lru/lru.go b/vendor/github.com/aws/smithy-go/container/private/cache/lru/lru.go index 02ecb0a3277..6db123a75a3 100644 --- a/vendor/github.com/aws/smithy-go/container/private/cache/lru/lru.go +++ b/vendor/github.com/aws/smithy-go/container/private/cache/lru/lru.go @@ -45,6 +45,12 @@ func (l *lru) Get(k interface{}) (interface{}, bool) { } func (l *lru) Put(k interface{}, v interface{}) { + if e, ok := l.entries[k]; ok { + e.Value.(*element).value = v + l.mru.MoveToFront(e) + return + } + if len(l.entries) == l.cap { l.evict() } diff --git a/vendor/github.com/aws/smithy-go/go_module_metadata.go b/vendor/github.com/aws/smithy-go/go_module_metadata.go index 35938d40721..a1e928754a1 100644 --- a/vendor/github.com/aws/smithy-go/go_module_metadata.go +++ b/vendor/github.com/aws/smithy-go/go_module_metadata.go @@ -3,4 +3,4 @@ package smithy // goModuleVersion is the tagged release for this module -const goModuleVersion = "1.25.0" +const goModuleVersion = "1.25.1" diff --git a/vendor/github.com/dgryski/go-rendezvous/rdv.go b/vendor/github.com/dgryski/go-rendezvous/rdv.go deleted file mode 100644 index 7a6f8203c67..00000000000 --- a/vendor/github.com/dgryski/go-rendezvous/rdv.go +++ /dev/null @@ -1,79 +0,0 @@ -package rendezvous - -type Rendezvous struct { - nodes map[string]int - nstr []string - nhash []uint64 - hash Hasher -} - -type Hasher func(s string) uint64 - -func New(nodes []string, hash Hasher) *Rendezvous { - r := &Rendezvous{ - nodes: make(map[string]int, len(nodes)), - nstr: make([]string, len(nodes)), - nhash: make([]uint64, len(nodes)), - hash: hash, - } - - for i, n := range nodes { - r.nodes[n] = i - r.nstr[i] = n - r.nhash[i] = hash(n) - } - - return r -} - -func (r *Rendezvous) Lookup(k string) string { - // short-circuit if we're empty - if len(r.nodes) == 0 { - return "" - } - - khash := r.hash(k) - - var midx int - var mhash = xorshiftMult64(khash ^ r.nhash[0]) - - for i, nhash := range r.nhash[1:] { - if h := xorshiftMult64(khash ^ nhash); h > mhash { - midx = i + 1 - mhash = h - } - } - - return r.nstr[midx] -} - -func (r *Rendezvous) Add(node string) { - r.nodes[node] = len(r.nstr) - r.nstr = append(r.nstr, node) - r.nhash = append(r.nhash, r.hash(node)) -} - -func (r *Rendezvous) Remove(node string) { - // find index of node to remove - nidx := r.nodes[node] - - // remove from the slices - l := len(r.nstr) - r.nstr[nidx] = r.nstr[l] - r.nstr = r.nstr[:l] - - r.nhash[nidx] = r.nhash[l] - r.nhash = r.nhash[:l] - - // update the map - delete(r.nodes, node) - moved := r.nstr[nidx] - r.nodes[moved] = nidx -} - -func xorshiftMult64(x uint64) uint64 { - x ^= x >> 12 // a - x ^= x << 25 // b - x ^= x >> 27 // c - return x * 2685821657736338717 -} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/push.go b/vendor/github.com/prometheus/client_golang/prometheus/push/push.go new file mode 100644 index 00000000000..e524aa1303e --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/push.go @@ -0,0 +1,356 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package push provides functions to push metrics to a Pushgateway. It uses a +// builder approach. Create a Pusher with New and then add the various options +// by using its methods, finally calling Add or Push, like this: +// +// // Easy case: +// push.New("http://example.org/metrics", "my_job").Gatherer(myRegistry).Push() +// +// // Complex case: +// push.New("http://example.org/metrics", "my_job"). +// Collector(myCollector1). +// Collector(myCollector2). +// Grouping("zone", "xy"). +// Client(&myHTTPClient). +// BasicAuth("top", "secret"). +// Add() +// +// See the examples section for more detailed examples. +// +// See the documentation of the Pushgateway to understand the meaning of +// the grouping key and the differences between Push and Add: +// https://github.com/prometheus/pushgateway +package push + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + contentTypeHeader = "Content-Type" + // base64Suffix is appended to a label name in the request URL path to + // mark the following label value as base64 encoded. + base64Suffix = "@base64" +) + +var errJobEmpty = errors.New("job name is empty") + +// HTTPDoer is an interface for the one method of http.Client that is used by Pusher +type HTTPDoer interface { + Do(*http.Request) (*http.Response, error) +} + +// Pusher manages a push to the Pushgateway. Use New to create one, configure it +// with its methods, and finally use the Add or Push method to push. +type Pusher struct { + error error + + url, job string + grouping map[string]string + + gatherers prometheus.Gatherers + registerer prometheus.Registerer + + client HTTPDoer + header http.Header + useBasicAuth bool + username, password string + + expfmt expfmt.Format +} + +// New creates a new Pusher to push to the provided URL with the provided job +// name (which must not be empty). You can use just host:port or ip:port as url, +// in which case “http://” is added automatically. Alternatively, include the +// schema in the URL. However, do not include the “/metrics/jobs/…” part. +func New(url, job string) *Pusher { + var ( + reg = prometheus.NewRegistry() + err error + ) + if job == "" { + err = errJobEmpty + } + if !strings.Contains(url, "://") { + url = "http://" + url + } + url = strings.TrimSuffix(url, "/") + + return &Pusher{ + error: err, + url: url, + job: job, + grouping: map[string]string{}, + gatherers: prometheus.Gatherers{reg}, + registerer: reg, + client: &http.Client{}, + expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim), + } +} + +// Push collects/gathers all metrics from all Collectors and Gatherers added to +// this Pusher. Then, it pushes them to the Pushgateway configured while +// creating this Pusher, using the configured job name and any added grouping +// labels as grouping key. All previously pushed metrics with the same job and +// other grouping labels will be replaced with the metrics pushed by this +// call. (It uses HTTP method “PUT” to push to the Pushgateway.) +// +// Push returns the first error encountered by any method call (including this +// one) in the lifetime of the Pusher. +func (p *Pusher) Push() error { + return p.push(context.Background(), http.MethodPut) +} + +// PushContext is like Push but includes a context. +// +// If the context expires before HTTP request is complete, an error is returned. +func (p *Pusher) PushContext(ctx context.Context) error { + return p.push(ctx, http.MethodPut) +} + +// Add works like push, but only previously pushed metrics with the same name +// (and the same job and other grouping labels) will be replaced. (It uses HTTP +// method “POST” to push to the Pushgateway.) +func (p *Pusher) Add() error { + return p.push(context.Background(), http.MethodPost) +} + +// AddContext is like Add but includes a context. +// +// If the context expires before HTTP request is complete, an error is returned. +func (p *Pusher) AddContext(ctx context.Context) error { + return p.push(ctx, http.MethodPost) +} + +// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered +// to push them to the Pushgateway. The gathered metrics must not contain a job +// label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { + p.gatherers = append(p.gatherers, g) + return p +} + +// Collector adds a Collector to the Pusher, from which metrics will be +// collected to push them to the Pushgateway. The collected metrics must not +// contain a job label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Collector(c prometheus.Collector) *Pusher { + if p.error == nil { + p.error = p.registerer.Register(c) + } + return p +} + +// Error returns the error that was encountered. +func (p *Pusher) Error() error { + return p.error +} + +// Grouping adds a label pair to the grouping key of the Pusher, replacing any +// previously added label pair with the same label name. Note that setting any +// labels in the grouping key that are already contained in the metrics to push +// will lead to an error. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Grouping(name, value string) *Pusher { + if p.error == nil { + if !model.LabelName(name).IsValid() { + p.error = fmt.Errorf("grouping label has invalid name: %s", name) + return p + } + p.grouping[name] = value + } + return p +} + +// Client sets a custom HTTP client for the Pusher. For convenience, this method +// returns a pointer to the Pusher itself. +// Pusher only needs one method of the custom HTTP client: Do(*http.Request). +// Thus, rather than requiring a fully fledged http.Client, +// the provided client only needs to implement the HTTPDoer interface. +// Since *http.Client naturally implements that interface, it can still be used normally. +func (p *Pusher) Client(c HTTPDoer) *Pusher { + p.client = c + return p +} + +// Header sets a custom HTTP header for the Pusher's client. For convenience, this method +// returns a pointer to the Pusher itself. +func (p *Pusher) Header(header http.Header) *Pusher { + p.header = header + return p +} + +// BasicAuth configures the Pusher to use HTTP Basic Authentication with the +// provided username and password. For convenience, this method returns a +// pointer to the Pusher itself. +func (p *Pusher) BasicAuth(username, password string) *Pusher { + p.useBasicAuth = true + p.username = username + p.password = password + return p +} + +// Format configures the Pusher to use an encoding format given by the +// provided expfmt.Format. The default format is expfmt.FmtProtoDelim and +// should be used with the standard Prometheus Pushgateway. Custom +// implementations may require different formats. For convenience, this +// method returns a pointer to the Pusher itself. +func (p *Pusher) Format(format expfmt.Format) *Pusher { + p.expfmt = format + return p +} + +// Delete sends a “DELETE” request to the Pushgateway configured while creating +// this Pusher, using the configured job name and any added grouping labels as +// grouping key. Any added Gatherers and Collectors added to this Pusher are +// ignored by this method. +// +// Delete returns the first error encountered by any method call (including this +// one) in the lifetime of the Pusher. +func (p *Pusher) Delete() error { + if p.error != nil { + return p.error + } + req, err := http.NewRequest(http.MethodDelete, p.fullURL(), nil) + if err != nil { + return err + } + if p.header != nil { + req.Header = p.header + } + if p.useBasicAuth { + req.SetBasicAuth(p.username, p.password) + } + resp, err := p.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. + return fmt.Errorf("unexpected status code %d while deleting %s: %s", resp.StatusCode, p.fullURL(), body) + } + return nil +} + +func (p *Pusher) push(ctx context.Context, method string) error { + if p.error != nil { + return p.error + } + mfs, err := p.gatherers.Gather() + if err != nil { + return err + } + buf := &bytes.Buffer{} + enc := expfmt.NewEncoder(buf, p.expfmt) + // Check for pre-existing grouping labels: + for _, mf := range mfs { + for _, m := range mf.GetMetric() { + for _, l := range m.GetLabel() { + if l.GetName() == "job" { + return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) + } + if _, ok := p.grouping[l.GetName()]; ok { + return fmt.Errorf( + "pushed metric %s (%s) already contains grouping label %s", + mf.GetName(), m, l.GetName(), + ) + } + } + } + if err := enc.Encode(mf); err != nil { + return fmt.Errorf( + "failed to encode metric family %s, error is %w", + mf.GetName(), err) + } + } + req, err := http.NewRequestWithContext(ctx, method, p.fullURL(), buf) + if err != nil { + return err + } + if p.header != nil { + req.Header = p.header + } + if p.useBasicAuth { + req.SetBasicAuth(p.username, p.password) + } + req.Header.Set(contentTypeHeader, string(p.expfmt)) + resp, err := p.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + // Depending on version and configuration of the PGW, StatusOK or StatusAccepted may be returned. + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. + return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, p.fullURL(), body) + } + return nil +} + +// fullURL assembles the URL used to push/delete metrics and returns it as a +// string. The job name and any grouping label values containing a '/' will +// trigger a base64 encoding of the affected component and proper suffixing of +// the preceding component. Similarly, an empty grouping label value will be +// encoded as base64 just with a single `=` padding character (to avoid an empty +// path component). If the component does not contain a '/' but other special +// characters, the usual url.QueryEscape is used for compatibility with older +// versions of the Pushgateway and for better readability. +func (p *Pusher) fullURL() string { + urlComponents := []string{} + if encodedJob, base64 := encodeComponent(p.job); base64 { + urlComponents = append(urlComponents, "job"+base64Suffix, encodedJob) + } else { + urlComponents = append(urlComponents, "job", encodedJob) + } + for ln, lv := range p.grouping { + if encodedLV, base64 := encodeComponent(lv); base64 { + urlComponents = append(urlComponents, ln+base64Suffix, encodedLV) + } else { + urlComponents = append(urlComponents, ln, encodedLV) + } + } + return fmt.Sprintf("%s/metrics/%s", p.url, strings.Join(urlComponents, "/")) +} + +// encodeComponent encodes the provided string with base64.RawURLEncoding in +// case it contains '/' and as "=" in case it is empty. If neither is the case, +// it uses url.QueryEscape instead. It returns true in the former two cases. +func encodeComponent(s string) (string, bool) { + if s == "" { + return "=", true + } + if strings.Contains(s, "/") { + return base64.RawURLEncoding.EncodeToString([]byte(s)), true + } + return url.QueryEscape(s), false +} diff --git a/vendor/github.com/redis/go-redis/v9/.gitignore b/vendor/github.com/redis/go-redis/v9/.gitignore index 0d99709e349..93affec7fab 100644 --- a/vendor/github.com/redis/go-redis/v9/.gitignore +++ b/vendor/github.com/redis/go-redis/v9/.gitignore @@ -9,3 +9,11 @@ coverage.txt **/coverage.txt .vscode tmp/* +*.test +extra/redisotel-native/metrics-collector-app/ +# maintenanceNotifications upgrade documentation (temporary) +maintenanceNotifications/docs/ + +# Docker-generated files (TLS certificates, cluster data, etc.) +dockers/*/tls/ +dockers/osscluster-tls/ diff --git a/vendor/github.com/redis/go-redis/v9/.golangci.yml b/vendor/github.com/redis/go-redis/v9/.golangci.yml index 872454ff7f1..dd13c2c2938 100644 --- a/vendor/github.com/redis/go-redis/v9/.golangci.yml +++ b/vendor/github.com/redis/go-redis/v9/.golangci.yml @@ -26,6 +26,8 @@ linters: - builtin$ - examples$ formatters: + enable: + - gofmt exclusions: generated: lax paths: diff --git a/vendor/github.com/redis/go-redis/v9/CHANGELOG.md b/vendor/github.com/redis/go-redis/v9/CHANGELOG.md deleted file mode 100644 index e1652b179ad..00000000000 --- a/vendor/github.com/redis/go-redis/v9/CHANGELOG.md +++ /dev/null @@ -1,133 +0,0 @@ -## Unreleased - -### Changed - -* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) - Users can use the OpenTelemetry sampler to control the sampling behavior. - For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep - a similar behavior (drop orphan spans) of `go-redis` as before. - -## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) - - -### Features - -* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602)) -* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe)) -* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af)) - - - -## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01) - - -### Bug Fixes - -* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241)) - - -### Features - -* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e)) -* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8)) -* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af)) - - - -## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02) - -### New Features - -- feat(scan): scan time.Time sets the default decoding (#2413) -- Add support for CLUSTER LINKS command (#2504) -- Add support for acl dryrun command (#2502) -- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500) -- Add support for LCS Command (#2480) -- Add support for BZMPOP (#2456) -- Adding support for ZMPOP command (#2408) -- Add support for LMPOP (#2440) -- feat: remove pool unused fields (#2438) -- Expiretime and PExpireTime (#2426) -- Implement `FUNCTION` group of commands (#2475) -- feat(zadd): add ZAddLT and ZAddGT (#2429) -- Add: Support for COMMAND LIST command (#2491) -- Add support for BLMPOP (#2442) -- feat: check pipeline.Do to prevent confusion with Exec (#2517) -- Function stats, function kill, fcall and fcall_ro (#2486) -- feat: Add support for CLUSTER SHARDS command (#2507) -- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498) - -### Fixed - -- fix: eval api cmd.SetFirstKeyPos (#2501) -- fix: limit the number of connections created (#2441) -- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479) -- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458) -- fix: group lag can be null (#2448) - -### Maintenance - -- Updating to the latest version of redis (#2508) -- Allowing for running tests on a port other than the fixed 6380 (#2466) -- redis 7.0.8 in tests (#2450) -- docs: Update redisotel example for v9 (#2425) -- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476) -- chore: add Chinese translation (#2436) -- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421) -- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420) -- chore(deps): bump actions/setup-go from 3 to 4 (#2495) -- docs: add instructions for the HSet api (#2503) -- docs: add reading lag field comment (#2451) -- test: update go mod before testing(go mod tidy) (#2423) -- docs: fix comment typo (#2505) -- test: remove testify (#2463) -- refactor: change ListElementCmd to KeyValuesCmd. (#2443) -- fix(appendArg): appendArg case special type (#2489) - -## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01) - -### Features - -* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65)) - -## v9 2023-01-30 - -### Breaking - -- Changed Pipelines to not be thread-safe any more. - -### Added - -- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was - contributed by @monkey92t who has done the majority of work in this release. -- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts - and deadlines. See - [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details. -- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example, - `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`. -- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html) -- Added `redis.HasErrorPrefix` to help working with errors. - -### Changed - -- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is - completely gone in v9. -- Reworked hook interface and added `DialHook`. -- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See - [example](example/otel) and - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html). -- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making - an allocation. -- Renamed the option `MaxConnAge` to `ConnMaxLifetime`. -- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`. -- Removed connection reaper in favor of `MaxIdleConns`. -- Removed `WithContext` since `context.Context` can be passed directly as an arg. -- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and - it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to - reset commands for some reason. - -### Fixed - -- Improved and fixed pipeline retries. -- As usually, added support for more commands and fixed some bugs. diff --git a/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md b/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md index 7228a4a060c..8c68c522e5f 100644 --- a/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md +++ b/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md @@ -37,7 +37,7 @@ Here's how to get started with your code contribution: > Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about > the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity > to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`. -> By default the docker image that will be pulled and started is `redislabs/client-libs-test:rs-7.4.0-v2`. +> By default the docker image that will be pulled and started is `redislabs/client-libs-test:8.2.1-pre`. > If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box. 4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient). diff --git a/vendor/github.com/redis/go-redis/v9/Makefile b/vendor/github.com/redis/go-redis/v9/Makefile index 655f16f44ff..90f03b57e6d 100644 --- a/vendor/github.com/redis/go-redis/v9/Makefile +++ b/vendor/github.com/redis/go-redis/v9/Makefile @@ -1,11 +1,30 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) +REDIS_VERSION ?= 8.8 +RE_CLUSTER ?= false +RCE_DOCKER ?= true +CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.8.0 docker.start: + export RE_CLUSTER=$(RE_CLUSTER) && \ + export RCE_DOCKER=$(RCE_DOCKER) && \ + export REDIS_VERSION=$(REDIS_VERSION) && \ + export CLIENT_LIBS_TEST_IMAGE=$(CLIENT_LIBS_TEST_IMAGE) && \ docker compose --profile all up -d --quiet-pull docker.stop: docker compose --profile all down +docker.e2e.start: + @echo "Starting Redis and cae-resp-proxy for E2E tests..." + docker compose --profile e2e up -d --quiet-pull + @echo "Waiting for services to be ready..." + @sleep 3 + @echo "Services ready!" + +docker.e2e.stop: + @echo "Stopping E2E services..." + docker compose --profile e2e down + test: $(MAKE) docker.start @if [ -z "$(REDIS_VERSION)" ]; then \ @@ -27,7 +46,10 @@ test.ci: set -e; for dir in $(GO_MOD_DIRS); do \ echo "go test in $${dir}"; \ (cd "$${dir}" && \ - go mod tidy -compat=1.18 && \ + export RE_CLUSTER=$(RE_CLUSTER) && \ + export RCE_DOCKER=$(RCE_DOCKER) && \ + export REDIS_VERSION=$(REDIS_VERSION) && \ + go mod tidy && \ go vet && \ go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ done @@ -38,7 +60,10 @@ test.ci.skip-vectorsets: set -e; for dir in $(GO_MOD_DIRS); do \ echo "go test in $${dir} (skipping vector sets)"; \ (cd "$${dir}" && \ - go mod tidy -compat=1.18 && \ + export RE_CLUSTER=$(RE_CLUSTER) && \ + export RCE_DOCKER=$(RCE_DOCKER) && \ + export REDIS_VERSION=$(REDIS_VERSION) && \ + go mod tidy && \ go vet && \ go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ @@ -47,11 +72,41 @@ test.ci.skip-vectorsets: go vet -vettool ./internal/customvet/customvet bench: + export RE_CLUSTER=$(RE_CLUSTER) && \ + export RCE_DOCKER=$(RCE_DOCKER) && \ + export REDIS_VERSION=$(REDIS_VERSION) && \ go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example -.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt +test.e2e: + @echo "Running E2E tests with auto-start proxy..." + $(MAKE) docker.e2e.start + @echo "Running tests..." + @E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1) + $(MAKE) docker.e2e.stop + @echo "E2E tests completed!" + +test.e2e.docker: + @echo "Running Docker-compatible E2E tests..." + $(MAKE) docker.e2e.start + @echo "Running unified injector tests..." + @E2E_SCENARIO_TESTS=true go test -v -run "TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1) + $(MAKE) docker.e2e.stop + @echo "Docker E2E tests completed!" + +test.e2e.logic: + @echo "Running E2E logic tests (no proxy required)..." + @E2E_SCENARIO_TESTS=true \ + REDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \ + FAULT_INJECTION_API_URL=http://localhost:8080 \ + go test -v -run "TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ + @echo "Logic tests completed!" + +.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop build: + export RE_CLUSTER=$(RE_CLUSTER) && \ + export RCE_DOCKER=$(RCE_DOCKER) && \ + export REDIS_VERSION=$(REDIS_VERSION) && \ go build . fmt: @@ -63,5 +118,5 @@ go_mod_tidy: echo "go mod tidy in $${dir}"; \ (cd "$${dir}" && \ go get -u ./... && \ - go mod tidy -compat=1.18); \ + go mod tidy); \ done diff --git a/vendor/github.com/redis/go-redis/v9/README.md b/vendor/github.com/redis/go-redis/v9/README.md index c37a52ec70a..ae90d2b7db0 100644 --- a/vendor/github.com/redis/go-redis/v9/README.md +++ b/vendor/github.com/redis/go-redis/v9/README.md @@ -2,7 +2,7 @@ [![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc) -[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/) +[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.io/docs/latest/develop/clients/go/) [![Go Report Card](https://goreportcard.com/badge/github.com/redis/go-redis/v9)](https://goreportcard.com/report/github.com/redis/go-redis/v9) [![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) @@ -17,16 +17,24 @@ ## Supported versions In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support: -- [Redis 7.2](https://raw.githubusercontent.com/redis/redis/7.2/00-RELEASENOTES) - using Redis Stack 7.2 for modules support -- [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support -- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included +- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 +- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 +- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4 +- [Redis 8.8](https://raw.githubusercontent.com/redis/redis/8.8/00-RELEASENOTES) - using Redis CE 8.8 -Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three -versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0), -[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with +Although the `go.mod` states it requires at minimum `go 1.24`, our CI is configured to run the tests against all supported +versions of Redis and multiple versions of Go ([1.24](https://go.dev/doc/devel/release#go1.24.0), oldstable, and stable). We observe that some modules related test may not pass with Redis Stack 7.2 and some commands are changed with Redis CE 8.0. -Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version -in the `go.mod` to `go 1.24` in one of the next releases. +Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+. +Please do refer to the documentation and the tests if you experience any issues. + +### Array data type (Redis 8.8+) + +Starting with Redis 8.8, go-redis exposes the new array data type via the `AR*` command family +(`ARSET`, `ARGET`, `ARGETRANGE`, `ARMSET`, `ARMGET`, `ARINSERT`, `ARDEL`, `ARDELRANGE`, +`ARLEN`, `ARCOUNT`, `ARNEXT`, `ARSEEK`, `ARSCAN`, `ARGREP`, `ARRING`, `ARLASTITEMS`, +`ARINFO`/`ARINFOFULL`, and the `AROP*` reducers). See `array_commands.go` for the full +surface. The API is experimental and may change in a future release. ## How do I Redis? @@ -42,10 +50,6 @@ in the `go.mod` to `go 1.24` in one of the next releases. [Work at Redis](https://redis.com/company/careers/jobs/) -## Documentation - -- [English](https://redis.uptrace.dev) -- [简体中文](https://redis.uptrace.dev/zh/) ## Resources @@ -53,17 +57,20 @@ in the `go.mod` to `go 1.24` in one of the next releases. - [Chat](https://discord.gg/W4txy5AeKM) - [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9) - [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples) +- [Release notes](./RELEASE-NOTES.md) ([GitHub Releases](https://github.com/redis/go-redis/releases)) + +## old documentation + +- [English](https://redis.uptrace.dev) +- [简体中文](https://redis.uptrace.dev/zh/) ## Ecosystem -- [Redis Mock](https://github.com/go-redis/redismock) +- [Entra ID (Azure AD)](https://github.com/redis/go-redis-entraid) - [Distributed Locks](https://github.com/bsm/redislock) - [Redis Cache](https://github.com/go-redis/cache) - [Rate limiting](https://github.com/go-redis/redis_rate) -This client also works with [Kvrocks](https://github.com/apache/incubator-kvrocks), a distributed -key value NoSQL database that uses RocksDB as storage engine and is compatible with Redis protocol. - ## Features - Redis commands except QUIT and SYNC. @@ -74,9 +81,9 @@ key value NoSQL database that uses RocksDB as storage engine and is compatible w - [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html). - [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html). - [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html). -- [Redis Ring](https://redis.uptrace.dev/guide/ring.html). - [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html). - [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/) +- [Customizable read and write buffers size.](#custom-buffer-sizes) ## Installation @@ -112,6 +119,7 @@ func ExampleClient() { Password: "", // no password set DB: 0, // use default DB }) + defer rdb.Close() err := rdb.Set(ctx, "key", "value", 0).Err() if err != nil { @@ -137,6 +145,29 @@ func ExampleClient() { } ``` +### Dial retries and backoff + +Connection establishment can be retried by the connection pool when dialing fails. + +- **`DialerRetries`**: maximum number of dial attempts (default: 5). +- **`DialerRetryTimeout`**: default delay between attempts when no custom backoff is provided (default: 100ms). +- **`DialerRetryBackoff`**: optional function hook to control the delay between attempts. + +Example: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + + DialerRetries: 5, + DialerRetryTimeout: 100 * time.Millisecond, // used when DialerRetryBackoff is nil + + // Optional: exponential backoff with jitter and a cap. + DialerRetryBackoff: redis.DialRetryBackoffExponential(100*time.Millisecond, 2*time.Second), +}) +defer rdb.Close() +``` + ### Authentication The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options: @@ -297,6 +328,18 @@ func main() { ``` +### Buffer Size Configuration + +go-redis uses 32KiB read and write buffers by default for optimal performance. For high-throughput applications or large pipelines, you can customize buffer sizes: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + ReadBufferSize: 1024 * 1024, // 1MiB read buffer + WriteBufferSize: 1024 * 1024, // 1MiB write buffer +}) +``` + ### Advanced Configuration go-redis supports extending the client identification phase to allow projects to send their own custom client identification. @@ -322,18 +365,17 @@ rdb := redis.NewClient(&redis.Options{ }) ``` -#### Unstable RESP3 Structures for RediSearch Commands -When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. +#### RESP3 for RediSearch Commands (`UnstableResp3` is deprecated) +As of v9.20, `FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP` +parse RESP3 (map) responses into the same typed result objects as RESP2. **No flag +is required — `Val()` / `Result()` work uniformly on both protocols.** -To enable unstable RESP3, set the option in your client configuration: +The legacy `UnstableResp3` option is now a **no-op** and is retained on every +options struct only for backwards compatibility. It will be removed in a future +release; new code should not set it. -```go -redis.NewClient(&redis.Options{ - UnstableResp3: true, - }) -``` -**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data. - Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them: +`RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP +payload directly: ```go res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult() @@ -360,6 +402,21 @@ For example: ``` You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/). +#### Custom buffer sizes +Prior to v9.12, the buffer size was the default go value of 4096 bytes. Starting from v9.12, +go-redis uses 32KiB read and write buffers by default for optimal performance. +For high-throughput applications or large pipelines, you can customize buffer sizes: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + ReadBufferSize: 1024 * 1024, // 1MiB read buffer + WriteBufferSize: 1024 * 1024, // 1MiB write buffer +}) +``` + +**Important**: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes. + ## Contributing We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub. We appreciate your help in making go-redis better for everyone. @@ -400,38 +457,150 @@ vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello") res, err := rdb.Do(ctx, "set", "key", "value").Result() ``` -## Run the test +## Typed Errors -go-redis will start a redis-server and run the test cases. - -The paths of redis-server bin file and redis config file are defined in `main_test.go`: +go-redis provides typed error checking functions for common Redis errors: ```go -var ( - redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server")) - redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf")) -) +// Cluster and replication errors +redis.IsLoadingError(err) // Redis is loading the dataset +redis.IsReadOnlyError(err) // Write to read-only replica +redis.IsClusterDownError(err) // Cluster is down +redis.IsTryAgainError(err) // Command should be retried +redis.IsMasterDownError(err) // Master is down +redis.IsMovedError(err) // Returns (address, true) if key moved +redis.IsAskError(err) // Returns (address, true) if key being migrated + +// Connection and resource errors +redis.IsMaxClientsError(err) // Maximum clients reached +redis.IsAuthError(err) // Authentication failed (NOAUTH, WRONGPASS, unauthenticated) +redis.IsPermissionError(err) // Permission denied (NOPERM) +redis.IsOOMError(err) // Out of memory (OOM) + +// Transaction errors +redis.IsExecAbortError(err) // Transaction aborted (EXECABORT) ``` -For local testing, you can change the variables to refer to your local files, or create a soft link -to the corresponding folder for redis-server and copy the config file to `testdata/redis/`: +### Error Wrapping in Hooks -```shell -ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src -cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/ +When wrapping errors in hooks, use custom error types with `Unwrap()` method (preferred) or `fmt.Errorf` with `%w`. Always call `cmd.SetErr()` to preserve error type information: + +```go +// Custom error type (preferred) +type AppError struct { + Code string + RequestID string + Err error +} + +func (e *AppError) Error() string { + return fmt.Sprintf("[%s] request_id=%s: %v", e.Code, e.RequestID, e.Err) +} + +func (e *AppError) Unwrap() error { + return e.Err +} + +// Hook implementation +func (h MyHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { + return func(ctx context.Context, cmd redis.Cmder) error { + err := next(ctx, cmd) + if err != nil { + // Wrap with custom error type + wrappedErr := &AppError{ + Code: "REDIS_ERROR", + RequestID: getRequestID(ctx), + Err: err, + } + cmd.SetErr(wrappedErr) + return wrappedErr // Return wrapped error to preserve it + } + return nil + } +} + +// Typed error detection works through wrappers +if redis.IsLoadingError(err) { + // Retry logic +} + +// Extract custom error if needed +var appErr *AppError +if errors.As(err, &appErr) { + log.Printf("Request: %s", appErr.RequestID) +} ``` -Lastly, run: +Alternatively, use `fmt.Errorf` with `%w`: +```go +wrappedErr := fmt.Errorf("context: %w", err) +cmd.SetErr(wrappedErr) +``` -```shell -go test +### Pipeline Hook Example + +For pipeline operations, use `ProcessPipelineHook`: + +```go +type PipelineLoggingHook struct{} + +func (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook { + return next +} + +func (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { + return next +} + +func (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { + return func(ctx context.Context, cmds []redis.Cmder) error { + start := time.Now() + + // Execute the pipeline + err := next(ctx, cmds) + + duration := time.Since(start) + log.Printf("Pipeline executed %d commands in %v", len(cmds), duration) + + // Process individual command errors + // Note: Individual command errors are already set on each cmd by the pipeline execution + for _, cmd := range cmds { + if cmdErr := cmd.Err(); cmdErr != nil { + // Check for specific error types using typed error functions + if redis.IsAuthError(cmdErr) { + log.Printf("Auth error in pipeline command %s: %v", cmd.Name(), cmdErr) + } else if redis.IsPermissionError(cmdErr) { + log.Printf("Permission error in pipeline command %s: %v", cmd.Name(), cmdErr) + } + + // Optionally wrap individual command errors to add context + // The wrapped error preserves type information through errors.As() + wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr) + cmd.SetErr(wrappedErr) + } + } + + // Return the pipeline-level error (connection errors, etc.) + // You can wrap it if needed, or return it as-is + return err + } +} + +// Register the hook +rdb.AddHook(PipelineLoggingHook{}) + +// Use pipeline - errors are still properly typed +pipe := rdb.Pipeline() +pipe.Set(ctx, "key1", "value1", 0) +pipe.Get(ctx, "key2") +_, err := pipe.Exec(ctx) ``` -Another option is to run your specific tests with an already running redis. The example below, tests -against a redis running on port 9999.: +## Run the test +Recommended to use Docker, just need to run: ```shell -REDIS_PORT=9999 go test +make test ``` ## See also diff --git a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md b/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md index f6a4abb921f..de24456c3b2 100644 --- a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md +++ b/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md @@ -1,5 +1,769 @@ # Release Notes +# 9.20.1 (2026-06-11) + +This is a patch release containing bug fixes only. There are no new features or breaking changes; upgrading from 9.20.0 is a drop-in replacement. + +## 🚀 Highlights + +### RESP3 pub/sub message loss fixed + +`PeekPushNotificationName` previously inspected only the bytes already buffered by `bufio`, so when a push frame header straddled a buffer fill boundary it could return a **truncated** notification name (e.g. `"messa"` instead of `"message"`). The push processor then mis-routed the frame and `ReadReply` silently dropped it, causing intermittent RESP3 pub/sub message loss. The peek now grows its window (36 bytes → up to 4 KiB) and reads more from the connection until the header is complete, cleanly separating incomplete prefixes from corrupt frames (including overflow-safe bulk-length handling). Fixes [#3839](https://github.com/redis/go-redis/issues/3839). + +([#3842](https://github.com/redis/go-redis/pull/3842)) by [@ndyakov](https://github.com/ndyakov) + +## 🐛 Bug Fixes + +- **RESP3 push peeking**: `PeekPushNotificationName` no longer returns a truncated notification name when a push frame header spans a buffer boundary, preventing silent RESP3 pub/sub message loss (fixes [#3839](https://github.com/redis/go-redis/issues/3839)) ([#3842](https://github.com/redis/go-redis/pull/3842)) by [@ndyakov](https://github.com/ndyakov) +- **`FT.HYBRID` vector params**: Vector data is now always sent via `PARAMS` with auto-generated param names (`__vector_param_N`, with collision avoidance) when `VectorParamName` is omitted, since Redis no longer accepts inline vector blobs; the `FTHybridOptions.Params` map is no longer mutated, so the same options struct can be reused across calls ([#3844](https://github.com/redis/go-redis/pull/3844)) by [@ndyakov](https://github.com/ndyakov) +- **`CLUSTER SHARDS` forward compatibility**: Unknown shard- and node-level attributes in the `CLUSTER SHARDS` reply are now skipped via `DiscardNext()` instead of erroring, so clients keep working when the server introduces new fields ([#3843](https://github.com/redis/go-redis/pull/3843)) by [@madolson](https://github.com/madolson) +- **PubSub double reconnect**: `PubSub.releaseConn` no longer reconnects twice when a connection is both unusable (or pending handoff) and reports a bad-connection error, avoiding a wasted connection establish-then-close cycle ([#3833](https://github.com/redis/go-redis/pull/3833)) by [@cxljs](https://github.com/cxljs) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@cxljs](https://github.com/cxljs), [@madolson](https://github.com/madolson), [@ndyakov](https://github.com/ndyakov) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.20.0...v9.20.1 + +# 9.20.0 (2026-05-28) + +## 🚀 Highlights + +### Redis 8.8 Support + +This release adds support for **Redis 8.8**. The README's supported-versions list now includes Redis 8.8 alongside 8.0/8.2/8.4, and CI exercises the `8.8-rc1` client-libs-test image across the full suite (Makefile, build workflow, doctests, run-tests action, and docker-compose). + +Coverage for the new commands that ship in the 8.x line, rounded out in this release: + +- **`AR*` array data type** ([#3813](https://github.com/redis/go-redis/pull/3813)) — new array data structure, exposed via the `ArrayCmdable` interface (see the experimental-features highlight below). +- **`INCREX`** ([#3816](https://github.com/redis/go-redis/pull/3816)) — atomic increment with expiration in a single round-trip. +- **`XNACK`** ([#3790](https://github.com/redis/go-redis/pull/3790)) — explicit negative-acknowledge of pending stream entries. +- **`XAUTOCLAIM` PEL deletes** ([#3798](https://github.com/redis/go-redis/pull/3798)) — `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the pending entries list. +- **`TS.RANGE` multiple aggregators** ([#3791](https://github.com/redis/go-redis/pull/3791)) — `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` accept multiple aggregators in a single call. +- **`Z(UNION|INTER|DIFF)` `COUNT` aggregator** ([#3802](https://github.com/redis/go-redis/pull/3802)) — `COUNT` reducer for sorted-set set operations. +- **`JSON.SET FPHA`** ([#3797](https://github.com/redis/go-redis/pull/3797)) — new `FPHA` argument that specifies the floating-point type for homogeneous FP arrays. + +CI image bump ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa). Command coverage contributions by [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@ndyakov](https://github.com/ndyakov), and [@ofekshenawa](https://github.com/ofekshenawa). + +### Stable RESP3 for RediSearch (`UnstableResp3` deprecated) + +`FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP` now parse RESP3 (map) responses into the same typed result objects as RESP2 — `Val()` and `Result()` work uniformly on both protocols, no flag required. Previously, RESP3 search responses required `UnstableResp3: true` and were returned as opaque maps accessible only via `RawResult()` / `RawVal()`. + +As a result, the `UnstableResp3` option is now a **no-op** across every options struct (`Options`, `ClusterOptions`, `UniversalOptions`, `FailoverOptions`, `RingOptions`) and has been marked `// Deprecated:`. The field is retained for backwards compatibility — existing code that sets `UnstableResp3: true` will continue to compile and behave identically — but it will be removed in a future release and new code should not set it. `RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP payload. + +([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov) + +### Experimental Array Data Structure Commands + +Adds an experimental `ArrayCmdable` interface with the `AR*` command family (`ARSet`, `ARGet`, `ARGetRange`, `ARMSet`, `ARMGet`, `ARDel`, `ARDelRange`, `ARScan`, `ARSeek`, `ARNext`, `ARLastItems`, `ARGrep`, `ARGrepWithValues`, `ARInfo`/`ARInfoFull`, and typed reducers `AROpSum`/`AROpMin`/`AROpMax`/`AROpAnd`/`AROpOr`/`AROpXor`/`AROpMatch`/`AROpUsed`) for working with Redis 8.8's new array data type. **API is experimental and may change in a future release.** + +([#3813](https://github.com/redis/go-redis/pull/3813)) by [@cxljs](https://github.com/cxljs) + +## ✨ New Features + +- **RESP3 search parser**: First-class RESP3 parsing for `FT.SEARCH`/`FT.AGGREGATE`/`FT.INFO`/`FT.SPELLCHECK`/`FT.SYNDUMP` responses with backwards compatibility for RESP2 ([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov) +- **INCREX**: New `INCREX` command support — atomic increment with expiration ([#3816](https://github.com/redis/go-redis/pull/3816)) by [@ndyakov](https://github.com/ndyakov) +- **XNACK**: Client support for the `XNACK` stream command for explicitly negative-acknowledging pending entries ([#3790](https://github.com/redis/go-redis/pull/3790)) by [@elena-kolevska](https://github.com/elena-kolevska) +- **TS range multiple aggregators**: `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` now accept multiple aggregators in a single call ([#3791](https://github.com/redis/go-redis/pull/3791)) by [@elena-kolevska](https://github.com/elena-kolevska) +- **`XAutoClaim` deleted IDs**: `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the PEL ([#3798](https://github.com/redis/go-redis/pull/3798)) by [@Khukharr](https://github.com/Khukharr) +- **`JSON.SET FPHA`**: `JSON.SET` accepts a new `FPHA` argument that specifies the floating-point type for homogeneous floating-point arrays ([#3797](https://github.com/redis/go-redis/pull/3797)) by [@ndyakov](https://github.com/ndyakov) +- **Sorted-set union/intersection COUNT**: `ZUNION`/`ZINTER`/`ZDIFF` aggregator now supports `COUNT` ([#3802](https://github.com/redis/go-redis/pull/3802)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **`FT.HYBRID` vector validation**: Validates hybrid-search vector input types and adds proper typed vector parameters ([#3756](https://github.com/redis/go-redis/pull/3756)) by [@DengY11](https://github.com/DengY11) +- **Cluster pool wait stats**: `ClusterClient.PoolStats()` now accumulates `WaitCount` and `WaitDurationNs` across all node pools (previously always zero) ([#3809](https://github.com/redis/go-redis/pull/3809)) by [@LINKIWI](https://github.com/LINKIWI) + +## 🐛 Bug Fixes + +- **TLS-only Cluster PubSub**: `CLUSTER SLOTS` port-0 entries now fall back to the origin endpoint's port, fixing `dial tcp :0: connection refused` on TLS-only clusters started with `--port 0 --tls-port ` (fixes [#3726](https://github.com/redis/go-redis/issues/3726)) ([#3828](https://github.com/redis/go-redis/pull/3828)) by [@ndyakov](https://github.com/ndyakov) +- **Sharded PubSub reconnect routing**: `PubSub.conn()` now passes both regular (`c.channels`) and sharded (`c.schannels`) channels into the per-PubSub `newConn` closure. Previously, `ClusterClient.SSubscribe`-only PubSubs reconnected to a random node (because the routing closure saw an empty channel list), the `SSUBSCRIBE` was sent to the wrong shard, and the resulting `MOVED` reply was silently dropped ([#3829](https://github.com/redis/go-redis/pull/3829)) by [@ndyakov](https://github.com/ndyakov) +- **ClusterClient `Watch` retry**: User errors returned from a `Watch` callback are no longer subjected to cluster-retry classification; transient cluster errors still retry, but a callback returning e.g. `net.ErrClosed` short-circuits immediately ([#3821](https://github.com/redis/go-redis/pull/3821)) by [@obiyang](https://github.com/obiyang) +- **Sentinel concurrent-probe leak**: `MasterAddr`'s concurrent sentinel probe now closes the non-winning sentinel clients instead of leaking them ([#3827](https://github.com/redis/go-redis/pull/3827)) by [@cxljs](https://github.com/cxljs) +- **Sentinel rediscovery loop on master-only setups**: `replicaAddrs` no longer tears down the cached sentinel client when the replica list is empty, eliminating a continuous rediscovery loop on master-only Sentinel deployments that flooded logs and added per-operation latency ([#3795](https://github.com/redis/go-redis/pull/3795)) by [@shahyash2609](https://github.com/shahyash2609) +- **Pool `CloseConn` hooks**: `Pool.CloseConn` now triggers registered hooks, fixing a memory leak when connections are closed explicitly rather than via the normal removal path ([#3818](https://github.com/redis/go-redis/pull/3818)) by [@ndyakov](https://github.com/ndyakov) +- **Dial TCP error redirection**: Wrapped `dial tcp` errors are now correctly classified as redirectable so cluster routing can recover from a single unreachable node ([#3810](https://github.com/redis/go-redis/pull/3810)) by [@vladisa88](https://github.com/vladisa88) +- **Pool `Close` health checks**: `ConnPool.Close` now only runs health checks against idle connections, avoiding spurious activity on connections still in use ([#3805](https://github.com/redis/go-redis/pull/3805)) by [@ndyakov](https://github.com/ndyakov) +- **VLinks return type**: Fixed the return type of `VLINKS`/`VLINKSWITHSCORES` vector-set replies ([#3820](https://github.com/redis/go-redis/pull/3820)) by [@romanpovol](https://github.com/romanpovol) + +## 🧪 Testing & Infrastructure + +- **Flaky tests**: Stabilized several flaky tests in the sentinel and pool suites ([#3815](https://github.com/redis/go-redis/pull/3815)) by [@ndyakov](https://github.com/ndyakov) +- **Sentinel failover metric race**: Fixed a data race in the sentinel failover metric test ([#3824](https://github.com/redis/go-redis/pull/3824)) by [@cxljs](https://github.com/cxljs) +- **`waitForSentinelClusterStable` post-conditions**: The sentinel test harness now waits for replicas to be fully connected (not just present in the count) and is robust to randomized spec ordering after failover specs, eliminating an intermittent `Expected master to equal slave` flake ([#3830](https://github.com/redis/go-redis/pull/3830)) by [@ndyakov](https://github.com/ndyakov) +- **`govulncheck` workflow**: New scheduled GitHub Actions workflow runs `govulncheck` on every push, PR, and weekly, surfacing newly disclosed Go vulnerabilities even when no code changes ([#3779](https://github.com/redis/go-redis/pull/3779)) by [@solardome](https://github.com/solardome) +- **CI Redis 8.8-rc1**: CI now exercises the 8.8-rc1 Redis image ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa) + +## 🧰 Maintenance + +- **`Cmd.Slot()` lookup refactor**: Caches the per-command `CommandInfo` and short-circuits keyless commands before the switch dispatch, removing redundant `Peek` calls ([#3804](https://github.com/redis/go-redis/pull/3804)) by [@retr0-kernel](https://github.com/retr0-kernel) +- **stdlib `math/rand`**: Replaced `internal/rand` with `math/rand` from the standard library now that the minimum Go version is 1.24 ([#3823](https://github.com/redis/go-redis/pull/3823)) by [@cxljs](https://github.com/cxljs) +- **ConnPool queue channel**: Removed the unused queue channel from `ConnPool`, trimming the pool's footprint ([#3826](https://github.com/redis/go-redis/pull/3826)) by [@cxljs](https://github.com/cxljs) +- **Extra packages LICENSE**: Added a LICENSE file to each `extra/*` package ([#3817](https://github.com/redis/go-redis/pull/3817)) by [@ndyakov](https://github.com/ndyakov) +- **README & CI image**: Documentation refresh and bumped the default CI image tag ([#3822](https://github.com/redis/go-redis/pull/3822)) by [@ndyakov](https://github.com/ndyakov) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@cxljs](https://github.com/cxljs), [@DengY11](https://github.com/DengY11), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@LINKIWI](https://github.com/LINKIWI), [@ndyakov](https://github.com/ndyakov), [@obiyang](https://github.com/obiyang), [@ofekshenawa](https://github.com/ofekshenawa), [@retr0-kernel](https://github.com/retr0-kernel), [@romanpovol](https://github.com/romanpovol), [@shahyash2609](https://github.com/shahyash2609), [@solardome](https://github.com/solardome), [@vladisa88](https://github.com/vladisa88) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.19.0...v9.20.0 + +# 9.19.0 (2026-04-27) + +## 🚀 Highlights + +### FIPS-Compatible Script Helper + +`Script` now supports a FIPS-safe execution mode that avoids client-side SHA-1 computation, which is blocked in strict FIPS environments. A new `NewScriptServerSHA` constructor uses `SCRIPT LOAD` to obtain and cache the digest from the server, then runs commands via `EVALSHA`/`EVALSHA_RO`. Falls back to `EVAL`/`EVALRO` if loading fails, and transparently retries once on `NOSCRIPT`. The default behavior is unchanged for existing users. + +([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati) + +### FT.AGGREGATE Step-Based Pipeline Builder + +Added a new step-based `FT.AGGREGATE` pipeline API via `FTAggregateOptions.Steps`, allowing `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` (with per-step `MAX`) to be repeated and interleaved in arbitrary order — matching Redis's native multi-stage aggregation semantics. The legacy `Load`/`Apply`/`GroupBy`/`SortBy`/`SortByMax` fields are now deprecated. + +([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov) + +### Raw RESP Protocol Access + +Added `DoRaw` and `DoRawWriteTo` methods for executing arbitrary commands and reading the raw RESP response. Useful for proxying, custom protocol inspection, and working with commands not yet wrapped by go-redis. + +([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa) + +### Configurable Dial Retry Backoff + +Added `DialerRetryBackoff` option (plumbed through `Options`, `ClusterOptions`, `RingOptions`, `FailoverOptions`) to let callers customize the delay between failed dial attempts. Helpers `DialRetryBackoffConstant` and `DialRetryBackoffExponential` (with jitter and cap) are provided out of the box. Dial timeout is now also applied **per attempt** rather than across all retries. + +([#3706](https://github.com/redis/go-redis/pull/3706), [#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker) + +## ✨ New Features + +- **FT.AGGREGATE Steps**: Step-based pipeline builder for `FT.AGGREGATE` with support for repeated/interleaved `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` stages ([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov) +- **VectorSet commands**: Added `VISMEMBER` and `WITHATTRIBS` support ([#3753](https://github.com/redis/go-redis/pull/3753)) by [@romanpovol](https://github.com/romanpovol) +- **FIPS-safe Script**: `NewScriptServerSHA` uses `SCRIPT LOAD` to obtain the digest from the server, avoiding client-side SHA-1 ([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati) +- **Raw RESP access**: `DoRaw` and `DoRawWriteTo` for raw RESP protocol access ([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **Dial retry backoff**: `DialerRetryBackoff` function option with constant and exponential helpers ([#3706](https://github.com/redis/go-redis/pull/3706)) by [@mwhooker](https://github.com/mwhooker) +- **Typed NOSCRIPT error**: Redis `NOSCRIPT` replies are now surfaced as a typed error for easier handling ([#3738](https://github.com/redis/go-redis/pull/3738)) by [@LINKIWI](https://github.com/LINKIWI) +- **PubSub ClientSetName**: Added `ClientSetName` method to `PubSub` ([#3727](https://github.com/redis/go-redis/pull/3727)) by [@Flack74](https://github.com/Flack74) +- **ReplicaOf**: New `ReplicaOf` method replaces the deprecated `SlaveOf` ([#3720](https://github.com/redis/go-redis/pull/3720)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **HSCAN BinaryUnmarshaler**: `HScan` now supports types implementing `encoding.BinaryUnmarshaler` ([#3768](https://github.com/redis/go-redis/pull/3768)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1) + +## 🐛 Bug Fixes + +- **Auto hostname type detection**: Improved endpoint type detection for maintenance notifications using DNS-based classification; handles empty hosts and expanded private-IP ranges ([#3789](https://github.com/redis/go-redis/pull/3789)) by [@ndyakov](https://github.com/ndyakov) +- **HELLO fallback**: Don't send `CLIENT MAINT_NOTIFICATIONS` handshake when `HELLO` fails and connection falls back to RESP2; fail fast when explicitly enabled with RESP3 ([#3788](https://github.com/redis/go-redis/pull/3788)) by [@ndyakov](https://github.com/ndyakov) +- **Dial TCP retry**: `ShouldRetry` now treats `net.OpError` with `Op == "dial"` timeout errors as safe to retry since no command was sent ([#3787](https://github.com/redis/go-redis/pull/3787)) by [@vladisa88](https://github.com/vladisa88) +- **wrappedOnClose leak**: Fixed resource leak caused by repeatedly wrapping `baseClient` close logic; replaced with a bounded, concurrency-safe named-hook registry ([#3785](https://github.com/redis/go-redis/pull/3785)) by [@ndyakov](https://github.com/ndyakov) +- **Pool Close() on stale connections**: Suppress close errors (e.g., TLS `closeNotify` timeouts) for connections already dropped by the server due to idle timeout ([#3778](https://github.com/redis/go-redis/pull/3778)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **FIFO waiter ordering**: Fixed race in `ConnStateMachine.notifyWaiters` that could wake multiple waiters under a single mutex hold and violate FIFO ordering ([#3777](https://github.com/redis/go-redis/pull/3777)) by [@0x48core](https://github.com/0x48core) +- **Lua READONLY detection**: Detect `READONLY` errors embedded in Lua script error messages on read-only replicas so commands are correctly retried ([#3769](https://github.com/redis/go-redis/pull/3769)) by [@zhengjilei](https://github.com/zhengjilei) +- **VectorScoreSliceCmd RESP2**: Fixed `VSimWithScores`, `VSimWithArgsWithScores`, and `VLinksWithScores` which were broken on RESP2 connections returning flat arrays instead of maps ([#3767](https://github.com/redis/go-redis/pull/3767)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **Closed connection handling**: Two fixes for closed connection handling in the pool ([#3764](https://github.com/redis/go-redis/pull/3764)) by [@cxljs](https://github.com/cxljs) +- **ZRangeArgs Rev**: Fixed `ZRangeArgs` with `Rev` + `ByScore`/`ByLex` incorrectly swapping `Start`/`Stop`, breaking `ZRANGESTORE` ([#3751](https://github.com/redis/go-redis/pull/3751)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **OTel metric instrument types**: Fixed metric instrument types in `redisotel-native` ([#3743](https://github.com/redis/go-redis/pull/3743)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **Options.clone() data race**: Fixed data race when cloning `Options` ([#3739](https://github.com/redis/go-redis/pull/3739)) by [@rubensayshi](https://github.com/rubensayshi) +- **Connection closure metrics**: Fixed connection closure metrics and enabled all metric groups by default in `redisotel-native` ([#3735](https://github.com/redis/go-redis/pull/3735)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **OTel semconv v1.38.0**: Use metric definition from `otel/semconv/v1.38.0` in `redisotel-native` ([#3731](https://github.com/redis/go-redis/pull/3731)) by [@wzy9607](https://github.com/wzy9607) +- **SETNX semantics**: Use `SET ... NX` instead of the deprecated `SETNX` command ([#3723](https://github.com/redis/go-redis/pull/3723)) by [@ndyakov](https://github.com/ndyakov) +- **TIME keyless routing**: Mark `TIME` as a keyless command for correct cluster routing ([#3722](https://github.com/redis/go-redis/pull/3722)) by [@fatal10110](https://github.com/fatal10110) +- **Dial timeout per retry**: Dial timeout now applies per attempt instead of across all retry attempts combined ([#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker) +- **Cluster metrics attributes**: Fixed `pool.name` being appended per node, which corrupted and dropped user-provided custom attributes ([#3699](https://github.com/redis/go-redis/pull/3699)) by [@Jesse-Bonfire](https://github.com/Jesse-Bonfire) +- **initConn nil dereference**: Fixed nil pointer dereference and potential deadlock in `*baseClient.initConn()`; added explicit nil option guards to client constructors ([#3676](https://github.com/redis/go-redis/pull/3676)) by [@olde-ducke](https://github.com/olde-ducke) + +## ⚡ Performance + +- **RESP reader**: Optimized RESP reader by eliminating intermediate string allocations ([#3774](https://github.com/redis/go-redis/pull/3774)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1) +- **Inline rendezvous hashing**: Replaced `github.com/dgryski/go-rendezvous` dependency with an in-repo implementation in `internal/hashtag`, reducing the dependency graph while preserving algorithm parity ([#3762](https://github.com/redis/go-redis/pull/3762)) by [@bigsk05](https://github.com/bigsk05) + +## 🧪 Testing & Infrastructure + +- **Release automation**: Added `repository`, `ref`, and `client-libs-test-image-tag` inputs to the `run-tests` composite action; `redis-version` is now optional so unstable builds use `REDIS_VERSION` from the Makefile ([#3749](https://github.com/redis/go-redis/pull/3749)) by [@dariaguy](https://github.com/dariaguy) +- **Go 1.24**: Updated minimum Go version to 1.24 and use `-compat=1.24` in release scripts ([#3714](https://github.com/redis/go-redis/pull/3714), [#3754](https://github.com/redis/go-redis/pull/3754)) by [@ndyakov](https://github.com/ndyakov), [@cxljs](https://github.com/cxljs) + +## 🧰 Maintenance + +- **Pool state machine**: Removed redundant `Conn.closed` atomic field in favor of the state machine's `StateClosed` ([#3783](https://github.com/redis/go-redis/pull/3783)) by [@cxljs](https://github.com/cxljs) +- **OTel SDK**: Updated OpenTelemetry SDK dependencies in `redisotel`/`redisotel-native` ([#3770](https://github.com/redis/go-redis/pull/3770)) by [@ndyakov](https://github.com/ndyakov) +- **Go 1.21+ built-ins**: Use `maps.Keys`, `slices.Collect`, `slices.Contains`, `clear()`, and `slices.SortFunc` instead of custom helpers ([#3758](https://github.com/redis/go-redis/pull/3758), [#3746](https://github.com/redis/go-redis/pull/3746)) by [@cxljs](https://github.com/cxljs) +- **HGetAll docs**: Added Go doc comment to `HGetAll` describing behavior and complexity ([#3776](https://github.com/redis/go-redis/pull/3776)) by [@0x48core](https://github.com/0x48core) +- **Docs links**: Fixed irrelevant docs links ([#3724](https://github.com/redis/go-redis/pull/3724)) by [@olzhas-sabiyev](https://github.com/olzhas-sabiyev) +- **Examples cleanup**: Removed throughput binary from examples ([#3733](https://github.com/redis/go-redis/pull/3733)) by [@ndyakov](https://github.com/ndyakov) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@0x48core](https://github.com/0x48core), [@Aaditya-dubey1](https://github.com/Aaditya-dubey1), [@Copilot](https://github.com/apps/copilot-swe-agent), [@Flack74](https://github.com/Flack74), [@Jesse-Bonfire](https://github.com/Jesse-Bonfire), [@LINKIWI](https://github.com/LINKIWI), [@bigsk05](https://github.com/bigsk05), [@chaitanyabodlapati](https://github.com/chaitanyabodlapati), [@cxljs](https://github.com/cxljs), [@dariaguy](https://github.com/dariaguy), [@fatal10110](https://github.com/fatal10110), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@olde-ducke](https://github.com/olde-ducke), [@olzhas-sabiyev](https://github.com/olzhas-sabiyev), [@romanpovol](https://github.com/romanpovol), [@rubensayshi](https://github.com/rubensayshi), [@vladisa88](https://github.com/vladisa88), [@wzy9607](https://github.com/wzy9607), [@zhengjilei](https://github.com/zhengjilei) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0...v9.19.0 + +# 9.18.0 (2026-02-16) + +## 🚀 Highlights + +### Redis 8.6 Support + +Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS. + +### Smart Client Handoff (Maintenance Notifications) for Cluster + +This release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by: +- **Relaxing timeouts during migration** (SMIGRATING) to prevent false failures +- **Triggering lazy cluster state reloads** upon completion (SMIGRATED) +- Enabling seamless operations during Redis Enterprise maintenance windows + +([#3643](https://github.com/redis/go-redis/pull/3643)) by [@ndyakov](https://github.com/ndyakov) + +### OpenTelemetry Native Metrics Support + +Added comprehensive OpenTelemetry metrics support following the [OpenTelemetry Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/). The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new `extra/redisotel-native` package. + +**Metric groups include:** +- Command metrics: Operation duration with retry tracking +- Connection basic: Connection count and creation time +- Resiliency: Errors, handoffs, timeout relaxation +- Connection advanced: Wait time and use time +- Pubsub metrics: Published and received messages +- Stream metrics: Processing duration and maintenance notifications + +([#3637](https://github.com/redis/go-redis/pull/3637)) by [@ofekshenawa](https://github.com/ofekshenawa) + +## ✨ New Features + +- **HOTKEYS Commands**: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization ([#3695](https://github.com/redis/go-redis/pull/3695)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **Streams Idempotent Production**: Added support for Redis 8.6+ Streams Idempotent Production with `ProducerID`, `IdempotentID`, `IdempotentAuto` in `XAddArgs` and new `XCFGSET` command ([#3693](https://github.com/redis/go-redis/pull/3693)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **NaN Values for TimeSeries**: Added support for NaN (Not a Number) values in Redis time series commands ([#3687](https://github.com/redis/go-redis/pull/3687)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **DialerRetries Options**: Added `DialerRetries` and `DialerRetryTimeout` to `ClusterOptions`, `RingOptions`, and `FailoverOptions` ([#3686](https://github.com/redis/go-redis/pull/3686)) by [@naveenchander30](https://github.com/naveenchander30) +- **ConnMaxLifetimeJitter**: Added jitter configuration to distribute connection expiration times and prevent thundering herd ([#3666](https://github.com/redis/go-redis/pull/3666)) by [@cyningsun](https://github.com/cyningsun) +- **Digest Helper Functions**: Added `DigestString` and `DigestBytes` helper functions for client-side xxh3 hashing compatible with Redis DIGEST command ([#3679](https://github.com/redis/go-redis/pull/3679)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **SMIGRATED New Format**: Updated SMIGRATED parser to support new format and remember original host:port ([#3697](https://github.com/redis/go-redis/pull/3697)) by [@ndyakov](https://github.com/ndyakov) +- **Cluster State Reload Interval**: Added cluster state reload interval option for maintenance notifications ([#3663](https://github.com/redis/go-redis/pull/3663)) by [@ndyakov](https://github.com/ndyakov) + +## 🐛 Bug Fixes + +- **PubSub nil pointer dereference**: Fixed nil pointer dereference in PubSub after `WithTimeout()` - `pubSubPool` is now properly cloned ([#3710](https://github.com/redis/go-redis/pull/3710)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **MaintNotificationsConfig nil check**: Guard against nil `MaintNotificationsConfig` in `initConn` ([#3707](https://github.com/redis/go-redis/pull/3707)) by [@veeceey](https://github.com/veeceey) +- **wantConnQueue zombie elements**: Fixed zombie `wantConn` elements accumulation in `wantConnQueue` ([#3680](https://github.com/redis/go-redis/pull/3680)) by [@cyningsun](https://github.com/cyningsun) +- **XADD/XTRIM approx flag**: Fixed XADD and XTRIM to use `=` when approx is false ([#3684](https://github.com/redis/go-redis/pull/3684)) by [@ndyakov](https://github.com/ndyakov) +- **Sentinel timeout retry**: When connection to a sentinel times out, attempt to connect to other sentinels ([#3654](https://github.com/redis/go-redis/pull/3654)) by [@cxljs](https://github.com/cxljs) + +## ⚡ Performance + +- **Fuzz test optimization**: Eliminated repeated string conversions, used functional approach for cleaner operation selection ([#3692](https://github.com/redis/go-redis/pull/3692)) by [@feiguoL](https://github.com/feiguoL) +- **Pre-allocate capacity**: Pre-allocate slice capacity to prevent multiple capacity expansions ([#3689](https://github.com/redis/go-redis/pull/3689)) by [@feelshu](https://github.com/feelshu) + +## 🧪 Testing + +- **Comprehensive TLS tests**: Added comprehensive TLS tests and example for standalone, cluster, and certificate authentication ([#3681](https://github.com/redis/go-redis/pull/3681)) by [@ndyakov](https://github.com/ndyakov) +- **Redis 8.6**: Updated CI to use Redis 8.6-pre ([#3685](https://github.com/redis/go-redis/pull/3685)) by [@ndyakov](https://github.com/ndyakov) + +## 🧰 Maintenance + +- **Deprecation warnings**: Added deprecation warnings for commands based on Redis documentation ([#3673](https://github.com/redis/go-redis/pull/3673)) by [@ndyakov](https://github.com/ndyakov) +- **Use errors.Join()**: Replaced custom error join function with standard library `errors.Join()` ([#3653](https://github.com/redis/go-redis/pull/3653)) by [@cxljs](https://github.com/cxljs) +- **Use Go 1.21 min/max**: Use Go 1.21's built-in min/max functions ([#3656](https://github.com/redis/go-redis/pull/3656)) by [@cxljs](https://github.com/cxljs) +- **Proper formatting**: Code formatting improvements ([#3670](https://github.com/redis/go-redis/pull/3670)) by [@12ya](https://github.com/12ya) +- **Set commands documentation**: Added comprehensive documentation to all set command methods ([#3642](https://github.com/redis/go-redis/pull/3642)) by [@iamamirsalehi](https://github.com/iamamirsalehi) +- **MaxActiveConns docs**: Added default value documentation for `MaxActiveConns` ([#3674](https://github.com/redis/go-redis/pull/3674)) by [@codykaup](https://github.com/codykaup) +- **README example update**: Updated README example ([#3657](https://github.com/redis/go-redis/pull/3657)) by [@cxljs](https://github.com/cxljs) +- **Cluster maintnotif example**: Added example application for cluster maintenance notifications ([#3651](https://github.com/redis/go-redis/pull/3651)) by [@ndyakov](https://github.com/ndyakov) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@12ya](https://github.com/12ya), [@Copilot](https://github.com/apps/copilot-swe-agent), [@codykaup](https://github.com/codykaup), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@feelshu](https://github.com/feelshu), [@feiguoL](https://github.com/feiguoL), [@iamamirsalehi](https://github.com/iamamirsalehi), [@naveenchander30](https://github.com/naveenchander30), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@veeceey](https://github.com/veeceey) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.18.0 + +# 9.18.0-beta.2 (2025-12-09) + +## 🚀 Highlights + +### Go Version Update + +This release updates the minimum required Go version to 1.21. This is part of a gradual migration strategy where the minimum supported Go version will be three versions behind the latest release. With each new Go version release, we will bump the minimum version by one, ensuring compatibility while staying current with the Go ecosystem. + +### Stability Improvements + +This release includes several important stability fixes: +- Fixed a critical panic in the handoff worker manager that could occur when handling nil errors +- Improved test reliability for Smart Client Handoff functionality +- Fixed logging format issues that could cause runtime errors + +## ✨ New Features + +- OpenTelemetry metrics improvements for nil response handling ([#3638](https://github.com/redis/go-redis/pull/3638)) by [@fengve](https://github.com/fengve) + +## 🐛 Bug Fixes + +- Fixed panic on nil error in handoffWorkerManager closeConnFromRequest ([#3633](https://github.com/redis/go-redis/pull/3633)) by [@ccoVeille](https://github.com/ccoVeille) +- Fixed bad sprintf syntax in logging ([#3632](https://github.com/redis/go-redis/pull/3632)) by [@ccoVeille](https://github.com/ccoVeille) + +## 🧰 Maintenance + +- Updated minimum Go version to 1.21 ([#3640](https://github.com/redis/go-redis/pull/3640)) by [@ndyakov](https://github.com/ndyakov) +- Use Go 1.20 idiomatic string<->byte conversion ([#3435](https://github.com/redis/go-redis/pull/3435)) by [@justinhwang](https://github.com/justinhwang) +- Reduce flakiness of Smart Client Handoff test ([#3641](https://github.com/redis/go-redis/pull/3641)) by [@kiryazovi-redis](https://github.com/kiryazovi-redis) +- Revert PR #3634 (Observability metrics phase1) ([#3635](https://github.com/redis/go-redis/pull/3635)) by [@ofekshenawa](https://github.com/ofekshenawa) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@justinhwang](https://github.com/justinhwang), [@ndyakov](https://github.com/ndyakov), [@kiryazovi-redis](https://github.com/kiryazovi-redis), [@fengve](https://github.com/fengve), [@ccoVeille](https://github.com/ccoVeille), [@ofekshenawa](https://github.com/ofekshenawa) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0-beta.1...v9.18.0-beta.2 + +# 9.18.0-beta.1 (2025-12-01) + +## 🚀 Highlights + +### Request and Response Policy Based Routing in Cluster Mode + +This beta release introduces comprehensive support for Redis COMMAND-based request and response policy routing for cluster clients. This feature enables intelligent command routing and response aggregation based on Redis command metadata. + +**Key Features:** +- **Command Policy Loader**: Automatically parses and caches COMMAND metadata with routing/aggregation hints +- **Enhanced Routing Engine**: Supports all request policies including: + - `default(keyless)` - Commands without keys + - `default(hashslot)` - Commands with hash slot routing + - `all_shards` - Commands that need to run on all shards + - `all_nodes` - Commands that need to run on all nodes + - `multi_shard` - Commands that span multiple shards + - `special` - Commands with custom routing logic +- **Response Aggregator**: Intelligently combines multi-shard replies based on response policies: + - `all_succeeded` - All shards must succeed + - `one_succeeded` - At least one shard must succeed + - `agg_sum` - Aggregate numeric responses + - `special` - Custom aggregation logic (e.g., FT.CURSOR) +- **Raw Command Support**: Policies are enforced on `Client.Do(ctx, args...)` + +This feature is particularly useful for Redis Stack commands like RediSearch that need to operate across multiple shards in a cluster. + +### Connection Pool Improvements + +Fixed a critical defect in the connection pool's turn management mechanism that could lead to connection leaks under certain conditions. The fix ensures proper 1:1 correspondence between turns and connections. + +## ✨ New Features + +- Request and Response Policy Based Routing in Cluster Mode ([#3422](https://github.com/redis/go-redis/pull/3422)) by [@ofekshenawa](https://github.com/ofekshenawa) + +## 🐛 Bug Fixes + +- Fixed connection pool turn management to prevent connection leaks ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun) + +## 🧰 Maintenance + +- chore(deps): bump rojopolis/spellcheck-github-actions from 0.54.0 to 0.55.0 ([#3627](https://github.com/redis/go-redis/pull/3627)) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@cyningsun](https://github.com/cyningsun), [@ofekshenawa](https://github.com/ofekshenawa), [@ndyakov](https://github.com/ndyakov) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.18.0-beta.1 + +# 9.17.1 (2025-11-25) + +## 🐛 Bug Fixes + +- add wait to keyless commands list ([#3615](https://github.com/redis/go-redis/pull/3615)) by [@marcoferrer](https://github.com/marcoferrer) +- fix(time): remove cached time optimization ([#3611](https://github.com/redis/go-redis/pull/3611)) by [@ndyakov](https://github.com/ndyakov) + +## 🧰 Maintenance + +- chore(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 ([#3609](https://github.com/redis/go-redis/pull/3609)) +- chore(deps): bump actions/checkout from 5 to 6 ([#3610](https://github.com/redis/go-redis/pull/3610)) +- chore(script): fix help call in tag.sh ([#3606](https://github.com/redis/go-redis/pull/3606)) by [@ndyakov](https://github.com/ndyakov) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@marcoferrer](https://github.com/marcoferrer) and [@ndyakov](https://github.com/ndyakov) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.17.1 + +# 9.17.0 (2025-11-19) + +## 🚀 Highlights + +### Redis 8.4 Support +Added support for Redis 8.4, including new commands and features ([#3572](https://github.com/redis/go-redis/pull/3572)) + +### Typed Errors +Introduced typed errors for better error handling using `errors.As` instead of string checks. Errors can now be wrapped and set to commands in hooks without breaking library functionality ([#3602](https://github.com/redis/go-redis/pull/3602)) + +### New Commands +- **CAS/CAD Commands**: Added support for Compare-And-Set/Compare-And-Delete operations with conditional matching (`IFEQ`, `IFNE`, `IFDEQ`, `IFDNE`) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) +- **MSETEX**: Atomically set multiple key-value pairs with expiration options and conditional modes ([#3580](https://github.com/redis/go-redis/pull/3580)) +- **XReadGroup CLAIM**: Consume both incoming and idle pending entries from streams in a single call ([#3578](https://github.com/redis/go-redis/pull/3578)) +- **ACL Commands**: Added `ACLGenPass`, `ACLUsers`, and `ACLWhoAmI` ([#3576](https://github.com/redis/go-redis/pull/3576)) +- **SLOWLOG Commands**: Added `SLOWLOG LEN` and `SLOWLOG RESET` ([#3585](https://github.com/redis/go-redis/pull/3585)) +- **LATENCY Commands**: Added `LATENCY LATEST` and `LATENCY RESET` ([#3584](https://github.com/redis/go-redis/pull/3584)) + +### Search & Vector Improvements +- **Hybrid Search**: Added **EXPERIMENTAL** support for the new `FT.HYBRID` command ([#3573](https://github.com/redis/go-redis/pull/3573)) +- **Vector Range**: Added `VRANGE` command for vector sets ([#3543](https://github.com/redis/go-redis/pull/3543)) +- **FT.INFO Enhancements**: Added vector-specific attributes in FT.INFO response ([#3596](https://github.com/redis/go-redis/pull/3596)) + +### Connection Pool Improvements +- **Improved Connection Success Rate**: Implemented FIFO queue-based fairness and context pattern for connection creation to prevent premature cancellation under high concurrency ([#3518](https://github.com/redis/go-redis/pull/3518)) +- **Connection State Machine**: Resolved race conditions and improved pool performance with proper state tracking ([#3559](https://github.com/redis/go-redis/pull/3559)) +- **Pool Performance**: Significant performance improvements with faster semaphores, lockless hook manager, and reduced allocations (47-67% faster Get/Put operations) ([#3565](https://github.com/redis/go-redis/pull/3565)) + +### Metrics & Observability +- **Canceled Metric Attribute**: Added 'canceled' metrics attribute to distinguish context cancellation errors from other errors ([#3566](https://github.com/redis/go-redis/pull/3566)) + +## ✨ New Features + +- Typed errors with wrapping support ([#3602](https://github.com/redis/go-redis/pull/3602)) by [@ndyakov](https://github.com/ndyakov) +- CAS/CAD commands (marked as experimental) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) by [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis) +- MSETEX command support ([#3580](https://github.com/redis/go-redis/pull/3580)) by [@ofekshenawa](https://github.com/ofekshenawa) +- XReadGroup CLAIM argument ([#3578](https://github.com/redis/go-redis/pull/3578)) by [@ofekshenawa](https://github.com/ofekshenawa) +- ACL commands: GenPass, Users, WhoAmI ([#3576](https://github.com/redis/go-redis/pull/3576)) by [@destinyoooo](https://github.com/destinyoooo) +- SLOWLOG commands: LEN, RESET ([#3585](https://github.com/redis/go-redis/pull/3585)) by [@destinyoooo](https://github.com/destinyoooo) +- LATENCY commands: LATEST, RESET ([#3584](https://github.com/redis/go-redis/pull/3584)) by [@destinyoooo](https://github.com/destinyoooo) +- Hybrid search command (FT.HYBRID) ([#3573](https://github.com/redis/go-redis/pull/3573)) by [@htemelski-redis](https://github.com/htemelski-redis) +- Vector range command (VRANGE) ([#3543](https://github.com/redis/go-redis/pull/3543)) by [@cxljs](https://github.com/cxljs) +- Vector-specific attributes in FT.INFO ([#3596](https://github.com/redis/go-redis/pull/3596)) by [@ndyakov](https://github.com/ndyakov) +- Improved connection pool success rate with FIFO queue ([#3518](https://github.com/redis/go-redis/pull/3518)) by [@cyningsun](https://github.com/cyningsun) +- Canceled metrics attribute for context errors ([#3566](https://github.com/redis/go-redis/pull/3566)) by [@pvragov](https://github.com/pvragov) + +## 🐛 Bug Fixes + +- Fixed Failover Client MaintNotificationsConfig ([#3600](https://github.com/redis/go-redis/pull/3600)) by [@ajax16384](https://github.com/ajax16384) +- Fixed ACLGenPass function to use the bit parameter ([#3597](https://github.com/redis/go-redis/pull/3597)) by [@destinyoooo](https://github.com/destinyoooo) +- Return error instead of panic from commands ([#3568](https://github.com/redis/go-redis/pull/3568)) by [@dragneelfps](https://github.com/dragneelfps) +- Safety harness in `joinErrors` to prevent panic ([#3577](https://github.com/redis/go-redis/pull/3577)) by [@manisharma](https://github.com/manisharma) + +## ⚡ Performance + +- Connection state machine with race condition fixes ([#3559](https://github.com/redis/go-redis/pull/3559)) by [@ndyakov](https://github.com/ndyakov) +- Pool performance improvements: 47-67% faster Get/Put, 33% less memory, 50% fewer allocations ([#3565](https://github.com/redis/go-redis/pull/3565)) by [@ndyakov](https://github.com/ndyakov) + +## 🧪 Testing & Infrastructure + +- Updated to Redis 8.4.0 image ([#3603](https://github.com/redis/go-redis/pull/3603)) by [@ndyakov](https://github.com/ndyakov) +- Added Redis 8.4-RC1-pre to CI ([#3572](https://github.com/redis/go-redis/pull/3572)) by [@ndyakov](https://github.com/ndyakov) +- Refactored tests for idiomatic Go ([#3561](https://github.com/redis/go-redis/pull/3561), [#3562](https://github.com/redis/go-redis/pull/3562), [#3563](https://github.com/redis/go-redis/pull/3563)) by [@12ya](https://github.com/12ya) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@12ya](https://github.com/12ya), [@ajax16384](https://github.com/ajax16384), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@destinyoooo](https://github.com/destinyoooo), [@dragneelfps](https://github.com/dragneelfps), [@htemelski-redis](https://github.com/htemelski-redis), [@manisharma](https://github.com/manisharma), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@pvragov](https://github.com/pvragov) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.16.0...v9.17.0 + +# 9.16.0 (2025-10-23) + +## 🚀 Highlights + +### Maintenance Notifications Support + +This release introduces comprehensive support for Redis maintenance notifications, enabling applications to handle server maintenance events gracefully. The new `maintnotifications` package provides: + +- **RESP3 Push Notifications**: Full support for Redis RESP3 protocol push notifications +- **Connection Handoff**: Automatic connection migration during server maintenance with configurable retry policies and circuit breakers +- **Graceful Degradation**: Configurable timeout relaxation during maintenance windows to prevent false failures +- **Event-Driven Architecture**: Background workers with on-demand scaling for efficient handoff processing +- **Production-Ready**: Comprehensive E2E testing framework and monitoring capabilities + +For detailed usage examples and configuration options, see the [maintenance notifications documentation](maintnotifications/README.md). + +## ✨ New Features + +- **Trace Filtering**: Add support for filtering traces for specific commands, including pipeline operations and dial operations ([#3519](https://github.com/redis/go-redis/pull/3519), [#3550](https://github.com/redis/go-redis/pull/3550)) + - New `TraceCmdFilter` option to selectively trace commands + - Reduces overhead by excluding high-frequency or low-value commands from traces + +## 🐛 Bug Fixes + +- **Pipeline Error Handling**: Fix issue where pipeline repeatedly sets the same error ([#3525](https://github.com/redis/go-redis/pull/3525)) +- **Connection Pool**: Ensure re-authentication does not interfere with connection handoff operations ([#3547](https://github.com/redis/go-redis/pull/3547)) + +## 🔧 Improvements + +- **Hash Commands**: Update hash command implementations ([#3523](https://github.com/redis/go-redis/pull/3523)) +- **OpenTelemetry**: Use `metric.WithAttributeSet` to avoid unnecessary attribute copying in redisotel ([#3552](https://github.com/redis/go-redis/pull/3552)) + +## 📚 Documentation + +- **Cluster Client**: Add explanation for why `MaxRetries` is disabled for `ClusterClient` ([#3551](https://github.com/redis/go-redis/pull/3551)) + +## 🧪 Testing & Infrastructure + +- **E2E Testing**: Upgrade E2E testing framework with improved reliability and coverage ([#3541](https://github.com/redis/go-redis/pull/3541)) +- **Release Process**: Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530)) + +## 📦 Dependencies + +- Bump `rojopolis/spellcheck-github-actions` from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520)) +- Bump `github/codeql-action` from 3 to 4 ([#3544](https://github.com/redis/go-redis/pull/3544)) + +## 👥 Contributors + +We'd like to thank all the contributors who worked on this release! + +[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@Sovietaced](https://github.com/Sovietaced), [@Udhayarajan](https://github.com/Udhayarajan), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@Pika-Gopher](https://github.com/Pika-Gopher), [@cxljs](https://github.com/cxljs), [@huiyifyj](https://github.com/huiyifyj), [@omid-h70](https://github.com/omid-h70) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.14.0...v9.16.0 + + +# 9.15.0 was accidentally released. Please use version 9.16.0 instead. + +# 9.15.0-beta.3 (2025-09-26) + +## Highlights +This beta release includes a pre-production version of processing push notifications and hitless upgrades. + +# Changes + +- chore: Update hash_commands.go ([#3523](https://github.com/redis/go-redis/pull/3523)) + +## 🚀 New Features + +- feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418)) + +## 🐛 Bug Fixes + +- fix: pipeline repeatedly sets the error ([#3525](https://github.com/redis/go-redis/pull/3525)) + +## 🧰 Maintenance + +- chore(deps): bump rojopolis/spellcheck-github-actions from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520)) +- feat(e2e-testing): maintnotifications e2e and refactor ([#3526](https://github.com/redis/go-redis/pull/3526)) +- feat(tag.sh): Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@cxljs](https://github.com/cxljs), [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), and [@omid-h70](https://github.com/omid-h70) + + +# 9.15.0-beta.1 (2025-09-10) + +## Highlights +This beta release includes a pre-production version of processing push notifications and hitless upgrades. + +### Hitless Upgrades +Hitless upgrades is a major new feature that allows for zero-downtime upgrades in Redis clusters. +You can find more information in the [Hitless Upgrades documentation](https://github.com/redis/go-redis/tree/master/hitless). + +# Changes + +## 🚀 New Features +- [CAE-1088] & [CAE-1072] feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@ofekshenawa](https://github.com/ofekshenawa) + + +# 9.14.0 (2025-09-10) + +## Highlights +- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510)) + +# Changes + +## 🚀 New Features + +- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510)) + +## 🐛 Bug Fixes + +- fix: SetErr on Cmd if the command cannot be queued correctly in multi/exec ([#3509](https://github.com/redis/go-redis/pull/3509)) + +## 🧰 Maintenance + +- Updates release drafter config to exclude dependabot ([#3511](https://github.com/redis/go-redis/pull/3511)) +- chore(deps): bump actions/setup-go from 5 to 6 ([#3504](https://github.com/redis/go-redis/pull/3504)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@elena-kolevska](https://github.com/elena-kolevksa), [@htemelski-redis](https://github.com/htemelski-redis) and [@ndyakov](https://github.com/ndyakov) + + +# 9.13.0 (2025-09-03) + +## Highlights +- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496)) +- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470)) +- Fixes on Read and Write buffer sizes and UniversalOptions + +## Changes +- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496)) +- fix(test): fix a timing issue in pubsub test ([#3498](https://github.com/redis/go-redis/pull/3498)) +- Allow users to enable read-write splitting in failover mode. ([#3482](https://github.com/redis/go-redis/pull/3482)) +- Set the read/write buffer size of the sentinel client to 4KiB ([#3476](https://github.com/redis/go-redis/pull/3476)) + +## 🚀 New Features + +- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499)) +- Support subscriptions against cluster slave nodes ([#3480](https://github.com/redis/go-redis/pull/3480)) +- Add wait metrics to otel ([#3493](https://github.com/redis/go-redis/pull/3493)) +- Clean failing timeout implementation ([#3472](https://github.com/redis/go-redis/pull/3472)) + +## 🐛 Bug Fixes + +- Do not assume that all non-IP hosts are loopbacks ([#3085](https://github.com/redis/go-redis/pull/3085)) +- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470)) + +## 🧰 Maintenance + +- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499)) +- fix(make test): Add default env in makefile ([#3491](https://github.com/redis/go-redis/pull/3491)) +- Update the introduction to running tests in README.md ([#3495](https://github.com/redis/go-redis/pull/3495)) +- test: Add comprehensive edge case tests for IncrByFloat command ([#3477](https://github.com/redis/go-redis/pull/3477)) +- Set the default read/write buffer size of Redis connection to 32KiB ([#3483](https://github.com/redis/go-redis/pull/3483)) +- Bumps test image to 8.2.1-pre ([#3478](https://github.com/redis/go-redis/pull/3478)) +- fix UniversalOptions miss ReadBufferSize and WriteBufferSize options ([#3485](https://github.com/redis/go-redis/pull/3485)) +- chore(deps): bump actions/checkout from 4 to 5 ([#3484](https://github.com/redis/go-redis/pull/3484)) +- Removes dry run for stale issues policy ([#3471](https://github.com/redis/go-redis/pull/3471)) +- Update otel metrics URL ([#3474](https://github.com/redis/go-redis/pull/3474)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@LINKIWI](https://github.com/LINKIWI), [@cxljs](https://github.com/cxljs), [@cybersmeashish](https://github.com/cybersmeashish), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@suever](https://github.com/suever) + + +# 9.12.1 (2025-08-11) +## 🚀 Highlights +In the last version (9.12.0) the client introduced bigger write and read buffer sized. The default value we set was 512KiB. +However, users reported that this is too big for most use cases and can lead to high memory usage. +In this version the default value is changed to 256KiB. The `README.md` was updated to reflect the +correct default value and include a note that the default value can be changed. + +## 🐛 Bug Fixes + +- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468)) + +## 🧰 Maintenance + +- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468)) +- chore: update & fix otel example ([#3466](https://github.com/redis/go-redis/pull/3466)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@ndyakov](https://github.com/ndyakov) and [@vmihailenco](https://github.com/vmihailenco) + +# 9.12.0 (2025-08-05) + +## 🚀 Highlights + +- This release includes support for [Redis 8.2](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.2-release-notes/). +- Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands. +- Adds support for `EPSILON` option in `FT.VSIM`. +- Includes bug fixes and improvements contributed by the community related to ring and [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel). + +## Changes +- Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458)) +- chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459)) +- Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450)) +- feat: Add "skip_verify" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428)) +- fix: `errors.Join` requires Go 1.20 or later ([#3442](https://github.com/redis/go-redis/pull/3442)) +- DOC-4344 document quickstart examples ([#3426](https://github.com/redis/go-redis/pull/3426)) +- feat(bitop): add support for the new bitop operations ([#3409](https://github.com/redis/go-redis/pull/3409)) + +## 🚀 New Features + +- feat: recover addIdleConn may occur panic ([#2445](https://github.com/redis/go-redis/pull/2445)) +- feat(ring): specify custom health check func via HeartbeatFn option ([#2940](https://github.com/redis/go-redis/pull/2940)) +- Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436)) +- add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453)) +- Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449)) +- VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454)) +- Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444)) + +## 🐛 Bug Fixes + +- fix(redisotel): fix buggy append in reportPoolStats ([#3122](https://github.com/redis/go-redis/pull/3122)) +- fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457)) +- [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403)) + +## 🧰 Maintenance + +- Merges stale issues jobs into one job with two steps ([#3463](https://github.com/redis/go-redis/pull/3463)) +- improve code readability ([#3446](https://github.com/redis/go-redis/pull/3446)) +- chore(release): 9.12.0-beta.1 ([#3460](https://github.com/redis/go-redis/pull/3460)) +- DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443)) +- Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461)) +- bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@monkey92t](https://github.com/monkey92t), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn), [@smnvdev](https://github.com/smnvdev), [@strobil](https://github.com/strobil) and [@wzy9607](https://github.com/wzy9607) + +## New Contributors +* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in [#3409](https://github.com/redis/go-redis/pull/3409) +* [@smnvdev](https://github.com/smnvdev) made their first contribution in [#3403](https://github.com/redis/go-redis/pull/3403) +* [@rokn](https://github.com/rokn) made their first contribution in [#3444](https://github.com/redis/go-redis/pull/3444) + +# 9.11.0 (2025-06-24) + +## 🚀 Highlights + +Fixes TxPipeline to work correctly in cluster scenarios, allowing execution of commands +only in the same slot. + +# Changes + +## 🚀 New Features + +- Set cluster slot for `scan` commands, rather than random ([#2623](https://github.com/redis/go-redis/pull/2623)) +- Add CredentialsProvider field to UniversalOptions ([#2927](https://github.com/redis/go-redis/pull/2927)) +- feat(redisotel): add WithCallerEnabled option ([#3415](https://github.com/redis/go-redis/pull/3415)) + +## 🐛 Bug Fixes + +- fix(txpipeline): keyless commands should take the slot of the keyed ([#3411](https://github.com/redis/go-redis/pull/3411)) +- fix(loading): cache the loaded flag for slave nodes ([#3410](https://github.com/redis/go-redis/pull/3410)) +- fix(txpipeline): should return error on multi/exec on multiple slots ([#3408](https://github.com/redis/go-redis/pull/3408)) +- fix: check if the shard exists to avoid returning nil ([#3396](https://github.com/redis/go-redis/pull/3396)) + +## 🧰 Maintenance + +- feat: optimize connection pool waitTurn ([#3412](https://github.com/redis/go-redis/pull/3412)) +- chore(ci): update CI redis builds ([#3407](https://github.com/redis/go-redis/pull/3407)) +- chore: remove a redundant method from `Ring`, `Client` and `ClusterClient` ([#3401](https://github.com/redis/go-redis/pull/3401)) +- test: refactor TestBasicCredentials using table-driven tests ([#3406](https://github.com/redis/go-redis/pull/3406)) +- perf: reduce unnecessary memory allocation operations ([#3399](https://github.com/redis/go-redis/pull/3399)) +- fix: insert entry during iterating over a map ([#3398](https://github.com/redis/go-redis/pull/3398)) +- DOC-5229 probabilistic data type examples ([#3413](https://github.com/redis/go-redis/pull/3413)) +- chore(deps): bump rojopolis/spellcheck-github-actions from 0.49.0 to 0.51.0 ([#3414](https://github.com/redis/go-redis/pull/3414)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@andy-stark-redis](https://github.com/andy-stark-redis), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@cxljs](https://github.com/cxljs), [@dcherubini](https://github.com/dcherubini), [@dependabot[bot]](https://github.com/apps/dependabot), [@iamamirsalehi](https://github.com/iamamirsalehi), [@ndyakov](https://github.com/ndyakov), [@pete-woods](https://github.com/pete-woods), [@twz915](https://github.com/twz915) and [dependabot[bot]](https://github.com/apps/dependabot) + # 9.10.0 (2025-06-06) ## 🚀 Highlights @@ -161,3 +925,139 @@ For a complete list of changes, see the [full changelog](https://github.com/redi We would like to thank all the contributors who made this release possible: [@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi) + + +# Old Changelog +## Unreleased + +### Changed + +* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) + Users can use the OpenTelemetry sampler to control the sampling behavior. + For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep + a similar behavior (drop orphan spans) of `go-redis` as before. + +## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) + + +### Features + +* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602)) +* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe)) +* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af)) + + + +## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01) + + +### Bug Fixes + +* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241)) + + +### Features + +* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e)) +* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8)) +* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af)) + + + +## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02) + +### New Features + +- feat(scan): scan time.Time sets the default decoding (#2413) +- Add support for CLUSTER LINKS command (#2504) +- Add support for acl dryrun command (#2502) +- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500) +- Add support for LCS Command (#2480) +- Add support for BZMPOP (#2456) +- Adding support for ZMPOP command (#2408) +- Add support for LMPOP (#2440) +- feat: remove pool unused fields (#2438) +- Expiretime and PExpireTime (#2426) +- Implement `FUNCTION` group of commands (#2475) +- feat(zadd): add ZAddLT and ZAddGT (#2429) +- Add: Support for COMMAND LIST command (#2491) +- Add support for BLMPOP (#2442) +- feat: check pipeline.Do to prevent confusion with Exec (#2517) +- Function stats, function kill, fcall and fcall_ro (#2486) +- feat: Add support for CLUSTER SHARDS command (#2507) +- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498) + +### Fixed + +- fix: eval api cmd.SetFirstKeyPos (#2501) +- fix: limit the number of connections created (#2441) +- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479) +- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458) +- fix: group lag can be null (#2448) + +### Maintenance + +- Updating to the latest version of redis (#2508) +- Allowing for running tests on a port other than the fixed 6380 (#2466) +- redis 7.0.8 in tests (#2450) +- docs: Update redisotel example for v9 (#2425) +- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476) +- chore: add Chinese translation (#2436) +- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421) +- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420) +- chore(deps): bump actions/setup-go from 3 to 4 (#2495) +- docs: add instructions for the HSet api (#2503) +- docs: add reading lag field comment (#2451) +- test: update go mod before testing(go mod tidy) (#2423) +- docs: fix comment typo (#2505) +- test: remove testify (#2463) +- refactor: change ListElementCmd to KeyValuesCmd. (#2443) +- fix(appendArg): appendArg case special type (#2489) + +## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01) + +### Features + +* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65)) + +## v9 2023-01-30 + +### Breaking + +- Changed Pipelines to not be thread-safe any more. + +### Added + +- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was + contributed by @monkey92t who has done the majority of work in this release. +- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts + and deadlines. See + [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details. +- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example, + `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`. +- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See + [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html) +- Added `redis.HasErrorPrefix` to help working with errors. + +### Changed + +- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is + completely gone in v9. +- Reworked hook interface and added `DialHook`. +- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See + [example](example/otel) and + [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html). +- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making + an allocation. +- Renamed the option `MaxConnAge` to `ConnMaxLifetime`. +- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`. +- Removed connection reaper in favor of `MaxIdleConns`. +- Removed `WithContext` since `context.Context` can be passed directly as an arg. +- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and + it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to + reset commands for some reason. + +### Fixed + +- Improved and fixed pipeline retries. +- As usually, added support for more commands and fixed some bugs. diff --git a/vendor/github.com/redis/go-redis/v9/RELEASING.md b/vendor/github.com/redis/go-redis/v9/RELEASING.md index 1115db4e3e5..033ec100bec 100644 --- a/vendor/github.com/redis/go-redis/v9/RELEASING.md +++ b/vendor/github.com/redis/go-redis/v9/RELEASING.md @@ -1,15 +1,146 @@ # Releasing -1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub: +This document is the runbook for cutting a go-redis release. It is intended +for maintainers with write/tag access to the repository. + +For the format and style of the release notes themselves, see +[.github/RELEASE_NOTES_TEMPLATE.md](./.github/RELEASE_NOTES_TEMPLATE.md). + +## Versioning + +go-redis follows [Semantic Versioning](https://semver.org/): + +- **Patch** (`vX.Y.Z+1`) — bug fixes, no API changes. +- **Minor** (`vX.Y+1.0`) — backwards-compatible new features, deprecations. +- **Major** (`vX+1.0.0`) — breaking changes. Coordinate with the team first. + +Pre-releases use `vX.Y.Z-beta.N` / `vX.Y.Z-rc.N`. + +## Pre-release checklist + +- [ ] Target branch is `master` and CI is green on the latest commit. +- [ ] All PRs intended for this release are merged. +- [ ] There are no open issues in the release milestone (if used). +- [ ] `CHANGELOG` / release notes have been considered; dependabot-only + and doc-only changes are excluded per the template. +- [ ] Confirm the next version number and decide if it's a patch / minor / major. + +## 1. Draft the release notes + +1. Open the draft release auto-generated by + [release-drafter](.github/release-drafter-config.yml) on GitHub. +2. Prepend a new section to [`RELEASE-NOTES.md`](./RELEASE-NOTES.md) using + [`.github/RELEASE_NOTES_TEMPLATE.md`](./.github/RELEASE_NOTES_TEMPLATE.md) + as the format. Keep the file in chronological order (newest first). +3. Pick 3–5 **Highlights** — the most user-facing, impactful changes. +4. Remove dependabot bumps and doc-only typo fixes from the lists. +5. Verify every PR has a contributor attribution and link. +6. Open a PR with just the release-notes change if you want review before + bumping versions, otherwise include it in the release PR below. + +## 2. Bump versions and open the release PR + +Create a release branch from `master`: + +```shell +git checkout master && git pull --ff-only +git checkout -b release/vX.Y.Z +``` + +Run the release script on that branch: ```shell -TAG=v1.0.0 ./scripts/release.sh +TAG=vX.Y.Z ./scripts/release.sh ``` -2. Open a pull request and wait for the build to finish. +What the script does (and explicitly does **not** do): -3. Merge the pull request and run `tag.sh` to create tags for packages: +- ✅ Validates `TAG` matches the semver regex and isn't already a git tag. +- ✅ Rewrites every `redis/go-redis*` line in every sub-module `go.mod` to + point at the new `TAG`. Trailing `// indirect` markers are preserved. +- ✅ Runs `go mod tidy -compat=1.24` in each sub-module. +- ✅ Updates the return value in [`version.go`](./version.go). +- ❌ Does **not** switch branches (runs in your current branch). +- ❌ Does **not** require a clean working tree (so you can mix it with + release-notes edits in the same branch). +- ❌ Does **not** commit, tag, or push anything. + +Review and commit the changes yourself: ```shell -TAG=v1.0.0 ./scripts/tag.sh +git diff # sanity-check the bumps +git add -u +git commit -m "chore: release vX.Y.Z" +git push origin release/vX.Y.Z ``` + +Then on GitHub: + +- [ ] Open a PR from `release/vX.Y.Z` into `master`. +- [ ] Wait for all required CI checks (build, golangci-lint, spellcheck, + doctests, e2e where applicable) to pass. +- [ ] Get at least one maintainer approval. +- [ ] Merge the PR (use a merge commit — the tag will point at the merge SHA). + +## 3. Tag the release + +After the release PR is merged, pull the latest `master` and dry-run the +tagger: + +```shell +git checkout master && git pull --ff-only +TAG=vX.Y.Z ./scripts/tag.sh vX.Y.Z +``` + +The script defaults to **dry-run** and prints the commands it would run. +Verify the output, then apply for real with `-t`: + +```shell +./scripts/tag.sh vX.Y.Z -t +``` + +This creates and pushes: +- The top-level tag `vX.Y.Z`. +- A per-module tag `/vX.Y.Z` for each public sub-module + (skipping `example/*` and `internal/*`). + +## 4. Publish the GitHub release + +1. On GitHub, open the draft release created by release-drafter. +2. Set the tag to `vX.Y.Z` and the target to `master`. +3. Replace the auto-generated body with the curated notes from + `RELEASE-NOTES.md` for this version. +4. For pre-releases, check **"Set as a pre-release"**. +5. Publish. + +## 5. Post-release + +- [ ] Verify the release appears on + [pkg.go.dev](https://pkg.go.dev/github.com/redis/go-redis/v9) within + a few minutes (trigger a fetch by visiting the version URL if needed). +- [ ] Announce on Discord (see the link in `CONTRIBUTING.md`). +- [ ] Close the release milestone if one was used. +- [ ] Open follow-up issues for anything deferred from this release. + +## Hotfix / patch release + +For an urgent fix on top of the latest release: + +1. Branch from the latest release tag: `git checkout -b hotfix/vX.Y.Z+1 vX.Y.Z`. +2. Cherry-pick (or re-apply) only the required fix commits. +3. Follow the normal release flow above with `TAG=vX.Y.Z+1`. +4. Make sure the fix is also present on `master` (forward-port if necessary). + +## Troubleshooting + +- **`release.sh` fails with "tag already exists"** — the tag has already + been created. Pick the next version, or delete the local tag first if + it was created by mistake. +- **`tag.sh` reports version mismatch in a `go.mod`** — a sub-module was + not updated by `release.sh`. Fix the `go.mod` manually (or re-run + `release.sh`), amend the release PR, and re-run the tagger. +- **`version.go` does not contain the tag** — `release.sh` did not run or + the bump was reverted. Re-run `release.sh` on the release branch. +- **pkg.go.dev does not show the new version** — visit + `https://pkg.go.dev/github.com/redis/go-redis/v9@vX.Y.Z` once to trigger + a fetch from the module proxy. diff --git a/vendor/github.com/redis/go-redis/v9/acl_commands.go b/vendor/github.com/redis/go-redis/v9/acl_commands.go index 9cb800bb3b5..0a8a195ceb2 100644 --- a/vendor/github.com/redis/go-redis/v9/acl_commands.go +++ b/vendor/github.com/redis/go-redis/v9/acl_commands.go @@ -8,8 +8,12 @@ type ACLCmdable interface { ACLLog(ctx context.Context, count int64) *ACLLogCmd ACLLogReset(ctx context.Context) *StatusCmd + ACLGenPass(ctx context.Context, bit int) *StringCmd + ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd ACLDelUser(ctx context.Context, username string) *IntCmd + ACLUsers(ctx context.Context) *StringSliceCmd + ACLWhoAmI(ctx context.Context) *StringCmd ACLList(ctx context.Context) *StringSliceCmd ACLCat(ctx context.Context) *StringSliceCmd @@ -65,6 +69,29 @@ func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...strin return cmd } +func (c cmdable) ACLGenPass(ctx context.Context, bit int) *StringCmd { + args := make([]interface{}, 0, 3) + args = append(args, "acl", "genpass") + if bit > 0 { + args = append(args, bit) + } + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLUsers(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "users") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLWhoAmI(ctx context.Context) *StringCmd { + cmd := NewStringCmd(ctx, "acl", "whoami") + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "acl", "list") _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/adapters.go b/vendor/github.com/redis/go-redis/v9/adapters.go new file mode 100644 index 00000000000..952a4c2660c --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/adapters.go @@ -0,0 +1,118 @@ +package redis + +import ( + "context" + "errors" + "net" + "time" + + "github.com/redis/go-redis/v9/internal/interfaces" + "github.com/redis/go-redis/v9/push" +) + +// ErrInvalidCommand is returned when an invalid command is passed to ExecuteCommand. +var ErrInvalidCommand = errors.New("invalid command type") + +// ErrInvalidPool is returned when the pool type is not supported. +var ErrInvalidPool = errors.New("invalid pool type") + +// newClientAdapter creates a new client adapter for regular Redis clients. +func newClientAdapter(client *baseClient) interfaces.ClientInterface { + return &clientAdapter{client: client} +} + +// clientAdapter adapts a Redis client to implement interfaces.ClientInterface. +type clientAdapter struct { + client *baseClient +} + +// GetOptions returns the client options. +func (ca *clientAdapter) GetOptions() interfaces.OptionsInterface { + return &optionsAdapter{options: ca.client.opt} +} + +// GetPushProcessor returns the client's push notification processor. +func (ca *clientAdapter) GetPushProcessor() interfaces.NotificationProcessor { + return &pushProcessorAdapter{processor: ca.client.pushProcessor} +} + +// optionsAdapter adapts Redis options to implement interfaces.OptionsInterface. +type optionsAdapter struct { + options *Options +} + +// GetReadTimeout returns the read timeout. +func (oa *optionsAdapter) GetReadTimeout() time.Duration { + return oa.options.ReadTimeout +} + +// GetWriteTimeout returns the write timeout. +func (oa *optionsAdapter) GetWriteTimeout() time.Duration { + return oa.options.WriteTimeout +} + +// GetNetwork returns the network type. +func (oa *optionsAdapter) GetNetwork() string { + return oa.options.Network +} + +// GetAddr returns the connection address. +func (oa *optionsAdapter) GetAddr() string { + return oa.options.Addr +} + +// GetNodeAddress returns the address of the Redis node as reported by the server. +// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation. +// For standalone clients, this defaults to Addr. +func (oa *optionsAdapter) GetNodeAddress() string { + return oa.options.NodeAddress +} + +// IsTLSEnabled returns true if TLS is enabled. +func (oa *optionsAdapter) IsTLSEnabled() bool { + return oa.options.TLSConfig != nil +} + +// GetProtocol returns the protocol version. +func (oa *optionsAdapter) GetProtocol() int { + return oa.options.Protocol +} + +// GetPoolSize returns the connection pool size. +func (oa *optionsAdapter) GetPoolSize() int { + return oa.options.PoolSize +} + +// NewDialer returns a new dialer function for the connection. +func (oa *optionsAdapter) NewDialer() func(context.Context) (net.Conn, error) { + baseDialer := oa.options.NewDialer() + return func(ctx context.Context) (net.Conn, error) { + // Extract network and address from the options + network := oa.options.Network + addr := oa.options.Addr + return baseDialer(ctx, network, addr) + } +} + +// pushProcessorAdapter adapts a push.NotificationProcessor to implement interfaces.NotificationProcessor. +type pushProcessorAdapter struct { + processor push.NotificationProcessor +} + +// RegisterHandler registers a handler for a specific push notification name. +func (ppa *pushProcessorAdapter) RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error { + if pushHandler, ok := handler.(push.NotificationHandler); ok { + return ppa.processor.RegisterHandler(pushNotificationName, pushHandler, protected) + } + return errors.New("handler must implement push.NotificationHandler") +} + +// UnregisterHandler removes a handler for a specific push notification name. +func (ppa *pushProcessorAdapter) UnregisterHandler(pushNotificationName string) error { + return ppa.processor.UnregisterHandler(pushNotificationName) +} + +// GetHandler returns the handler for a specific push notification name. +func (ppa *pushProcessorAdapter) GetHandler(pushNotificationName string) interface{} { + return ppa.processor.GetHandler(pushNotificationName) +} diff --git a/vendor/github.com/redis/go-redis/v9/array_commands.go b/vendor/github.com/redis/go-redis/v9/array_commands.go new file mode 100644 index 00000000000..71037dfd63f --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/array_commands.go @@ -0,0 +1,387 @@ +package redis + +import ( + "context" +) + +// note: the APIs is experimental and may be subject to change. +// +// ArrayCmdable defines the interface for Redis Array data structure commands +// available in Redis 8.8.0+. +// +// Redis array supports index range [0, math.MaxUint64-1), so index parameters use uint64. +type ArrayCmdable interface { + ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd + ARGet(ctx context.Context, key string, index uint64) *StringCmd + ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd + ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd + ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd + ARInsert(ctx context.Context, key string, values ...string) *UintCmd + ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd + ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd + ARLen(ctx context.Context, key string) *UintCmd + ARCount(ctx context.Context, key string) *UintCmd + ARNext(ctx context.Context, key string) *UintCmd + ARSeek(ctx context.Context, key string, index uint64) *IntCmd + ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd + ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd + ARScan(ctx context.Context, key string, start, end uint64, args *ARScanArgs) *AREntrySliceCmd + AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd + AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd + AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd + AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd + AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd + AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd + AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd + AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd + ARGrep(ctx context.Context, key string, start, end string, args *ARGrepArgs) *UintSliceCmd + ARGrepWithValues(ctx context.Context, key string, start, end string, args *ARGrepArgs) *AREntrySliceCmd + ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd + ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd +} + +// AREntry represents an index-value pair for ARMSET. +type AREntry struct { + Index uint64 + Value string +} + +// ARRange represents a start-end range for ARDELRANGE. +type ARRange struct { + Start uint64 + End uint64 +} + +// ARScanArgs contains optional arguments for ARSCAN. +type ARScanArgs struct { + Limit uint64 +} + +// ARGrepPredicateType defines the type of predicate for ARGREP. +type ARGrepPredicateType string + +const ( + ARGrepExact ARGrepPredicateType = "EXACT" + ARGrepMatch ARGrepPredicateType = "MATCH" + ARGrepGlob ARGrepPredicateType = "GLOB" + ARGrepRegex ARGrepPredicateType = "RE" +) + +// ARGrepPredicate represents a search predicate for ARGREP. +type ARGrepPredicate struct { + Type ARGrepPredicateType + Value string +} + +// ARGrepArgs contains optional arguments for ARGREP. +// Redis ARGREP defaults to OR when multiple predicates are given. +// Set CombineAnd to true to combine predicates with AND instead. +type ARGrepArgs struct { + Predicates []ARGrepPredicate + CombineAnd bool + Limit uint64 + NoCase bool +} + +// ARSet sets one or more contiguous values starting at an index in an array. +// Returns the number of new slots that were set (previously empty). +func (c cmdable) ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd { + args := make([]any, 3, 3+len(values)) + args[0] = "arset" + args[1] = key + args[2] = index + for _, v := range values { + args = append(args, v) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARGet gets the value at an index in an array. +// Returns redis.Nil if the key or index does not exist. +func (c cmdable) ARGet(ctx context.Context, key string, index uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arget", key, index) + _ = c(ctx, cmd) + return cmd +} + +// ARGetRange gets values in a range of indexes. +// Returns values in the range, with nil for unset indexes. +func (c cmdable) ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd { + cmd := NewSliceCmd(ctx, "argetrange", key, start, end) + _ = c(ctx, cmd) + return cmd +} + +// ARMGet gets values at multiple indexes in an array. +// Returns values at the specified indexes, with nil for unset indexes. +func (c cmdable) ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd { + args := make([]any, 2+len(indexes)) + args[0] = "armget" + args[1] = key + for i, idx := range indexes { + args[2+i] = idx + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARMSet sets multiple index-value pairs in an array. +// Returns the number of new slots that were set (previously empty). +func (c cmdable) ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd { + args := make([]any, 2, 2+2*len(members)) + args[0] = "armset" + args[1] = key + for _, m := range members { + args = append(args, m.Index, m.Value) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARInsert inserts one or more values at consecutive indexes. +// Returns the last index where a value was inserted. +func (c cmdable) ARInsert(ctx context.Context, key string, values ...string) *UintCmd { + args := make([]any, 2, 2+len(values)) + args[0] = "arinsert" + args[1] = key + for _, v := range values { + args = append(args, v) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARDel deletes elements at the specified indexes in an array. +// Returns the number of elements deleted. +func (c cmdable) ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd { + args := make([]any, 2+len(indexes)) + args[0] = "ardel" + args[1] = key + for i, idx := range indexes { + args[2+i] = idx + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARDelRange deletes elements in one or more ranges. +// Returns the number of elements deleted. +func (c cmdable) ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd { + args := make([]any, 2, 2+2*len(ranges)) + args[0] = "ardelrange" + args[1] = key + for _, r := range ranges { + args = append(args, r.Start, r.End) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARLen returns the length of an array (max index + 1). +// Returns 0 if the key does not exist. +func (c cmdable) ARLen(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arlen", key) + _ = c(ctx, cmd) + return cmd +} + +// ARCount returns the number of non-empty elements in an array. +// Returns 0 if the key does not exist. +func (c cmdable) ARCount(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arcount", key) + _ = c(ctx, cmd) + return cmd +} + +// ARNext returns the next index ARINSERT would use. +// Returns 0 for missing keys or when no insert happened yet. +// Returns nil when the insertion cursor is exhausted / would overflow. +func (c cmdable) ARNext(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arnext", key) + _ = c(ctx, cmd) + return cmd +} + +// ARSeek sets the ARINSERT / ARRING cursor to a specific index. +// Returns 1 if the cursor was set, 0 if the key does not exist. +func (c cmdable) ARSeek(ctx context.Context, key string, index uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arseek", key, index) + _ = c(ctx, cmd) + return cmd +} + +// ARInfo returns metadata about an array. +func (c cmdable) ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key) + _ = c(ctx, cmd) + return cmd +} + +// ARInfoFull returns detailed metadata about an array including slice statistics. +func (c cmdable) ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key, "full") + _ = c(ctx, cmd) + return cmd +} + +// ARScan iterates existing elements in a range, returning index-value pairs. +func (c cmdable) ARScan(ctx context.Context, key string, start, end uint64, scanArgs *ARScanArgs) *AREntrySliceCmd { + args := make([]any, 4, 6) + args[0], args[1], args[2], args[3] = "arscan", key, start, end + if scanArgs != nil && scanArgs.Limit > 0 { + args = append(args, "limit", scanArgs.Limit) + } + cmd := NewAREntrySliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// AROpSum returns the sum of numeric elements in a range. +func (c cmdable) AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "SUM") + _ = c(ctx, cmd) + return cmd +} + +// AROpMin returns the minimum numeric element in a range. +func (c cmdable) AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "MIN") + _ = c(ctx, cmd) + return cmd +} + +// AROpMax returns the maximum numeric element in a range. +func (c cmdable) AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "MAX") + _ = c(ctx, cmd) + return cmd +} + +// AROpAnd returns the bitwise AND of integer elements in a range. +func (c cmdable) AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "AND") + _ = c(ctx, cmd) + return cmd +} + +// AROpOr returns the bitwise OR of integer elements in a range. +func (c cmdable) AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "OR") + _ = c(ctx, cmd) + return cmd +} + +// AROpXor returns the bitwise XOR of integer elements in a range. +func (c cmdable) AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "XOR") + _ = c(ctx, cmd) + return cmd +} + +// AROpMatch returns the count of elements matching a target string in a range. +func (c cmdable) AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "MATCH", value) + _ = c(ctx, cmd) + return cmd +} + +// AROpUsed returns the count of non-empty slots in a range. +func (c cmdable) AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "USED") + _ = c(ctx, cmd) + return cmd +} + +// ARGrep searches array elements in a range using textual predicates. +// Returns matching indexes only. Use ARGrepWithValues to also get the values. +func (c cmdable) ARGrep(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *UintSliceCmd { + args := make([]any, 4, 4+grepArgs.Len()) + args[0], args[1], args[2], args[3] = "argrep", key, start, end + args = grepArgs.Append(args) + cmd := NewUintSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARGrepWithValues searches array elements in a range using textual predicates. +// Returns matching indexes and their values as index-value pairs. +func (c cmdable) ARGrepWithValues(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *AREntrySliceCmd { + args := make([]any, 4, 5+grepArgs.Len()) + args[0], args[1], args[2], args[3] = "argrep", key, start, end + args = grepArgs.Append(args) + args = append(args, "withvalues") + cmd := NewAREntrySliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (args *ARGrepArgs) Len() int { + if args == nil { + return 0 + } + n := 2 * len(args.Predicates) + if args.CombineAnd { + n++ + } + if args.Limit > 0 { + n += 2 + } + if args.NoCase { + n++ + } + return n +} + +func (args *ARGrepArgs) Append(a []any) []any { + if args == nil { + return a + } + for _, p := range args.Predicates { + a = append(a, string(p.Type), p.Value) + } + if args.CombineAnd { + a = append(a, "and") + } + if args.Limit > 0 { + a = append(a, "limit", args.Limit) + } + if args.NoCase { + a = append(a, "nocase") + } + return a +} + +// ARRing inserts values into a ring buffer of specified size, wrapping and truncating as needed. +// Returns the last index where a value was inserted. +func (c cmdable) ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd { + args := make([]any, 3, 3+len(values)) + args[0] = "arring" + args[1] = key + args[2] = size + for _, v := range values { + args = append(args, v) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARLastItems returns the most recently inserted elements. +// When rev is true, returns items in reverse order. +func (c cmdable) ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd { + args := make([]any, 3, 4) + args[0], args[1], args[2] = "arlastitems", key, count + if rev { + args = append(args, "rev") + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/auth/auth.go b/vendor/github.com/redis/go-redis/v9/auth/auth.go index 1f5c8022485..21667a1284b 100644 --- a/vendor/github.com/redis/go-redis/v9/auth/auth.go +++ b/vendor/github.com/redis/go-redis/v9/auth/auth.go @@ -9,12 +9,30 @@ type StreamingCredentialsProvider interface { // Subscribe subscribes to the credentials provider for updates. // It returns the current credentials, a cancel function to unsubscribe from the provider, // and an error if any. + // + // Implementations MUST be idempotent with respect to listener identity: + // subscribing the same listener value more than once must not produce + // duplicate notifications and must not create multiple independent + // subscriptions that each need to be cancelled separately. Every + // UnsubscribeFunc returned for a given listener must cancel that + // listener's subscription; calling any one of them must be sufficient to + // stop updates to that listener, and calling subsequent ones must be a + // safe no-op. Callers (including go-redis internals) may retain only + // the most recently returned UnsubscribeFunc and rely on it to fully + // unsubscribe the listener. + // // TODO(ndyakov): Should we add context to the Subscribe method? Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) } // UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. // It is used to unsubscribe from the provider when the credentials are no longer needed. +// +// Per the StreamingCredentialsProvider.Subscribe contract, if the same +// listener is subscribed multiple times, every UnsubscribeFunc returned for +// that listener must fully unsubscribe it on first invocation, and +// subsequent invocations (from any of the equivalent UnsubscribeFuncs) must +// be a safe no-op. type UnsubscribeFunc func() error // CredentialsListener is an interface that defines the methods for a credentials listener. diff --git a/vendor/github.com/redis/go-redis/v9/bitmap_commands.go b/vendor/github.com/redis/go-redis/v9/bitmap_commands.go index a215582890e..86aa9b7efc6 100644 --- a/vendor/github.com/redis/go-redis/v9/bitmap_commands.go +++ b/vendor/github.com/redis/go-redis/v9/bitmap_commands.go @@ -12,6 +12,10 @@ type BitMapCmdable interface { BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpNot(ctx context.Context, destKey string, key string) *IntCmd BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd @@ -78,22 +82,50 @@ func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) return cmd } +// BitOpAnd creates a new bitmap in which users are members of all given bitmaps func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "and", destKey, keys...) } +// BitOpOr creates a new bitmap in which users are member of at least one given bitmap func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "or", destKey, keys...) } +// BitOpXor creates a new bitmap in which users are the result of XORing all given bitmaps func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "xor", destKey, keys...) } +// BitOpNot creates a new bitmap in which users are not members of a given bitmap func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd { return c.bitOp(ctx, "not", destKey, key) } +// BitOpDiff creates a new bitmap in which users are members of bitmap X but not of any of bitmaps Y1, Y2, … +// Introduced with Redis 8.2 +func (c cmdable) BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "diff", destKey, keys...) +} + +// BitOpDiff1 creates a new bitmap in which users are members of one or more of bitmaps Y1, Y2, … but not members of bitmap X +// Introduced with Redis 8.2 +func (c cmdable) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "diff1", destKey, keys...) +} + +// BitOpAndOr creates a new bitmap in which users are members of bitmap X and also members of one or more of bitmaps Y1, Y2, … +// Introduced with Redis 8.2 +func (c cmdable) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "andor", destKey, keys...) +} + +// BitOpOne creates a new bitmap in which users are members of exactly one of the given bitmaps +// Introduced with Redis 8.2 +func (c cmdable) BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "one", destKey, keys...) +} + // BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end // if you need the `byte | bit` parameter, please use `BitPosSpan`. func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd { @@ -109,7 +141,9 @@ func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64 args[3] = pos[0] args[4] = pos[1] default: - panic("too many arguments") + cmd := NewIntCmd(ctx) + cmd.SetErr(errors.New("too many arguments")) + return cmd } cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) @@ -150,7 +184,9 @@ func (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface args[0] = "BITFIELD_RO" args[1] = key if len(values)%2 != 0 { - panic("BitFieldRO: invalid number of arguments, must be even") + c := NewIntSliceCmd(ctx) + c.SetErr(errors.New("BitFieldRO: invalid number of arguments, must be even")) + return c } for i := 0; i < len(values); i += 2 { args = append(args, "GET", values[i], values[i+1]) diff --git a/vendor/github.com/redis/go-redis/v9/cluster_commands.go b/vendor/github.com/redis/go-redis/v9/cluster_commands.go index 4857b01eaa5..a02683f2075 100644 --- a/vendor/github.com/redis/go-redis/v9/cluster_commands.go +++ b/vendor/github.com/redis/go-redis/v9/cluster_commands.go @@ -42,6 +42,9 @@ func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd { return cmd } +// ClusterSlots returns the mapping of cluster slots to nodes. +// +// Deprecated: Use ClusterShards instead as of Redis 7.0.0. func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd { cmd := NewClusterSlotsCmd(ctx, "cluster", "slots") _ = c(ctx, cmd) @@ -153,6 +156,9 @@ func (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd { return cmd } +// ClusterSlaves lists the replica nodes of a master node. +// +// Deprecated: Use ClusterReplicas instead as of Redis 5.0.0. func (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "cluster", "slaves", nodeID) _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/command.go b/vendor/github.com/redis/go-redis/v9/command.go index 56b2257214f..57a26c8ba88 100644 --- a/vendor/github.com/redis/go-redis/v9/command.go +++ b/vendor/github.com/redis/go-redis/v9/command.go @@ -4,6 +4,8 @@ import ( "bufio" "context" "fmt" + "io" + "maps" "net" "regexp" "strconv" @@ -14,9 +16,186 @@ import ( "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hscan" "github.com/redis/go-redis/v9/internal/proto" + "github.com/redis/go-redis/v9/internal/routing" "github.com/redis/go-redis/v9/internal/util" ) +// keylessCommands contains Redis commands that have empty key specifications (9th slot empty) +// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands +var keylessCommands = map[string]struct{}{ + "acl": {}, + "asking": {}, + "auth": {}, + "bgrewriteaof": {}, + "bgsave": {}, + "client": {}, + "cluster": {}, + "config": {}, + "debug": {}, + "discard": {}, + "echo": {}, + "exec": {}, + "failover": {}, + "function": {}, + "hello": {}, + "hotkeys": {}, + "latency": {}, + "lolwut": {}, + "module": {}, + "monitor": {}, + "multi": {}, + "pfselftest": {}, + "ping": {}, + "psubscribe": {}, + "psync": {}, + "publish": {}, + "pubsub": {}, + "punsubscribe": {}, + "quit": {}, + "readonly": {}, + "readwrite": {}, + "replconf": {}, + "replicaof": {}, + "role": {}, + "save": {}, + "script": {}, + "select": {}, + "shutdown": {}, + "slaveof": {}, + "slowlog": {}, + "subscribe": {}, + "swapdb": {}, + "sync": {}, + "time": {}, + "unsubscribe": {}, + "unwatch": {}, + "wait": {}, +} + +// CmdTyper interface for getting command type +type CmdTyper interface { + GetCmdType() CmdType +} + +// CmdTypeGetter interface for getting command type without circular imports +type CmdTypeGetter interface { + GetCmdType() CmdType +} + +type CmdType uint8 + +const ( + CmdTypeGeneric CmdType = iota + CmdTypeString + CmdTypeInt + CmdTypeBool + CmdTypeFloat + CmdTypeStringSlice + CmdTypeIntSlice + CmdTypeFloatSlice + CmdTypeBoolSlice + CmdTypeMapStringString + CmdTypeMapStringInt + CmdTypeMapStringInterface + CmdTypeMapStringInterfaceSlice + CmdTypeSlice + CmdTypeStatus + CmdTypeDuration + CmdTypeTime + CmdTypeKeyValueSlice + CmdTypeStringStructMap + CmdTypeXMessageSlice + CmdTypeXStreamSlice + CmdTypeXPending + CmdTypeXPendingExt + CmdTypeXAutoClaim + CmdTypeXAutoClaimWithDeleted + CmdTypeXAutoClaimJustID + CmdTypeXInfoConsumers + CmdTypeXInfoGroups + CmdTypeXInfoStream + CmdTypeXInfoStreamFull + CmdTypeZSlice + CmdTypeZWithKey + CmdTypeScan + CmdTypeClusterSlots + CmdTypeGeoLocation + CmdTypeGeoSearchLocation + CmdTypeGeoPos + CmdTypeCommandsInfo + CmdTypeSlowLog + CmdTypeMapStringStringSlice + CmdTypeMapMapStringInterface + CmdTypeKeyValues + CmdTypeZSliceWithKey + CmdTypeFunctionList + CmdTypeFunctionStats + CmdTypeLCS + CmdTypeKeyFlags + CmdTypeClusterLinks + CmdTypeClusterShards + CmdTypeRankWithScore + CmdTypeClientInfo + CmdTypeACLLog + CmdTypeInfo + CmdTypeMonitor + CmdTypeJSON + CmdTypeJSONSlice + CmdTypeIntPointerSlice + CmdTypeScanDump + CmdTypeBFInfo + CmdTypeCFInfo + CmdTypeCMSInfo + CmdTypeTopKInfo + CmdTypeTDigestInfo + CmdTypeFTSynDump + CmdTypeAggregate + CmdTypeFTInfo + CmdTypeFTSpellCheck + CmdTypeFTSearch + CmdTypeTSTimestampValue + CmdTypeTSTimestampValueSlice + CmdTypeHotKeys + CmdTypeIncrEXInt + CmdTypeIncrEXFloat + CmdTypeUint + CmdTypeUintSlice + CmdTypeAREntrySlice +) + +type ( + CmdTypeXAutoClaimValue struct { + messages []XMessage + start string + } + + CmdTypeXAutoClaimWithDeletedValue struct { + messages []XMessage + start string + deletedIDs []string + } + + CmdTypeXAutoClaimJustIDValue struct { + ids []string + start string + } + + CmdTypeScanValue struct { + keys []string + cursor uint64 + } + + CmdTypeKeyValuesValue struct { + key string + values []string + } + + CmdTypeZSliceWithKeyValue struct { + key string + zSlice []Z + } +) + type Cmder interface { // command name. // e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster". @@ -34,15 +213,28 @@ type Cmder interface { // e.g. "set k v ex 10" -> "set k v ex 10: OK", "get k" -> "get k: v". String() string + // Clone creates a copy of the command. + Clone() Cmder + stringArg(int) string firstKeyPos() int8 SetFirstKeyPos(int8) + stepCount() int8 + SetStepCount(int8) readTimeout() *time.Duration readReply(rd *proto.Reader) error readRawReply(rd *proto.Reader) error SetErr(error) Err() error + + // NoRetry returns true if the command should not be retried on failure. + // Commands that write directly to an io.Writer should return true since + // partial writes cannot be undone on retry. + NoRetry() bool + + // GetCmdType returns the command type for fast value extraction + GetCmdType() CmdType } func setCmdsErr(cmds []Cmder, e error) { @@ -62,6 +254,18 @@ func cmdsFirstErr(cmds []Cmder) error { return nil } +// cmdsContainNoRetry returns true if any command in the slice has NoRetry() == true. +// If a pipeline contains a non-retryable command (e.g., RawWriteToCmd), the entire +// pipeline must not be retried to prevent data corruption from partial writes. +func cmdsContainNoRetry(cmds []Cmder) bool { + for _, cmd := range cmds { + if cmd.NoRetry() { + return true + } + } + return false +} + func writeCmds(wr *proto.Writer, cmds []Cmder) error { for _, cmd := range cmds { if err := writeCmd(wr, cmd); err != nil { @@ -75,26 +279,42 @@ func writeCmd(wr *proto.Writer, cmd Cmder) error { return wr.WriteArgs(cmd.Args()) } -func cmdFirstKeyPos(cmd Cmder) int { +// cmdFirstKeyPosWithInfo returns the first key position in a command's args (0 if none). +// Uses CommandInfo.FirstKeyPos when available (via cache peek, no network call), falling +// back to a hardcoded table. eval/evalsha variants are resolved from the runtime numkeys arg. +func cmdFirstKeyPosWithInfo(cmd Cmder, info *CommandInfo) int { if pos := cmd.firstKeyPos(); pos != 0 { return int(pos) } - switch cmd.Name() { + name := cmd.Name() + + // first check if the command is keyless + if _, ok := keylessCommands[name]; ok { + return 0 + } + + switch name { case "eval", "evalsha", "eval_ro", "evalsha_ro": if cmd.stringArg(2) != "0" { return 3 } return 0 - case "publish": - return 1 case "memory": // https://github.com/redis/redis/issues/7493 if cmd.stringArg(1) == "usage" { return 2 } + // CommandInfo (if available) gives the correct answer + // otherwise the hardcoded fallback applies. + } + + // Use CommandInfo cache when warm (in-memory only, no extra round-trips). + if info != nil { + return int(info.FirstKeyPos) } + return 1 } @@ -126,8 +346,10 @@ type baseCmd struct { args []interface{} err error keyPos int8 + _stepCount int8 rawVal interface{} _readTimeout *time.Duration + cmdType CmdType } var _ Cmder = (*Cmd)(nil) @@ -183,6 +405,14 @@ func (cmd *baseCmd) SetFirstKeyPos(keyPos int8) { cmd.keyPos = keyPos } +func (cmd *baseCmd) stepCount() int8 { + return cmd._stepCount +} + +func (cmd *baseCmd) SetStepCount(stepCount int8) { + cmd._stepCount = stepCount +} + func (cmd *baseCmd) SetErr(e error) { cmd.err = e } @@ -204,6 +434,41 @@ func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) { return err } +// NoRetry returns true if the command should not be retried on failure. +// By default, commands can be retried. Commands that write directly to an +// io.Writer (like RawWriteToCmd) should override this to return true since +// partial writes cannot be undone on retry. +func (cmd *baseCmd) NoRetry() bool { + return false +} + +func (cmd *baseCmd) GetCmdType() CmdType { + return cmd.cmdType +} + +func (cmd *baseCmd) cloneBaseCmd() baseCmd { + var readTimeout *time.Duration + if cmd._readTimeout != nil { + timeout := *cmd._readTimeout + readTimeout = &timeout + } + + // Create a copy of args slice + args := make([]interface{}, len(cmd.args)) + copy(args, cmd.args) + + return baseCmd{ + ctx: cmd.ctx, + args: args, + err: cmd.err, + keyPos: cmd.keyPos, + _stepCount: cmd._stepCount, + rawVal: cmd.rawVal, + _readTimeout: readTimeout, + cmdType: cmd.cmdType, + } +} + //------------------------------------------------------------------------------ type Cmd struct { @@ -215,8 +480,9 @@ type Cmd struct { func NewCmd(ctx context.Context, args ...interface{}) *Cmd { return &Cmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeGeneric, }, } } @@ -489,6 +755,129 @@ func (cmd *Cmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *Cmd) Clone() Cmder { + return &Cmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +//------------------------------------------------------------------------------ + +// RawCmd returns raw RESP protocol bytes without parsing. +type RawCmd struct { + baseCmd + val []byte +} + +var _ Cmder = (*RawCmd)(nil) + +func NewRawCmd(ctx context.Context, args ...interface{}) *RawCmd { + return &RawCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeGeneric, + }, + } +} + +func (cmd *RawCmd) SetVal(val []byte) { + cmd.val = val +} + +func (cmd *RawCmd) Val() []byte { + return cmd.val +} + +func (cmd *RawCmd) Result() ([]byte, error) { + return cmd.val, cmd.err +} + +func (cmd *RawCmd) Bytes() ([]byte, error) { + return cmd.val, cmd.err +} + +func (cmd *RawCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *RawCmd) readReply(rd *proto.Reader) (err error) { + cmd.val, err = rd.ReadRawReply() + return err +} + +func (cmd *RawCmd) Clone() Cmder { + var val []byte + if cmd.val != nil { + val = make([]byte, len(cmd.val)) + copy(val, cmd.val) + } + return &RawCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//------------------------------------------------------------------------------ + +// RawWriteToCmd streams raw RESP protocol bytes directly to an io.Writer without intermediate allocations. +type RawWriteToCmd struct { + baseCmd + w io.Writer + written int64 +} + +var _ Cmder = (*RawWriteToCmd)(nil) + +func NewRawWriteToCmd(ctx context.Context, w io.Writer, args ...interface{}) *RawWriteToCmd { + return &RawWriteToCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeGeneric, + }, + w: w, + } +} + +func (cmd *RawWriteToCmd) SetVal(written int64) { + cmd.written = written +} + +func (cmd *RawWriteToCmd) Val() int64 { + return cmd.written +} + +func (cmd *RawWriteToCmd) Result() (int64, error) { + return cmd.written, cmd.err +} + +func (cmd *RawWriteToCmd) String() string { + return cmdString(cmd, cmd.written) +} + +func (cmd *RawWriteToCmd) readReply(rd *proto.Reader) (err error) { + cmd.written, err = rd.ReadRawReplyWriteTo(cmd.w) + return err +} + +// NoRetry returns true because RawWriteToCmd writes directly to an io.Writer. +// If a retry occurs, partial data from failed attempts would be appended to +// the writer, causing data corruption. The caller must handle retries manually +// if needed, using a fresh writer for each attempt. +func (cmd *RawWriteToCmd) NoRetry() bool { + return true +} + +func (cmd *RawWriteToCmd) Clone() Cmder { + return &RawWriteToCmd{ + baseCmd: cmd.cloneBaseCmd(), + w: cmd.w, + written: cmd.written, + } +} + //------------------------------------------------------------------------------ type SliceCmd struct { @@ -502,8 +891,9 @@ var _ Cmder = (*SliceCmd)(nil) func NewSliceCmd(ctx context.Context, args ...interface{}) *SliceCmd { return &SliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeSlice, }, } } @@ -549,6 +939,18 @@ func (cmd *SliceCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *SliceCmd) Clone() Cmder { + var val []interface{} + if cmd.val != nil { + val = make([]interface{}, len(cmd.val)) + copy(val, cmd.val) + } + return &SliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type StatusCmd struct { @@ -562,8 +964,9 @@ var _ Cmder = (*StatusCmd)(nil) func NewStatusCmd(ctx context.Context, args ...interface{}) *StatusCmd { return &StatusCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeStatus, }, } } @@ -593,6 +996,13 @@ func (cmd *StatusCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *StatusCmd) Clone() Cmder { + return &StatusCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ type IntCmd struct { @@ -606,8 +1016,9 @@ var _ Cmder = (*IntCmd)(nil) func NewIntCmd(ctx context.Context, args ...interface{}) *IntCmd { return &IntCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeInt, }, } } @@ -637,6 +1048,128 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *IntCmd) Clone() Cmder { + return &IntCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +type UintCmd struct { + baseCmd + + val uint64 +} + +var _ Cmder = (*UintCmd)(nil) + +func NewUintCmd(ctx context.Context, args ...any) *UintCmd { + return &UintCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeUint, + }, + } +} + +func (cmd *UintCmd) SetVal(val uint64) { + cmd.val = val +} + +func (cmd *UintCmd) Val() uint64 { + return cmd.val +} + +func (cmd *UintCmd) Result() (uint64, error) { + return cmd.val, cmd.err +} + +func (cmd *UintCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *UintCmd) readReply(rd *proto.Reader) (err error) { + cmd.val, err = rd.ReadUint() + return err +} + +func (cmd *UintCmd) Clone() Cmder { + return &UintCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +//------------------------------------------------------------------------------ + +// DigestCmd is a command that returns a uint64 xxh3 hash digest. +// +// This command is specifically designed for the Redis DIGEST command, +// which returns the xxh3 hash of a key's value as a hex string. +// The hex string is automatically parsed to a uint64 value. +// +// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE, +// and DelExArgs commands. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/digest/ +type DigestCmd struct { + baseCmd + + val uint64 +} + +var _ Cmder = (*DigestCmd)(nil) + +func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd { + return &DigestCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *DigestCmd) SetVal(val uint64) { + cmd.val = val +} + +func (cmd *DigestCmd) Val() uint64 { + return cmd.val +} + +func (cmd *DigestCmd) Result() (uint64, error) { + return cmd.val, cmd.err +} + +func (cmd *DigestCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *DigestCmd) Clone() Cmder { + return &DigestCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) { + // Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890") + // We parse it as a uint64 xxh3 hash value + var hexStr string + hexStr, err = rd.ReadString() + if err != nil { + return err + } + + // Parse hex string to uint64 + cmd.val, err = strconv.ParseUint(hexStr, 16, 64) + return err +} + //------------------------------------------------------------------------------ type IntSliceCmd struct { @@ -650,8 +1183,9 @@ var _ Cmder = (*IntSliceCmd)(nil) func NewIntSliceCmd(ctx context.Context, args ...interface{}) *IntSliceCmd { return &IntSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeIntSlice, }, } } @@ -686,6 +1220,78 @@ func (cmd *IntSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *IntSliceCmd) Clone() Cmder { + var val []int64 + if cmd.val != nil { + val = make([]int64, len(cmd.val)) + copy(val, cmd.val) + } + return &IntSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +type UintSliceCmd struct { + baseCmd + + val []uint64 +} + +var _ Cmder = (*UintSliceCmd)(nil) + +func NewUintSliceCmd(ctx context.Context, args ...any) *UintSliceCmd { + return &UintSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeUintSlice, + }, + } +} + +func (cmd *UintSliceCmd) SetVal(val []uint64) { + cmd.val = val +} + +func (cmd *UintSliceCmd) Val() []uint64 { + return cmd.val +} + +func (cmd *UintSliceCmd) Result() ([]uint64, error) { + return cmd.val, cmd.err +} + +func (cmd *UintSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *UintSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val = make([]uint64, n) + for i := range cmd.val { + if cmd.val[i], err = rd.ReadUint(); err != nil { + return err + } + } + return nil +} + +func (cmd *UintSliceCmd) Clone() Cmder { + var val []uint64 + if cmd.val != nil { + val = make([]uint64, len(cmd.val)) + copy(val, cmd.val) + } + return &UintSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type DurationCmd struct { @@ -700,8 +1306,9 @@ var _ Cmder = (*DurationCmd)(nil) func NewDurationCmd(ctx context.Context, precision time.Duration, args ...interface{}) *DurationCmd { return &DurationCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeDuration, }, precision: precision, } @@ -739,6 +1346,14 @@ func (cmd *DurationCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *DurationCmd) Clone() Cmder { + return &DurationCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + precision: cmd.precision, + } +} + //------------------------------------------------------------------------------ type TimeCmd struct { @@ -752,8 +1367,9 @@ var _ Cmder = (*TimeCmd)(nil) func NewTimeCmd(ctx context.Context, args ...interface{}) *TimeCmd { return &TimeCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeTime, }, } } @@ -790,6 +1406,13 @@ func (cmd *TimeCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *TimeCmd) Clone() Cmder { + return &TimeCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ type BoolCmd struct { @@ -803,8 +1426,9 @@ var _ Cmder = (*BoolCmd)(nil) func NewBoolCmd(ctx context.Context, args ...interface{}) *BoolCmd { return &BoolCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeBool, }, } } @@ -837,6 +1461,13 @@ func (cmd *BoolCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *BoolCmd) Clone() Cmder { + return &BoolCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ type StringCmd struct { @@ -850,8 +1481,9 @@ var _ Cmder = (*StringCmd)(nil) func NewStringCmd(ctx context.Context, args ...interface{}) *StringCmd { return &StringCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeString, }, } } @@ -883,28 +1515,28 @@ func (cmd *StringCmd) Int() (int, error) { if cmd.err != nil { return 0, cmd.err } - return strconv.Atoi(cmd.Val()) + return strconv.Atoi(cmd.val) } func (cmd *StringCmd) Int64() (int64, error) { if cmd.err != nil { return 0, cmd.err } - return strconv.ParseInt(cmd.Val(), 10, 64) + return strconv.ParseInt(cmd.val, 10, 64) } func (cmd *StringCmd) Uint64() (uint64, error) { if cmd.err != nil { return 0, cmd.err } - return strconv.ParseUint(cmd.Val(), 10, 64) + return strconv.ParseUint(cmd.val, 10, 64) } func (cmd *StringCmd) Float32() (float32, error) { if cmd.err != nil { return 0, cmd.err } - f, err := strconv.ParseFloat(cmd.Val(), 32) + f, err := strconv.ParseFloat(cmd.val, 32) if err != nil { return 0, err } @@ -915,14 +1547,14 @@ func (cmd *StringCmd) Float64() (float64, error) { if cmd.err != nil { return 0, cmd.err } - return strconv.ParseFloat(cmd.Val(), 64) + return strconv.ParseFloat(cmd.val, 64) } func (cmd *StringCmd) Time() (time.Time, error) { if cmd.err != nil { return time.Time{}, cmd.err } - return time.Parse(time.RFC3339Nano, cmd.Val()) + return time.Parse(time.RFC3339Nano, cmd.val) } func (cmd *StringCmd) Scan(val interface{}) error { @@ -941,6 +1573,13 @@ func (cmd *StringCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *StringCmd) Clone() Cmder { + return &StringCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ type FloatCmd struct { @@ -954,8 +1593,9 @@ var _ Cmder = (*FloatCmd)(nil) func NewFloatCmd(ctx context.Context, args ...interface{}) *FloatCmd { return &FloatCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFloat, }, } } @@ -981,6 +1621,13 @@ func (cmd *FloatCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *FloatCmd) Clone() Cmder { + return &FloatCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ type FloatSliceCmd struct { @@ -994,8 +1641,9 @@ var _ Cmder = (*FloatSliceCmd)(nil) func NewFloatSliceCmd(ctx context.Context, args ...interface{}) *FloatSliceCmd { return &FloatSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFloatSlice, }, } } @@ -1036,6 +1684,18 @@ func (cmd *FloatSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *FloatSliceCmd) Clone() Cmder { + var val []float64 + if cmd.val != nil { + val = make([]float64, len(cmd.val)) + copy(val, cmd.val) + } + return &FloatSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type StringSliceCmd struct { @@ -1049,8 +1709,9 @@ var _ Cmder = (*StringSliceCmd)(nil) func NewStringSliceCmd(ctx context.Context, args ...interface{}) *StringSliceCmd { return &StringSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeStringSlice, }, } } @@ -1072,7 +1733,7 @@ func (cmd *StringSliceCmd) String() string { } func (cmd *StringSliceCmd) ScanSlice(container interface{}) error { - return proto.ScanSlice(cmd.Val(), container) + return proto.ScanSlice(cmd.val, container) } func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { @@ -1094,6 +1755,99 @@ func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *StringSliceCmd) Clone() Cmder { + var val []string + if cmd.val != nil { + val = make([]string, len(cmd.val)) + copy(val, cmd.val) + } + return &StringSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//------------------------------------------------------------------------------ + +// StringSliceSliceCmd returns a slice of string slices ([][]string). +// This is used for commands like VLINKS that return an array of arrays. +type StringSliceSliceCmd struct { + baseCmd + + val [][]string +} + +var _ Cmder = (*StringSliceSliceCmd)(nil) + +func NewStringSliceSliceCmd(ctx context.Context, args ...any) *StringSliceSliceCmd { + return &StringSliceSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringSliceSliceCmd) SetVal(val [][]string) { + cmd.val = val +} + +func (cmd *StringSliceSliceCmd) Val() [][]string { + return cmd.val +} + +func (cmd *StringSliceSliceCmd) Result() ([][]string, error) { + return cmd.val, cmd.err +} + +func (cmd *StringSliceSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringSliceSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val = make([][]string, n) + for i := range n { + // Read inner array + innerN, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val[i] = make([]string, innerN) + for j := range innerN { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.val[i][j] = "" + case err != nil: + return err + default: + cmd.val[i][j] = s + } + } + } + return nil +} + +func (cmd *StringSliceSliceCmd) Clone() Cmder { + var val [][]string + if cmd.val != nil { + val = make([][]string, len(cmd.val)) + for i, slice := range cmd.val { + if slice != nil { + val[i] = make([]string, len(slice)) + copy(val[i], slice) + } + } + } + return &StringSliceSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type KeyValue struct { @@ -1112,8 +1866,9 @@ var _ Cmder = (*KeyValueSliceCmd)(nil) func NewKeyValueSliceCmd(ctx context.Context, args ...interface{}) *KeyValueSliceCmd { return &KeyValueSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeKeyValueSlice, }, } } @@ -1188,6 +1943,18 @@ func (cmd *KeyValueSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl return nil } +func (cmd *KeyValueSliceCmd) Clone() Cmder { + var val []KeyValue + if cmd.val != nil { + val = make([]KeyValue, len(cmd.val)) + copy(val, cmd.val) + } + return &KeyValueSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type BoolSliceCmd struct { @@ -1201,8 +1968,9 @@ var _ Cmder = (*BoolSliceCmd)(nil) func NewBoolSliceCmd(ctx context.Context, args ...interface{}) *BoolSliceCmd { return &BoolSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeBoolSlice, }, } } @@ -1237,6 +2005,18 @@ func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *BoolSliceCmd) Clone() Cmder { + var val []bool + if cmd.val != nil { + val = make([]bool, len(cmd.val)) + copy(val, cmd.val) + } + return &BoolSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type MapStringStringCmd struct { @@ -1250,8 +2030,9 @@ var _ Cmder = (*MapStringStringCmd)(nil) func NewMapStringStringCmd(ctx context.Context, args ...interface{}) *MapStringStringCmd { return &MapStringStringCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringString, }, } } @@ -1316,6 +2097,20 @@ func (cmd *MapStringStringCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *MapStringStringCmd) Clone() Cmder { + var val map[string]string + if cmd.val != nil { + val = make(map[string]string, len(cmd.val)) + for k, v := range cmd.val { + val[k] = v + } + } + return &MapStringStringCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type MapStringIntCmd struct { @@ -1329,8 +2124,9 @@ var _ Cmder = (*MapStringIntCmd)(nil) func NewMapStringIntCmd(ctx context.Context, args ...interface{}) *MapStringIntCmd { return &MapStringIntCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringInt, }, } } @@ -1373,6 +2169,20 @@ func (cmd *MapStringIntCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *MapStringIntCmd) Clone() Cmder { + var val map[string]int64 + if cmd.val != nil { + val = make(map[string]int64, len(cmd.val)) + for k, v := range cmd.val { + val[k] = v + } + } + return &MapStringIntCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // ------------------------------------------------------------------------------ type MapStringSliceInterfaceCmd struct { baseCmd @@ -1382,8 +2192,9 @@ type MapStringSliceInterfaceCmd struct { func NewMapStringSliceInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringSliceInterfaceCmd { return &MapStringSliceInterfaceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringInterfaceSlice, }, } } @@ -1469,6 +2280,24 @@ func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *MapStringSliceInterfaceCmd) Clone() Cmder { + var val map[string][]interface{} + if cmd.val != nil { + val = make(map[string][]interface{}, len(cmd.val)) + for k, v := range cmd.val { + if v != nil { + newSlice := make([]interface{}, len(v)) + copy(newSlice, v) + val[k] = newSlice + } + } + } + return &MapStringSliceInterfaceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type StringStructMapCmd struct { @@ -1482,8 +2311,9 @@ var _ Cmder = (*StringStructMapCmd)(nil) func NewStringStructMapCmd(ctx context.Context, args ...interface{}) *StringStructMapCmd { return &StringStructMapCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeStringStructMap, }, } } @@ -1521,11 +2351,28 @@ func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *StringStructMapCmd) Clone() Cmder { + var val map[string]struct{} + if cmd.val != nil { + val = maps.Clone(cmd.val) + } + return &StringStructMapCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XMessage struct { ID string Values map[string]interface{} + // MillisElapsedFromDelivery is the number of milliseconds since the entry was last delivered. + // Only populated when using XREADGROUP with CLAIM argument for claimed entries. + MillisElapsedFromDelivery int64 + // DeliveredCount is the number of times the entry was delivered. + // Only populated when using XREADGROUP with CLAIM argument for claimed entries. + DeliveredCount int64 } type XMessageSliceCmd struct { @@ -1539,8 +2386,9 @@ var _ Cmder = (*XMessageSliceCmd)(nil) func NewXMessageSliceCmd(ctx context.Context, args ...interface{}) *XMessageSliceCmd { return &XMessageSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXMessageSlice, }, } } @@ -1566,6 +2414,25 @@ func (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) (err error) { return err } +func (cmd *XMessageSliceCmd) Clone() Cmder { + var val []XMessage + if cmd.val != nil { + val = make([]XMessage, len(cmd.val)) + for i, msg := range cmd.val { + val[i] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val[i].Values = maps.Clone(msg.Values) + } + } + } + return &XMessageSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + func readXMessageSlice(rd *proto.Reader) ([]XMessage, error) { n, err := rd.ReadArrayLen() if err != nil { @@ -1582,10 +2449,16 @@ func readXMessageSlice(rd *proto.Reader) ([]XMessage, error) { } func readXMessage(rd *proto.Reader) (XMessage, error) { - if err := rd.ReadFixedArrayLen(2); err != nil { + // Read array length can be 2 or 4 (with CLAIM metadata) + n, err := rd.ReadArrayLen() + if err != nil { return XMessage{}, err } + if n != 2 && n != 4 { + return XMessage{}, fmt.Errorf("redis: got %d elements in the XMessage array, expected 2 or 4", n) + } + id, err := rd.ReadString() if err != nil { return XMessage{}, err @@ -1598,10 +2471,24 @@ func readXMessage(rd *proto.Reader) (XMessage, error) { } } - return XMessage{ + msg := XMessage{ ID: id, Values: v, - }, nil + } + + if n == 4 { + msg.MillisElapsedFromDelivery, err = rd.ReadInt() + if err != nil { + return XMessage{}, err + } + + msg.DeliveredCount, err = rd.ReadInt() + if err != nil { + return XMessage{}, err + } + } + + return msg, nil } func stringInterfaceMapParser(rd *proto.Reader) (map[string]interface{}, error) { @@ -1645,8 +2532,9 @@ var _ Cmder = (*XStreamSliceCmd)(nil) func NewXStreamSliceCmd(ctx context.Context, args ...interface{}) *XStreamSliceCmd { return &XStreamSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXStreamSlice, }, } } @@ -1699,6 +2587,36 @@ func (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *XStreamSliceCmd) Clone() Cmder { + var val []XStream + if cmd.val != nil { + val = make([]XStream, len(cmd.val)) + for i, stream := range cmd.val { + val[i] = XStream{ + Stream: stream.Stream, + } + if stream.Messages != nil { + val[i].Messages = make([]XMessage, len(stream.Messages)) + for j, msg := range stream.Messages { + val[i].Messages[j] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val[i].Messages[j].Values = make(map[string]interface{}, len(msg.Values)) + for k, v := range msg.Values { + val[i].Messages[j].Values[k] = v + } + } + } + } + } + } + return &XStreamSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XPending struct { @@ -1718,8 +2636,9 @@ var _ Cmder = (*XPendingCmd)(nil) func NewXPendingCmd(ctx context.Context, args ...interface{}) *XPendingCmd { return &XPendingCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXPending, }, } } @@ -1782,6 +2701,27 @@ func (cmd *XPendingCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *XPendingCmd) Clone() Cmder { + var val *XPending + if cmd.val != nil { + val = &XPending{ + Count: cmd.val.Count, + Lower: cmd.val.Lower, + Higher: cmd.val.Higher, + } + if cmd.val.Consumers != nil { + val.Consumers = make(map[string]int64, len(cmd.val.Consumers)) + for k, v := range cmd.val.Consumers { + val.Consumers[k] = v + } + } + } + return &XPendingCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XPendingExt struct { @@ -1801,8 +2741,9 @@ var _ Cmder = (*XPendingExtCmd)(nil) func NewXPendingExtCmd(ctx context.Context, args ...interface{}) *XPendingExtCmd { return &XPendingExtCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXPendingExt, }, } } @@ -1857,6 +2798,18 @@ func (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *XPendingExtCmd) Clone() Cmder { + var val []XPendingExt + if cmd.val != nil { + val = make([]XPendingExt, len(cmd.val)) + copy(val, cmd.val) + } + return &XPendingExtCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XAutoClaimCmd struct { @@ -1871,8 +2824,9 @@ var _ Cmder = (*XAutoClaimCmd)(nil) func NewXAutoClaimCmd(ctx context.Context, args ...interface{}) *XAutoClaimCmd { return &XAutoClaimCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXAutoClaim, }, } } @@ -1919,52 +2873,76 @@ func (cmd *XAutoClaimCmd) readReply(rd *proto.Reader) error { } if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err - } + return rd.DiscardNext() } return nil } +func (cmd *XAutoClaimCmd) Clone() Cmder { + var val []XMessage + if cmd.val != nil { + val = make([]XMessage, len(cmd.val)) + for i, msg := range cmd.val { + val[i] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val[i].Values = make(map[string]interface{}, len(msg.Values)) + for k, v := range msg.Values { + val[i].Values[k] = v + } + } + } + } + return &XAutoClaimCmd{ + baseCmd: cmd.cloneBaseCmd(), + start: cmd.start, + val: val, + } +} + //------------------------------------------------------------------------------ -type XAutoClaimJustIDCmd struct { +type XAutoClaimWithDeletedCmd struct { baseCmd - start string - val []string + start string + val []XMessage + deletedIDs []string } -var _ Cmder = (*XAutoClaimJustIDCmd)(nil) +var _ Cmder = (*XAutoClaimWithDeletedCmd)(nil) -func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd { - return &XAutoClaimJustIDCmd{ +func NewXAutoClaimWithDeletedCmd(ctx context.Context, args ...interface{}) *XAutoClaimWithDeletedCmd { + return &XAutoClaimWithDeletedCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXAutoClaimWithDeleted, }, } } -func (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) { +func (cmd *XAutoClaimWithDeletedCmd) SetVal(val []XMessage, start string, deletedIDs []string) { cmd.val = val cmd.start = start + cmd.deletedIDs = deletedIDs } -func (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) { - return cmd.val, cmd.start +func (cmd *XAutoClaimWithDeletedCmd) Val() (messages []XMessage, start string, deletedIDs []string) { + return cmd.val, cmd.start, cmd.deletedIDs } -func (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) { - return cmd.val, cmd.start, cmd.err +func (cmd *XAutoClaimWithDeletedCmd) Result() (messages []XMessage, start string, deletedIDs []string, err error) { + return cmd.val, cmd.start, cmd.deletedIDs, cmd.err } -func (cmd *XAutoClaimJustIDCmd) String() string { +func (cmd *XAutoClaimWithDeletedCmd) String() string { return cmdString(cmd, cmd.val) } -func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { +func (cmd *XAutoClaimWithDeletedCmd) readReply(rd *proto.Reader) error { n, err := rd.ReadArrayLen() if err != nil { return err @@ -1975,7 +2953,7 @@ func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { 3: // Redis 7: // ok default: - return fmt.Errorf("redis: got %d elements in XAutoClaimJustID reply, wanted 2/3", n) + return fmt.Errorf("redis: got %d elements in XAutoClaim reply, wanted 2/3", n) } cmd.start, err = rd.ReadString() @@ -1983,54 +2961,179 @@ func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { return err } + cmd.val, err = readXMessageSlice(rd) + if err != nil { + return err + } + + if n < 3 { + return nil + } + nn, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val = make([]string, nn) + cmd.deletedIDs = make([]string, nn) for i := 0; i < nn; i++ { - cmd.val[i], err = rd.ReadString() + cmd.deletedIDs[i], err = rd.ReadString() if err != nil { return err } } - if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err + return nil +} + +func (cmd *XAutoClaimWithDeletedCmd) Clone() Cmder { + var val []XMessage + if cmd.val != nil { + val = make([]XMessage, len(cmd.val)) + for i, msg := range cmd.val { + val[i] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val[i].Values = make(map[string]interface{}, len(msg.Values)) + for k, v := range msg.Values { + val[i].Values[k] = v + } + } } } - - return nil + var deletedIDs []string + if cmd.deletedIDs != nil { + deletedIDs = make([]string, len(cmd.deletedIDs)) + copy(deletedIDs, cmd.deletedIDs) + } + return &XAutoClaimWithDeletedCmd{ + baseCmd: cmd.cloneBaseCmd(), + start: cmd.start, + val: val, + deletedIDs: deletedIDs, + } } //------------------------------------------------------------------------------ -type XInfoConsumersCmd struct { +type XAutoClaimJustIDCmd struct { baseCmd - val []XInfoConsumer -} -type XInfoConsumer struct { - Name string - Pending int64 - Idle time.Duration - Inactive time.Duration + start string + val []string } -var _ Cmder = (*XInfoConsumersCmd)(nil) +var _ Cmder = (*XAutoClaimJustIDCmd)(nil) -func NewXInfoConsumersCmd(ctx context.Context, stream string, group string) *XInfoConsumersCmd { - return &XInfoConsumersCmd{ +func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd { + return &XAutoClaimJustIDCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "consumers", stream, group}, + ctx: ctx, + args: args, + cmdType: CmdTypeXAutoClaimJustID, }, } } -func (cmd *XInfoConsumersCmd) SetVal(val []XInfoConsumer) { +func (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) { + cmd.val = val + cmd.start = start +} + +func (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) { + return cmd.val, cmd.start +} + +func (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) { + return cmd.val, cmd.start, cmd.err +} + +func (cmd *XAutoClaimJustIDCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + switch n { + case 2, // Redis 6 + 3: // Redis 7: + // ok + default: + return fmt.Errorf("redis: got %d elements in XAutoClaimJustID reply, wanted 2/3", n) + } + + cmd.start, err = rd.ReadString() + if err != nil { + return err + } + + nn, err := rd.ReadArrayLen() + if err != nil { + return err + } + + cmd.val = make([]string, nn) + for i := 0; i < nn; i++ { + cmd.val[i], err = rd.ReadString() + if err != nil { + return err + } + } + + if n >= 3 { + if err := rd.DiscardNext(); err != nil { + return err + } + } + + return nil +} + +func (cmd *XAutoClaimJustIDCmd) Clone() Cmder { + var val []string + if cmd.val != nil { + val = make([]string, len(cmd.val)) + copy(val, cmd.val) + } + return &XAutoClaimJustIDCmd{ + baseCmd: cmd.cloneBaseCmd(), + start: cmd.start, + val: val, + } +} + +//------------------------------------------------------------------------------ + +type XInfoConsumersCmd struct { + baseCmd + val []XInfoConsumer +} + +type XInfoConsumer struct { + Name string + Pending int64 + Idle time.Duration + Inactive time.Duration +} + +var _ Cmder = (*XInfoConsumersCmd)(nil) + +func NewXInfoConsumersCmd(ctx context.Context, stream string, group string) *XInfoConsumersCmd { + return &XInfoConsumersCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: []interface{}{"xinfo", "consumers", stream, group}, + cmdType: CmdTypeXInfoConsumers, + }, + } +} + +func (cmd *XInfoConsumersCmd) SetVal(val []XInfoConsumer) { cmd.val = val } @@ -2080,7 +3183,10 @@ func (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error { inactive, err = rd.ReadInt() cmd.val[i].Inactive = time.Duration(inactive) * time.Millisecond default: - return fmt.Errorf("redis: unexpected content %s in XINFO CONSUMERS reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } if err != nil { return err @@ -2091,6 +3197,18 @@ func (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *XInfoConsumersCmd) Clone() Cmder { + var val []XInfoConsumer + if cmd.val != nil { + val = make([]XInfoConsumer, len(cmd.val)) + copy(val, cmd.val) + } + return &XInfoConsumersCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XInfoGroupsCmd struct { @@ -2114,8 +3232,9 @@ var _ Cmder = (*XInfoGroupsCmd)(nil) func NewXInfoGroupsCmd(ctx context.Context, stream string) *XInfoGroupsCmd { return &XInfoGroupsCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "groups", stream}, + ctx: ctx, + args: []interface{}{"xinfo", "groups", stream}, + cmdType: CmdTypeXInfoGroups, }, } } @@ -2196,7 +3315,10 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { group.Lag = -1 } default: - return fmt.Errorf("redis: unexpected key %q in XINFO GROUPS reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } } @@ -2204,6 +3326,18 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *XInfoGroupsCmd) Clone() Cmder { + var val []XInfoGroup + if cmd.val != nil { + val = make([]XInfoGroup, len(cmd.val)) + copy(val, cmd.val) + } + return &XInfoGroupsCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XInfoStreamCmd struct { @@ -2222,6 +3356,13 @@ type XInfoStream struct { FirstEntry XMessage LastEntry XMessage RecordedFirstEntryID string + + IDMPDuration int64 + IDMPMaxSize int64 + PIDsTracked int64 + IIDsTracked int64 + IIDsAdded int64 + IIDsDuplicates int64 } var _ Cmder = (*XInfoStreamCmd)(nil) @@ -2229,8 +3370,9 @@ var _ Cmder = (*XInfoStreamCmd)(nil) func NewXInfoStreamCmd(ctx context.Context, stream string) *XInfoStreamCmd { return &XInfoStreamCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "stream", stream}, + ctx: ctx, + args: []interface{}{"xinfo", "stream", stream}, + cmdType: CmdTypeXInfoStream, }, } } @@ -2314,13 +3456,85 @@ func (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error { if err != nil { return err } + case "idmp-duration": + cmd.val.IDMPDuration, err = rd.ReadInt() + if err != nil { + return err + } + case "idmp-maxsize": + cmd.val.IDMPMaxSize, err = rd.ReadInt() + if err != nil { + return err + } + case "pids-tracked": + cmd.val.PIDsTracked, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-tracked": + cmd.val.IIDsTracked, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-added": + cmd.val.IIDsAdded, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-duplicates": + cmd.val.IIDsDuplicates, err = rd.ReadInt() + if err != nil { + return err + } default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } return nil } +func (cmd *XInfoStreamCmd) Clone() Cmder { + var val *XInfoStream + if cmd.val != nil { + val = &XInfoStream{ + Length: cmd.val.Length, + RadixTreeKeys: cmd.val.RadixTreeKeys, + RadixTreeNodes: cmd.val.RadixTreeNodes, + Groups: cmd.val.Groups, + LastGeneratedID: cmd.val.LastGeneratedID, + MaxDeletedEntryID: cmd.val.MaxDeletedEntryID, + EntriesAdded: cmd.val.EntriesAdded, + RecordedFirstEntryID: cmd.val.RecordedFirstEntryID, + } + // Clone XMessage fields + val.FirstEntry = XMessage{ + ID: cmd.val.FirstEntry.ID, + } + if cmd.val.FirstEntry.Values != nil { + val.FirstEntry.Values = make(map[string]interface{}, len(cmd.val.FirstEntry.Values)) + for k, v := range cmd.val.FirstEntry.Values { + val.FirstEntry.Values[k] = v + } + } + val.LastEntry = XMessage{ + ID: cmd.val.LastEntry.ID, + } + if cmd.val.LastEntry.Values != nil { + val.LastEntry.Values = make(map[string]interface{}, len(cmd.val.LastEntry.Values)) + for k, v := range cmd.val.LastEntry.Values { + val.LastEntry.Values[k] = v + } + } + } + return &XInfoStreamCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type XInfoStreamFullCmd struct { @@ -2338,6 +3552,12 @@ type XInfoStreamFull struct { Entries []XMessage Groups []XInfoStreamGroup RecordedFirstEntryID string + IDMPDuration int64 + IDMPMaxSize int64 + PIDsTracked int64 + IIDsTracked int64 + IIDsAdded int64 + IIDsDuplicates int64 } type XInfoStreamGroup struct { @@ -2346,6 +3566,7 @@ type XInfoStreamGroup struct { EntriesRead int64 Lag int64 PelCount int64 + NackedCount uint64 // redis version 8.8, number of NACK'd messages in the group Pending []XInfoStreamGroupPending Consumers []XInfoStreamConsumer } @@ -2376,8 +3597,9 @@ var _ Cmder = (*XInfoStreamFullCmd)(nil) func NewXInfoStreamFullCmd(ctx context.Context, args ...interface{}) *XInfoStreamFullCmd { return &XInfoStreamFullCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeXInfoStreamFull, }, } } @@ -2458,8 +3680,41 @@ func (cmd *XInfoStreamFullCmd) readReply(rd *proto.Reader) error { if err != nil { return err } + case "idmp-duration": + cmd.val.IDMPDuration, err = rd.ReadInt() + if err != nil { + return err + } + case "idmp-maxsize": + cmd.val.IDMPMaxSize, err = rd.ReadInt() + if err != nil { + return err + } + case "pids-tracked": + cmd.val.PIDsTracked, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-tracked": + cmd.val.IIDsTracked, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-added": + cmd.val.IIDsAdded, err = rd.ReadInt() + if err != nil { + return err + } + case "iids-duplicates": + cmd.val.IIDsDuplicates, err = rd.ReadInt() + if err != nil { + return err + } default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } return nil @@ -2513,6 +3768,11 @@ func readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) { if err != nil { return nil, err } + case "nacked-count": + group.NackedCount, err = rd.ReadUint() + if err != nil { + return nil, err + } case "pending": group.Pending, err = readXInfoStreamGroupPending(rd) if err != nil { @@ -2524,7 +3784,10 @@ func readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) { return nil, err } default: - return nil, fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return nil, err + } } } @@ -2649,8 +3912,10 @@ func readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) { c.Pending = append(c.Pending, p) } default: - return nil, fmt.Errorf("redis: unexpected content %s "+ - "in XINFO STREAM FULL reply", cKey) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return nil, err + } } if err != nil { return nil, err @@ -2662,6 +3927,45 @@ func readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) { return consumers, nil } +func (cmd *XInfoStreamFullCmd) Clone() Cmder { + var val *XInfoStreamFull + if cmd.val != nil { + val = &XInfoStreamFull{ + Length: cmd.val.Length, + RadixTreeKeys: cmd.val.RadixTreeKeys, + RadixTreeNodes: cmd.val.RadixTreeNodes, + LastGeneratedID: cmd.val.LastGeneratedID, + MaxDeletedEntryID: cmd.val.MaxDeletedEntryID, + EntriesAdded: cmd.val.EntriesAdded, + RecordedFirstEntryID: cmd.val.RecordedFirstEntryID, + } + // Clone Entries + if cmd.val.Entries != nil { + val.Entries = make([]XMessage, len(cmd.val.Entries)) + for i, msg := range cmd.val.Entries { + val.Entries[i] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val.Entries[i].Values = make(map[string]interface{}, len(msg.Values)) + for k, v := range msg.Values { + val.Entries[i].Values[k] = v + } + } + } + } + // Clone Groups - simplified copy for now due to complexity + if cmd.val.Groups != nil { + val.Groups = make([]XInfoStreamGroup, len(cmd.val.Groups)) + copy(val.Groups, cmd.val.Groups) + } + } + return &XInfoStreamFullCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type ZSliceCmd struct { @@ -2675,8 +3979,9 @@ var _ Cmder = (*ZSliceCmd)(nil) func NewZSliceCmd(ctx context.Context, args ...interface{}) *ZSliceCmd { return &ZSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeZSlice, }, } } @@ -2740,6 +4045,18 @@ func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl return nil } +func (cmd *ZSliceCmd) Clone() Cmder { + var val []Z + if cmd.val != nil { + val = make([]Z, len(cmd.val)) + copy(val, cmd.val) + } + return &ZSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type ZWithKeyCmd struct { @@ -2753,8 +4070,9 @@ var _ Cmder = (*ZWithKeyCmd)(nil) func NewZWithKeyCmd(ctx context.Context, args ...interface{}) *ZWithKeyCmd { return &ZWithKeyCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeZWithKey, }, } } @@ -2794,6 +4112,23 @@ func (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *ZWithKeyCmd) Clone() Cmder { + var val *ZWithKey + if cmd.val != nil { + val = &ZWithKey{ + Z: Z{ + Score: cmd.val.Score, + Member: cmd.val.Member, + }, + Key: cmd.val.Key, + } + } + return &ZWithKeyCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type ScanCmd struct { @@ -2810,8 +4145,9 @@ var _ Cmder = (*ScanCmd)(nil) func NewScanCmd(ctx context.Context, process cmdable, args ...interface{}) *ScanCmd { return &ScanCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeScan, }, process: process, } @@ -2859,6 +4195,20 @@ func (cmd *ScanCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *ScanCmd) Clone() Cmder { + var page []string + if cmd.page != nil { + page = make([]string, len(cmd.page)) + copy(page, cmd.page) + } + return &ScanCmd{ + baseCmd: cmd.cloneBaseCmd(), + page: page, + cursor: cmd.cursor, + process: cmd.process, + } +} + // Iterator creates a new ScanIterator. func (cmd *ScanCmd) Iterator() *ScanIterator { return &ScanIterator{ @@ -2891,8 +4241,9 @@ var _ Cmder = (*ClusterSlotsCmd)(nil) func NewClusterSlotsCmd(ctx context.Context, args ...interface{}) *ClusterSlotsCmd { return &ClusterSlotsCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeClusterSlots, }, } } @@ -3005,6 +4356,38 @@ func (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *ClusterSlotsCmd) Clone() Cmder { + var val []ClusterSlot + if cmd.val != nil { + val = make([]ClusterSlot, len(cmd.val)) + for i, slot := range cmd.val { + val[i] = ClusterSlot{ + Start: slot.Start, + End: slot.End, + } + if slot.Nodes != nil { + val[i].Nodes = make([]ClusterNode, len(slot.Nodes)) + for j, node := range slot.Nodes { + val[i].Nodes[j] = ClusterNode{ + ID: node.ID, + Addr: node.Addr, + } + if node.NetworkingMetadata != nil { + val[i].Nodes[j].NetworkingMetadata = make(map[string]string, len(node.NetworkingMetadata)) + for k, v := range node.NetworkingMetadata { + val[i].Nodes[j].NetworkingMetadata[k] = v + } + } + } + } + } + } + return &ClusterSlotsCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ // GeoLocation is used with GeoAdd to add geospatial location. @@ -3044,8 +4427,9 @@ var _ Cmder = (*GeoLocationCmd)(nil) func NewGeoLocationCmd(ctx context.Context, q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd { return &GeoLocationCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: geoLocationArgs(q, args...), + ctx: ctx, + args: geoLocationArgs(q, args...), + cmdType: CmdTypeGeoLocation, }, q: q, } @@ -3153,6 +4537,34 @@ func (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *GeoLocationCmd) Clone() Cmder { + var q *GeoRadiusQuery + if cmd.q != nil { + q = &GeoRadiusQuery{ + Radius: cmd.q.Radius, + Unit: cmd.q.Unit, + WithCoord: cmd.q.WithCoord, + WithDist: cmd.q.WithDist, + WithGeoHash: cmd.q.WithGeoHash, + Count: cmd.q.Count, + Sort: cmd.q.Sort, + Store: cmd.q.Store, + StoreDist: cmd.q.StoreDist, + withLen: cmd.q.withLen, + } + } + var locations []GeoLocation + if cmd.locations != nil { + locations = make([]GeoLocation, len(cmd.locations)) + copy(locations, cmd.locations) + } + return &GeoLocationCmd{ + baseCmd: cmd.cloneBaseCmd(), + q: q, + locations: locations, + } +} + //------------------------------------------------------------------------------ // GeoSearchQuery is used for GEOSearch/GEOSearchStore command query. @@ -3260,8 +4672,9 @@ func NewGeoSearchLocationCmd( ) *GeoSearchLocationCmd { return &GeoSearchLocationCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: geoSearchLocationArgs(opt, args), + cmdType: CmdTypeGeoSearchLocation, }, opt: opt, } @@ -3334,6 +4747,40 @@ func (cmd *GeoSearchLocationCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *GeoSearchLocationCmd) Clone() Cmder { + var opt *GeoSearchLocationQuery + if cmd.opt != nil { + opt = &GeoSearchLocationQuery{ + GeoSearchQuery: GeoSearchQuery{ + Member: cmd.opt.Member, + Longitude: cmd.opt.Longitude, + Latitude: cmd.opt.Latitude, + Radius: cmd.opt.Radius, + RadiusUnit: cmd.opt.RadiusUnit, + BoxWidth: cmd.opt.BoxWidth, + BoxHeight: cmd.opt.BoxHeight, + BoxUnit: cmd.opt.BoxUnit, + Sort: cmd.opt.Sort, + Count: cmd.opt.Count, + CountAny: cmd.opt.CountAny, + }, + WithCoord: cmd.opt.WithCoord, + WithDist: cmd.opt.WithDist, + WithHash: cmd.opt.WithHash, + } + } + var val []GeoLocation + if cmd.val != nil { + val = make([]GeoLocation, len(cmd.val)) + copy(val, cmd.val) + } + return &GeoSearchLocationCmd{ + baseCmd: cmd.cloneBaseCmd(), + opt: opt, + val: val, + } +} + //------------------------------------------------------------------------------ type GeoPos struct { @@ -3351,8 +4798,9 @@ var _ Cmder = (*GeoPosCmd)(nil) func NewGeoPosCmd(ctx context.Context, args ...interface{}) *GeoPosCmd { return &GeoPosCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeGeoPos, }, } } @@ -3408,17 +4856,37 @@ func (cmd *GeoPosCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *GeoPosCmd) Clone() Cmder { + var val []*GeoPos + if cmd.val != nil { + val = make([]*GeoPos, len(cmd.val)) + for i, pos := range cmd.val { + if pos != nil { + val[i] = &GeoPos{ + Longitude: pos.Longitude, + Latitude: pos.Latitude, + } + } + } + } + return &GeoPosCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type CommandInfo struct { - Name string - Arity int8 - Flags []string - ACLFlags []string - FirstKeyPos int8 - LastKeyPos int8 - StepCount int8 - ReadOnly bool + Name string + Arity int8 + Flags []string + ACLFlags []string + FirstKeyPos int8 + LastKeyPos int8 + StepCount int8 + ReadOnly bool + CommandPolicy *routing.CommandPolicy } type CommandsInfoCmd struct { @@ -3432,8 +4900,9 @@ var _ Cmder = (*CommandsInfoCmd)(nil) func NewCommandsInfoCmd(ctx context.Context, args ...interface{}) *CommandsInfoCmd { return &CommandsInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeCommandsInfo, }, } } @@ -3457,7 +4926,7 @@ func (cmd *CommandsInfoCmd) String() string { func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { const numArgRedis5 = 6 const numArgRedis6 = 7 - const numArgRedis7 = 10 + const numArgRedis7 = 10 // Also matches redis 8 n, err := rd.ReadArrayLen() if err != nil { @@ -3545,9 +5014,33 @@ func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { } if nn >= numArgRedis7 { - if err := rd.DiscardNext(); err != nil { + // The 8th argument is an array of tips. + tipsLen, err := rd.ReadArrayLen() + if err != nil { return err } + + rawTips := make(map[string]string, tipsLen) + if cmdInfo.ReadOnly { + rawTips[routing.ReadOnlyCMD] = "" + } + for f := 0; f < tipsLen; f++ { + tip, err := rd.ReadString() + if err != nil { + return err + } + + k, v, ok := strings.Cut(tip, ":") + if !ok { + // Handle tips that don't have a colon (like "nondeterministic_output") + rawTips[tip] = "" + } else { + // Handle normal key:value tips + rawTips[k] = v + } + } + cmdInfo.CommandPolicy = parseCommandPolicies(rawTips, cmdInfo.FirstKeyPos) + if err := rd.DiscardNext(); err != nil { return err } @@ -3562,13 +5055,47 @@ func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { return nil } -//------------------------------------------------------------------------------ - -type cmdsInfoCache struct { - fn func(ctx context.Context) (map[string]*CommandInfo, error) - - once internal.Once - cmds map[string]*CommandInfo +func (cmd *CommandsInfoCmd) Clone() Cmder { + var val map[string]*CommandInfo + if cmd.val != nil { + val = make(map[string]*CommandInfo, len(cmd.val)) + for k, v := range cmd.val { + if v != nil { + newInfo := &CommandInfo{ + Name: v.Name, + Arity: v.Arity, + FirstKeyPos: v.FirstKeyPos, + LastKeyPos: v.LastKeyPos, + StepCount: v.StepCount, + ReadOnly: v.ReadOnly, + CommandPolicy: v.CommandPolicy, // CommandPolicy can be shared as it's immutable + } + if v.Flags != nil { + newInfo.Flags = make([]string, len(v.Flags)) + copy(newInfo.Flags, v.Flags) + } + if v.ACLFlags != nil { + newInfo.ACLFlags = make([]string, len(v.ACLFlags)) + copy(newInfo.ACLFlags, v.ACLFlags) + } + val[k] = newInfo + } + } + } + return &CommandsInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//------------------------------------------------------------------------------ + +type cmdsInfoCache struct { + fn func(ctx context.Context) (map[string]*CommandInfo, error) + + once internal.Once + refreshLock sync.RWMutex + cmds map[string]*CommandInfo } func newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, error)) *cmdsInfoCache { @@ -3578,26 +5105,82 @@ func newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, err } func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error) { + c.refreshLock.Lock() + defer c.refreshLock.Unlock() + err := c.once.Do(func() error { cmds, err := c.fn(ctx) if err != nil { return err } + lowerCmds := make(map[string]*CommandInfo, len(cmds)) + // Extensions have cmd names in upper case. Convert them to lower case. for k, v := range cmds { - lower := internal.ToLower(k) - if lower != k { - cmds[lower] = v - } + lowerCmds[internal.ToLower(k)] = v } - c.cmds = cmds + c.cmds = lowerCmds return nil }) return c.cmds, err } +func (c *cmdsInfoCache) Refresh() { + c.refreshLock.Lock() + defer c.refreshLock.Unlock() + + c.once = internal.Once{} +} + +// Peek returns the cached CommandInfo map without triggering a Redis round-trip. +// Returns nil when the cache is cold; callers should fall back to other heuristics. +// Note: during the very first Get() (initial population) this call will block on +// the writer lock. After that, concurrent Peek() calls do not block each other. +// The returned map and its entries MUST NOT be mutated by the caller. +func (c *cmdsInfoCache) Peek() map[string]*CommandInfo { + if c == nil { + return nil + } + c.refreshLock.RLock() + defer c.refreshLock.RUnlock() + return c.cmds +} + +// ------------------------------------------------------------------------------ +const ( + requestPolicy = "request_policy" + responsePolicy = "response_policy" +) + +func parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy { + req := routing.ReqDefault + resp := routing.RespDefaultKeyless + if firstKeyPos > 0 { + resp = routing.RespDefaultHashSlot + } + + tips := make(map[string]string, len(commandInfoTips)) + for k, v := range commandInfoTips { + if k == requestPolicy { + if p, err := routing.ParseRequestPolicy(v); err == nil { + req = p + } + continue + } + if k == responsePolicy { + if p, err := routing.ParseResponsePolicy(v); err == nil { + resp = p + } + continue + } + tips[k] = v + } + + return &routing.CommandPolicy{Request: req, Response: resp, Tips: tips} +} + //------------------------------------------------------------------------------ type SlowLog struct { @@ -3622,8 +5205,9 @@ var _ Cmder = (*SlowLogCmd)(nil) func NewSlowLogCmd(ctx context.Context, args ...interface{}) *SlowLogCmd { return &SlowLogCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeSlowLog, }, } } @@ -3708,6 +5292,356 @@ func (cmd *SlowLogCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *SlowLogCmd) Clone() Cmder { + var val []SlowLog + if cmd.val != nil { + val = make([]SlowLog, len(cmd.val)) + for i, log := range cmd.val { + val[i] = SlowLog{ + ID: log.ID, + Time: log.Time, + Duration: log.Duration, + ClientAddr: log.ClientAddr, + ClientName: log.ClientName, + } + if log.Args != nil { + val[i].Args = make([]string, len(log.Args)) + copy(val[i].Args, log.Args) + } + } + } + return &SlowLogCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//----------------------------------------------------------------------- + +type Latency struct { + Name string + Time time.Time + Latest time.Duration + Max time.Duration +} + +type LatencyCmd struct { + baseCmd + val []Latency +} + +var _ Cmder = (*LatencyCmd)(nil) + +func NewLatencyCmd(ctx context.Context, args ...interface{}) *LatencyCmd { + return &LatencyCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *LatencyCmd) SetVal(val []Latency) { + cmd.val = val +} + +func (cmd *LatencyCmd) Val() []Latency { + return cmd.val +} + +func (cmd *LatencyCmd) Result() ([]Latency, error) { + return cmd.val, cmd.err +} + +func (cmd *LatencyCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *LatencyCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val = make([]Latency, n) + for i := 0; i < len(cmd.val); i++ { + nn, err := rd.ReadArrayLen() + if err != nil { + return err + } + if nn < 3 { + return fmt.Errorf("redis: got %d elements in latency get, expected at least 3", nn) + } + if cmd.val[i].Name, err = rd.ReadString(); err != nil { + return err + } + createdAt, err := rd.ReadInt() + if err != nil { + return err + } + cmd.val[i].Time = time.Unix(createdAt, 0) + latest, err := rd.ReadInt() + if err != nil { + return err + } + cmd.val[i].Latest = time.Duration(latest) * time.Millisecond + maximum, err := rd.ReadInt() + if err != nil { + return err + } + cmd.val[i].Max = time.Duration(maximum) * time.Millisecond + } + return nil +} + +func (cmd *LatencyCmd) Clone() Cmder { + var val []Latency + if cmd.val != nil { + val = make([]Latency, len(cmd.val)) + copy(val, cmd.val) + } + return &LatencyCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//----------------------------------------------------------------------- + +// HotKeysSlotRange represents a slot or slot range in the response. +// Single element slice = individual slot, two element slice = slot range [start, end]. +type HotKeysSlotRange []int64 + +// HotKeysKeyEntry represents a hot key entry with its metric value. +type HotKeysKeyEntry struct { + Key string + Value interface{} // Can be int64 or string +} + +// HotKeysResult represents the response data from HOTKEYS GET command. +// Field names match the Redis response format. +type HotKeysResult struct { + TrackingActive bool + SampleRatio uint8 + SelectedSlots []HotKeysSlotRange + SampledCommandsSelectedSlots time.Duration // Present when sample-ratio > 1 and selected-slots is not empty + AllCommandsSelectedSlots time.Duration // Present when selected-slots is not empty + AllCommandsAllSlots time.Duration + NetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty + NetBytesAllCommandsSelectedSlots int64 // Present when selected-slots is not empty + NetBytesAllCommandsAllSlots int64 + CollectionStartTime time.Time + CollectionDuration time.Duration + UsedCPUSys time.Duration + UsedCPUUser time.Duration + TotalNetBytes int64 + ByCPUTime []HotKeysKeyEntry + ByNetBytes []HotKeysKeyEntry +} + +type HotKeysCmd struct { + baseCmd + + val *HotKeysResult +} + +var _ Cmder = (*HotKeysCmd)(nil) + +func NewHotKeysCmd(ctx context.Context, args ...interface{}) *HotKeysCmd { + return &HotKeysCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeHotKeys, + }, + } +} + +func (cmd *HotKeysCmd) SetVal(val *HotKeysResult) { + cmd.val = val +} + +func (cmd *HotKeysCmd) Val() *HotKeysResult { + return cmd.val +} + +func (cmd *HotKeysCmd) Result() (*HotKeysResult, error) { + return cmd.val, cmd.err +} + +func (cmd *HotKeysCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *HotKeysCmd) readReply(rd *proto.Reader) error { + // HOTKEYS GET response is wrapped in an array for aggregation support + arrayLen, err := rd.ReadArrayLen() + if err != nil { + return err + } + + if arrayLen == 0 { + // Empty array means no tracking was started or after reset + cmd.val = nil + return nil + } + + // Read the first (and typically only) element which is a map + n, err := rd.ReadMapLen() + if err != nil { + return err + } + + result := &HotKeysResult{} + data := make(map[string]interface{}, n) + + for i := 0; i < n; i++ { + k, err := rd.ReadString() + if err != nil { + return err + } + v, err := rd.ReadReply() + if err != nil { + if err == Nil { + data[k] = Nil + continue + } + if err, ok := err.(proto.RedisError); ok { + data[k] = err + continue + } + return err + } + data[k] = v + } + + if v, ok := data["tracking-active"].(int64); ok { + result.TrackingActive = v == 1 + } + if v, ok := data["sample-ratio"].(int64); ok { + result.SampleRatio = uint8(v) + } + if v, ok := data["selected-slots"].([]interface{}); ok { + result.SelectedSlots = make([]HotKeysSlotRange, 0, len(v)) + for _, slot := range v { + switch s := slot.(type) { + case int64: + // Single slot + result.SelectedSlots = append(result.SelectedSlots, HotKeysSlotRange{s}) + case []interface{}: + // Slot range + slotRange := make(HotKeysSlotRange, 0, len(s)) + for _, sr := range s { + if val, ok := sr.(int64); ok { + slotRange = append(slotRange, val) + } + } + result.SelectedSlots = append(result.SelectedSlots, slotRange) + } + } + } + if v, ok := data["sampled-commands-selected-slots-us"].(int64); ok { + result.SampledCommandsSelectedSlots = time.Duration(v) * time.Microsecond + } + if v, ok := data["all-commands-selected-slots-us"].(int64); ok { + result.AllCommandsSelectedSlots = time.Duration(v) * time.Microsecond + } + if v, ok := data["all-commands-all-slots-us"].(int64); ok { + result.AllCommandsAllSlots = time.Duration(v) * time.Microsecond + } + if v, ok := data["net-bytes-sampled-commands-selected-slots"].(int64); ok { + result.NetBytesSampledCommandsSelectedSlots = v + } + if v, ok := data["net-bytes-all-commands-selected-slots"].(int64); ok { + result.NetBytesAllCommandsSelectedSlots = v + } + if v, ok := data["net-bytes-all-commands-all-slots"].(int64); ok { + result.NetBytesAllCommandsAllSlots = v + } + if v, ok := data["collection-start-time-unix-ms"].(int64); ok { + result.CollectionStartTime = time.UnixMilli(v) + } + if v, ok := data["collection-duration-ms"].(int64); ok { + result.CollectionDuration = time.Duration(v) * time.Millisecond + } + if v, ok := data["used-cpu-sys-ms"].(int64); ok { + result.UsedCPUSys = time.Duration(v) * time.Millisecond + } + if v, ok := data["used-cpu-user-ms"].(int64); ok { + result.UsedCPUUser = time.Duration(v) * time.Millisecond + } + if v, ok := data["total-net-bytes"].(int64); ok { + result.TotalNetBytes = v + } + + if v, ok := data["by-cpu-time-us"].([]interface{}); ok { + result.ByCPUTime = parseHotKeysKeyEntries(v) + } + + if v, ok := data["by-net-bytes"].([]interface{}); ok { + result.ByNetBytes = parseHotKeysKeyEntries(v) + } + + cmd.val = result + return nil +} + +// parseHotKeysKeyEntries parses the key-value pairs from HOTKEYS GET response. +func parseHotKeysKeyEntries(v []interface{}) []HotKeysKeyEntry { + entries := make([]HotKeysKeyEntry, 0, len(v)/2) + for i := 0; i < len(v); i += 2 { + if i+1 < len(v) { + key, keyOk := v[i].(string) + if keyOk { + entries = append(entries, HotKeysKeyEntry{ + Key: key, + Value: v[i+1], // Can be int64 or string + }) + } + } + } + return entries +} + +func (cmd *HotKeysCmd) Clone() Cmder { + var val *HotKeysResult + if cmd.val != nil { + val = &HotKeysResult{ + TrackingActive: cmd.val.TrackingActive, + SampleRatio: cmd.val.SampleRatio, + SampledCommandsSelectedSlots: cmd.val.SampledCommandsSelectedSlots, + AllCommandsSelectedSlots: cmd.val.AllCommandsSelectedSlots, + AllCommandsAllSlots: cmd.val.AllCommandsAllSlots, + NetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots, + NetBytesAllCommandsSelectedSlots: cmd.val.NetBytesAllCommandsSelectedSlots, + NetBytesAllCommandsAllSlots: cmd.val.NetBytesAllCommandsAllSlots, + CollectionStartTime: cmd.val.CollectionStartTime, + CollectionDuration: cmd.val.CollectionDuration, + UsedCPUSys: cmd.val.UsedCPUSys, + UsedCPUUser: cmd.val.UsedCPUUser, + TotalNetBytes: cmd.val.TotalNetBytes, + } + if cmd.val.SelectedSlots != nil { + val.SelectedSlots = make([]HotKeysSlotRange, len(cmd.val.SelectedSlots)) + for i, sr := range cmd.val.SelectedSlots { + val.SelectedSlots[i] = make(HotKeysSlotRange, len(sr)) + copy(val.SelectedSlots[i], sr) + } + } + if cmd.val.ByCPUTime != nil { + val.ByCPUTime = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTime)) + copy(val.ByCPUTime, cmd.val.ByCPUTime) + } + if cmd.val.ByNetBytes != nil { + val.ByNetBytes = make([]HotKeysKeyEntry, len(cmd.val.ByNetBytes)) + copy(val.ByNetBytes, cmd.val.ByNetBytes) + } + } + return &HotKeysCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //----------------------------------------------------------------------- type MapStringInterfaceCmd struct { @@ -3721,8 +5655,9 @@ var _ Cmder = (*MapStringInterfaceCmd)(nil) func NewMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceCmd { return &MapStringInterfaceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringInterface, }, } } @@ -3772,6 +5707,20 @@ func (cmd *MapStringInterfaceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *MapStringInterfaceCmd) Clone() Cmder { + var val map[string]interface{} + if cmd.val != nil { + val = make(map[string]interface{}, len(cmd.val)) + for k, v := range cmd.val { + val[k] = v + } + } + return &MapStringInterfaceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //----------------------------------------------------------------------- type MapStringStringSliceCmd struct { @@ -3785,8 +5734,9 @@ var _ Cmder = (*MapStringStringSliceCmd)(nil) func NewMapStringStringSliceCmd(ctx context.Context, args ...interface{}) *MapStringStringSliceCmd { return &MapStringStringSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringStringSlice, }, } } @@ -3836,6 +5786,25 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *MapStringStringSliceCmd) Clone() Cmder { + var val []map[string]string + if cmd.val != nil { + val = make([]map[string]string, len(cmd.val)) + for i, m := range cmd.val { + if m != nil { + val[i] = make(map[string]string, len(m)) + for k, v := range m { + val[i][k] = v + } + } + } + } + return &MapStringStringSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // ----------------------------------------------------------------------- // MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}. @@ -3847,8 +5816,9 @@ type MapMapStringInterfaceCmd struct { func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd { return &MapMapStringInterfaceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapMapStringInterface, }, } } @@ -3914,6 +5884,20 @@ func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *MapMapStringInterfaceCmd) Clone() Cmder { + var val map[string]interface{} + if cmd.val != nil { + val = make(map[string]interface{}, len(cmd.val)) + for k, v := range cmd.val { + val[k] = v + } + } + return &MapMapStringInterfaceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //----------------------------------------------------------------------- type MapStringInterfaceSliceCmd struct { @@ -3927,8 +5911,9 @@ var _ Cmder = (*MapStringInterfaceSliceCmd)(nil) func NewMapStringInterfaceSliceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceSliceCmd { return &MapStringInterfaceSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeMapStringInterfaceSlice, }, } } @@ -3979,6 +5964,25 @@ func (cmd *MapStringInterfaceSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *MapStringInterfaceSliceCmd) Clone() Cmder { + var val []map[string]interface{} + if cmd.val != nil { + val = make([]map[string]interface{}, len(cmd.val)) + for i, m := range cmd.val { + if m != nil { + val[i] = make(map[string]interface{}, len(m)) + for k, v := range m { + val[i][k] = v + } + } + } + } + return &MapStringInterfaceSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type KeyValuesCmd struct { @@ -3993,8 +5997,9 @@ var _ Cmder = (*KeyValuesCmd)(nil) func NewKeyValuesCmd(ctx context.Context, args ...interface{}) *KeyValuesCmd { return &KeyValuesCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeKeyValues, }, } } @@ -4041,6 +6046,19 @@ func (cmd *KeyValuesCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *KeyValuesCmd) Clone() Cmder { + var val []string + if cmd.val != nil { + val = make([]string, len(cmd.val)) + copy(val, cmd.val) + } + return &KeyValuesCmd{ + baseCmd: cmd.cloneBaseCmd(), + key: cmd.key, + val: val, + } +} + //------------------------------------------------------------------------------ type ZSliceWithKeyCmd struct { @@ -4055,8 +6073,9 @@ var _ Cmder = (*ZSliceWithKeyCmd)(nil) func NewZSliceWithKeyCmd(ctx context.Context, args ...interface{}) *ZSliceWithKeyCmd { return &ZSliceWithKeyCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeZSliceWithKey, }, } } @@ -4124,6 +6143,19 @@ func (cmd *ZSliceWithKeyCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *ZSliceWithKeyCmd) Clone() Cmder { + var val []Z + if cmd.val != nil { + val = make([]Z, len(cmd.val)) + copy(val, cmd.val) + } + return &ZSliceWithKeyCmd{ + baseCmd: cmd.cloneBaseCmd(), + key: cmd.key, + val: val, + } +} + type Function struct { Name string Description string @@ -4148,8 +6180,9 @@ var _ Cmder = (*FunctionListCmd)(nil) func NewFunctionListCmd(ctx context.Context, args ...interface{}) *FunctionListCmd { return &FunctionListCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFunctionList, }, } } @@ -4276,7 +6309,38 @@ func (cmd *FunctionListCmd) readFunctions(rd *proto.Reader) ([]Function, error) return functions, nil } -// FunctionStats contains information about the scripts currently executing on the server, and the available engines +func (cmd *FunctionListCmd) Clone() Cmder { + var val []Library + if cmd.val != nil { + val = make([]Library, len(cmd.val)) + for i, lib := range cmd.val { + val[i] = Library{ + Name: lib.Name, + Engine: lib.Engine, + Code: lib.Code, + } + if lib.Functions != nil { + val[i].Functions = make([]Function, len(lib.Functions)) + for j, fn := range lib.Functions { + val[i].Functions[j] = Function{ + Name: fn.Name, + Description: fn.Description, + } + if fn.Flags != nil { + val[i].Functions[j].Flags = make([]string, len(fn.Flags)) + copy(val[i].Functions[j].Flags, fn.Flags) + } + } + } + } + } + return &FunctionListCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +// FunctionStats contains information about the scripts currently executing on the server, and the available engines // - Engines: // Statistics about the engine like number of functions and number of libraries // - RunningScript: @@ -4329,8 +6393,9 @@ var _ Cmder = (*FunctionStatsCmd)(nil) func NewFunctionStatsCmd(ctx context.Context, args ...interface{}) *FunctionStatsCmd { return &FunctionStatsCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFunctionStats, }, } } @@ -4501,6 +6566,34 @@ func (cmd *FunctionStatsCmd) readRunningScripts(rd *proto.Reader) ([]RunningScri return runningScripts, len(runningScripts) > 0, nil } +func (cmd *FunctionStatsCmd) Clone() Cmder { + val := FunctionStats{ + isRunning: cmd.val.isRunning, + rs: cmd.val.rs, // RunningScript is a simple struct, can be copied directly + } + if cmd.val.Engines != nil { + val.Engines = make([]Engine, len(cmd.val.Engines)) + copy(val.Engines, cmd.val.Engines) + } + if cmd.val.allrs != nil { + val.allrs = make([]RunningScript, len(cmd.val.allrs)) + for i, rs := range cmd.val.allrs { + val.allrs[i] = RunningScript{ + Name: rs.Name, + Duration: rs.Duration, + } + if rs.Command != nil { + val.allrs[i].Command = make([]string, len(rs.Command)) + copy(val.allrs[i].Command, rs.Command) + } + } + } + return &FunctionStatsCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ // LCSQuery is a parameter used for the LCS command @@ -4564,8 +6657,9 @@ func NewLCSCmd(ctx context.Context, q *LCSQuery) *LCSCmd { } } cmd.baseCmd = baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeLCS, } return cmd @@ -4677,6 +6771,25 @@ func (cmd *LCSCmd) readPosition(rd *proto.Reader) (pos LCSPosition, err error) { return pos, nil } +func (cmd *LCSCmd) Clone() Cmder { + var val *LCSMatch + if cmd.val != nil { + val = &LCSMatch{ + MatchString: cmd.val.MatchString, + Len: cmd.val.Len, + } + if cmd.val.Matches != nil { + val.Matches = make([]LCSMatchedPosition, len(cmd.val.Matches)) + copy(val.Matches, cmd.val.Matches) + } + } + return &LCSCmd{ + baseCmd: cmd.cloneBaseCmd(), + readType: cmd.readType, + val: val, + } +} + // ------------------------------------------------------------------------ type KeyFlags struct { @@ -4695,8 +6808,9 @@ var _ Cmder = (*KeyFlagsCmd)(nil) func NewKeyFlagsCmd(ctx context.Context, args ...interface{}) *KeyFlagsCmd { return &KeyFlagsCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeKeyFlags, }, } } @@ -4755,6 +6869,26 @@ func (cmd *KeyFlagsCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *KeyFlagsCmd) Clone() Cmder { + var val []KeyFlags + if cmd.val != nil { + val = make([]KeyFlags, len(cmd.val)) + for i, kf := range cmd.val { + val[i] = KeyFlags{ + Key: kf.Key, + } + if kf.Flags != nil { + val[i].Flags = make([]string, len(kf.Flags)) + copy(val[i].Flags, kf.Flags) + } + } + } + return &KeyFlagsCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // --------------------------------------------------------------------------------------------------- type ClusterLink struct { @@ -4777,8 +6911,9 @@ var _ Cmder = (*ClusterLinksCmd)(nil) func NewClusterLinksCmd(ctx context.Context, args ...interface{}) *ClusterLinksCmd { return &ClusterLinksCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeClusterLinks, }, } } @@ -4844,6 +6979,18 @@ func (cmd *ClusterLinksCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *ClusterLinksCmd) Clone() Cmder { + var val []ClusterLink + if cmd.val != nil { + val = make([]ClusterLink, len(cmd.val)) + copy(val, cmd.val) + } + return &ClusterLinksCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // ------------------------------------------------------------------------------------------------------------------ type SlotRange struct { @@ -4879,8 +7026,9 @@ var _ Cmder = (*ClusterShardsCmd)(nil) func NewClusterShardsCmd(ctx context.Context, args ...interface{}) *ClusterShardsCmd { return &ClusterShardsCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeClusterShards, }, } } @@ -4977,7 +7125,9 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { case "health": cmd.val[i].Nodes[k].Health, err = rd.ReadString() default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS node reply", nodeKey) + if err = rd.DiscardNext(); err != nil { + return err + } } if err != nil { @@ -4986,7 +7136,9 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { } } default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS reply", key) + if err = rd.DiscardNext(); err != nil { + return err + } } } } @@ -4994,6 +7146,28 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *ClusterShardsCmd) Clone() Cmder { + var val []ClusterShard + if cmd.val != nil { + val = make([]ClusterShard, len(cmd.val)) + for i, shard := range cmd.val { + val[i] = ClusterShard{} + if shard.Slots != nil { + val[i].Slots = make([]SlotRange, len(shard.Slots)) + copy(val[i].Slots, shard.Slots) + } + if shard.Nodes != nil { + val[i].Nodes = make([]Node, len(shard.Nodes)) + copy(val[i].Nodes, shard.Nodes) + } + } + } + return &ClusterShardsCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // ----------------------------------------- type RankScore struct { @@ -5012,8 +7186,9 @@ var _ Cmder = (*RankWithScoreCmd)(nil) func NewRankWithScoreCmd(ctx context.Context, args ...interface{}) *RankWithScoreCmd { return &RankWithScoreCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeRankWithScore, }, } } @@ -5054,6 +7229,13 @@ func (cmd *RankWithScoreCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *RankWithScoreCmd) Clone() Cmder { + return &RankWithScoreCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // RankScore is a simple struct, can be copied directly + } +} + // -------------------------------------------------------------------------------------------------- // ClientFlags is redis-server client flags, copy from redis/src/server.h (redis 7.0) @@ -5139,6 +7321,9 @@ type ClientInfo struct { OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full) OutputMemory int // omem, output buffer memory usage TotalMemory int // tot-mem, total memory consumed by this client in its various buffers + TotalNetIn int // tot-net-in, total network input + TotalNetOut int // tot-net-out, total network output + TotalCmds int // tot-cmds, total number of commands processed IoThread int // io-thread id Events string // file descriptor events (see below) LastCmd string // cmd, last command played @@ -5147,6 +7332,9 @@ type ClientInfo struct { Resp int // redis version 7.0, client RESP protocol version LibName string // redis version 7.2, client library name LibVer string // redis version 7.2, client library version + ReadEvents uint64 // redis version 8.8, number of read events processed + AvgPipelineLenSum uint64 // redis version 8.8, sum of pipeline lengths + AvgPipelineLenCnt uint64 // redis version 8.8, count of pipeline operations } type ClientInfoCmd struct { @@ -5160,8 +7348,9 @@ var _ Cmder = (*ClientInfoCmd)(nil) func NewClientInfoCmd(ctx context.Context, args ...interface{}) *ClientInfoCmd { return &ClientInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeClientInfo, }, } } @@ -5304,6 +7493,12 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.OutputMemory, err = strconv.Atoi(val) case "tot-mem": info.TotalMemory, err = strconv.Atoi(val) + case "tot-net-in": + info.TotalNetIn, err = strconv.Atoi(val) + case "tot-net-out": + info.TotalNetOut, err = strconv.Atoi(val) + case "tot-cmds": + info.TotalCmds, err = strconv.Atoi(val) case "events": info.Events = val case "cmd": @@ -5320,8 +7515,14 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.LibVer = val case "io-thread": info.IoThread, err = strconv.Atoi(val) + case "read-events": + info.ReadEvents, err = strconv.ParseUint(val, 10, 64) + case "avg-pipeline-len-sum": + info.AvgPipelineLenSum, err = strconv.ParseUint(val, 10, 64) + case "avg-pipeline-len-cnt": + info.AvgPipelineLenCnt, err = strconv.ParseUint(val, 10, 64) default: - return nil, fmt.Errorf("redis: unexpected client info key(%s)", key) + // skip unknown fields } if err != nil { @@ -5332,6 +7533,53 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { return info, nil } +func (cmd *ClientInfoCmd) Clone() Cmder { + var val *ClientInfo + if cmd.val != nil { + val = &ClientInfo{ + ID: cmd.val.ID, + Addr: cmd.val.Addr, + LAddr: cmd.val.LAddr, + FD: cmd.val.FD, + Name: cmd.val.Name, + Age: cmd.val.Age, + Idle: cmd.val.Idle, + Flags: cmd.val.Flags, + DB: cmd.val.DB, + Sub: cmd.val.Sub, + PSub: cmd.val.PSub, + SSub: cmd.val.SSub, + Multi: cmd.val.Multi, + Watch: cmd.val.Watch, + QueryBuf: cmd.val.QueryBuf, + QueryBufFree: cmd.val.QueryBufFree, + ArgvMem: cmd.val.ArgvMem, + MultiMem: cmd.val.MultiMem, + BufferSize: cmd.val.BufferSize, + BufferPeak: cmd.val.BufferPeak, + OutputBufferLength: cmd.val.OutputBufferLength, + OutputListLength: cmd.val.OutputListLength, + OutputMemory: cmd.val.OutputMemory, + TotalMemory: cmd.val.TotalMemory, + IoThread: cmd.val.IoThread, + Events: cmd.val.Events, + LastCmd: cmd.val.LastCmd, + User: cmd.val.User, + Redir: cmd.val.Redir, + Resp: cmd.val.Resp, + LibName: cmd.val.LibName, + LibVer: cmd.val.LibVer, + ReadEvents: cmd.val.ReadEvents, + AvgPipelineLenSum: cmd.val.AvgPipelineLenSum, + AvgPipelineLenCnt: cmd.val.AvgPipelineLenCnt, + } + } + return &ClientInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // ------------------------------------------- type ACLLogEntry struct { @@ -5358,8 +7606,9 @@ var _ Cmder = (*ACLLogCmd)(nil) func NewACLLogCmd(ctx context.Context, args ...interface{}) *ACLLogCmd { return &ACLLogCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeACLLog, }, } } @@ -5429,7 +7678,10 @@ func (cmd *ACLLogCmd) readReply(rd *proto.Reader) error { case "timestamp-last-updated": entry.TimestampLastUpdated, err = rd.ReadInt() default: - return fmt.Errorf("redis: unexpected key %q in ACL LOG reply", key) + // skip unknown fields + if err := rd.DiscardNext(); err != nil { + return err + } } if err != nil { @@ -5441,6 +7693,72 @@ func (cmd *ACLLogCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *ACLLogCmd) Clone() Cmder { + var val []*ACLLogEntry + if cmd.val != nil { + val = make([]*ACLLogEntry, len(cmd.val)) + for i, entry := range cmd.val { + if entry != nil { + val[i] = &ACLLogEntry{ + Count: entry.Count, + Reason: entry.Reason, + Context: entry.Context, + Object: entry.Object, + Username: entry.Username, + AgeSeconds: entry.AgeSeconds, + EntryID: entry.EntryID, + TimestampCreated: entry.TimestampCreated, + TimestampLastUpdated: entry.TimestampLastUpdated, + } + // Clone ClientInfo if present + if entry.ClientInfo != nil { + val[i].ClientInfo = &ClientInfo{ + ID: entry.ClientInfo.ID, + Addr: entry.ClientInfo.Addr, + LAddr: entry.ClientInfo.LAddr, + FD: entry.ClientInfo.FD, + Name: entry.ClientInfo.Name, + Age: entry.ClientInfo.Age, + Idle: entry.ClientInfo.Idle, + Flags: entry.ClientInfo.Flags, + DB: entry.ClientInfo.DB, + Sub: entry.ClientInfo.Sub, + PSub: entry.ClientInfo.PSub, + SSub: entry.ClientInfo.SSub, + Multi: entry.ClientInfo.Multi, + Watch: entry.ClientInfo.Watch, + QueryBuf: entry.ClientInfo.QueryBuf, + QueryBufFree: entry.ClientInfo.QueryBufFree, + ArgvMem: entry.ClientInfo.ArgvMem, + MultiMem: entry.ClientInfo.MultiMem, + BufferSize: entry.ClientInfo.BufferSize, + BufferPeak: entry.ClientInfo.BufferPeak, + OutputBufferLength: entry.ClientInfo.OutputBufferLength, + OutputListLength: entry.ClientInfo.OutputListLength, + OutputMemory: entry.ClientInfo.OutputMemory, + TotalMemory: entry.ClientInfo.TotalMemory, + IoThread: entry.ClientInfo.IoThread, + Events: entry.ClientInfo.Events, + LastCmd: entry.ClientInfo.LastCmd, + User: entry.ClientInfo.User, + Redir: entry.ClientInfo.Redir, + Resp: entry.ClientInfo.Resp, + LibName: entry.ClientInfo.LibName, + LibVer: entry.ClientInfo.LibVer, + ReadEvents: entry.ClientInfo.ReadEvents, + AvgPipelineLenSum: entry.ClientInfo.AvgPipelineLenSum, + AvgPipelineLenCnt: entry.ClientInfo.AvgPipelineLenCnt, + } + } + } + } + } + return &ACLLogCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // LibraryInfo holds the library info. type LibraryInfo struct { LibName *string @@ -5469,8 +7787,9 @@ var _ Cmder = (*InfoCmd)(nil) func NewInfoCmd(ctx context.Context, args ...interface{}) *InfoCmd { return &InfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeInfo, }, } } @@ -5536,6 +7855,25 @@ func (cmd *InfoCmd) Item(section, key string) string { } } +func (cmd *InfoCmd) Clone() Cmder { + var val map[string]map[string]string + if cmd.val != nil { + val = make(map[string]map[string]string, len(cmd.val)) + for section, sectionMap := range cmd.val { + if sectionMap != nil { + val[section] = make(map[string]string, len(sectionMap)) + for k, v := range sectionMap { + val[section][k] = v + } + } + } + } + return &InfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + type MonitorStatus int const ( @@ -5554,8 +7892,9 @@ type MonitorCmd struct { func newMonitorCmd(ctx context.Context, ch chan string) *MonitorCmd { return &MonitorCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"monitor"}, + ctx: ctx, + args: []interface{}{"monitor"}, + cmdType: CmdTypeMonitor, }, ch: ch, status: monitorStatusIdle, @@ -5629,7 +7968,7 @@ type VectorScoreSliceCmd struct { var _ Cmder = (*VectorScoreSliceCmd)(nil) -func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { +func NewVectorScoreSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { return &VectorScoreSliceCmd{ baseCmd: baseCmd{ ctx: ctx, @@ -5638,6 +7977,11 @@ func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCm } } +// NewVectorInfoSliceCmd is an alias for NewVectorScoreSliceCmd kept for backwards compatibility. +func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { + return NewVectorScoreSliceCmd(ctx, args...) +} + func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) { cmd.val = val } @@ -5655,11 +7999,29 @@ func (cmd *VectorScoreSliceCmd) String() string { } func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() + typ, err := rd.PeekReplyType() if err != nil { return err } + var n int + if typ == proto.RespMap { + n, err = rd.ReadMapLen() + if err != nil { + return err + } + } else { + // RESP2 returns a flat array [name, score, name, score, ...] + n, err = rd.ReadArrayLen() + if err != nil { + return err + } + if n%2 != 0 { + return fmt.Errorf("redis: VectorScoreSliceCmd expects even number of elements, got %d", n) + } + n /= 2 + } + cmd.val = make([]VectorScore, n) for i := 0; i < n; i++ { name, err := rd.ReadString() @@ -5674,5 +8036,1081 @@ func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { } cmd.val[i].Score = score } + + return nil +} + +func (cmd *VectorScoreSliceCmd) Clone() Cmder { + return &VectorScoreSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +// VectorScoreSliceSliceCmd is used for VLINKS WITHSCORES which returns an array of arrays. +// In RESP3, each inner array contains maps of element -> score. +type VectorScoreSliceSliceCmd struct { + baseCmd + + val [][]VectorScore +} + +var _ Cmder = (*VectorScoreSliceSliceCmd)(nil) + +func NewVectorScoreSliceSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceSliceCmd { + return &VectorScoreSliceSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorScoreSliceSliceCmd) SetVal(val [][]VectorScore) { + cmd.val = val +} + +func (cmd *VectorScoreSliceSliceCmd) Val() [][]VectorScore { + return cmd.val +} + +func (cmd *VectorScoreSliceSliceCmd) Result() ([][]VectorScore, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorScoreSliceSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorScoreSliceSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + cmd.val = make([][]VectorScore, n) + for i := range n { + // Each level can be either a map (RESP3) or an array (RESP2) + levelTyp, err := rd.PeekReplyType() + if err != nil { + return err + } + + if levelTyp == proto.RespMap { + // RESP3 format: each level is a map {element: score, element: score, ...} + mapLen, err := rd.ReadMapLen() + if err != nil { + return err + } + + cmd.val[i] = make([]VectorScore, mapLen) + for j := range mapLen { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[i][j] = VectorScore{Name: name, Score: score} + } + } else { + // RESP2 format: each level is an array of [element, score, element, score, ...] pairs + innerLen, err := rd.ReadArrayLen() + if err != nil { + return err + } + + if innerLen%2 != 0 { + return fmt.Errorf("redis: got %d elements in the VLINKS array, wanted a multiple of 2", innerLen) + } + + cmd.val[i] = make([]VectorScore, innerLen/2) + for j := 0; j < innerLen; j += 2 { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[i][j/2] = VectorScore{Name: name, Score: score} + } + } + } + return nil } + +func (cmd *VectorScoreSliceSliceCmd) Clone() Cmder { + var val [][]VectorScore + if cmd.val != nil { + val = make([][]VectorScore, len(cmd.val)) + for i, slice := range cmd.val { + if slice != nil { + val[i] = make([]VectorScore, len(slice)) + copy(val[i], slice) + } + } + } + return &VectorScoreSliceSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +func readVectorAttribStringOrNil(rd *proto.Reader) (*string, error) { + v, err := rd.ReadReply() + if err != nil { + if err == proto.Nil { + return nil, nil + } + return nil, err + } + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("redis: can't parse reply=%T reading string", v) + } + return &s, nil +} + +type VectorAttribSliceCmd struct { + baseCmd + + val []VectorAttrib +} + +var _ Cmder = (*VectorAttribSliceCmd)(nil) + +func NewVectorAttribSliceCmd(ctx context.Context, args ...any) *VectorAttribSliceCmd { + return &VectorAttribSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorAttribSliceCmd) SetVal(val []VectorAttrib) { + cmd.val = val +} + +func (cmd *VectorAttribSliceCmd) Val() []VectorAttrib { + return cmd.val +} + +func (cmd *VectorAttribSliceCmd) Result() ([]VectorAttrib, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorAttribSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorAttribSliceCmd) readReply(rd *proto.Reader) error { + replyType, err := rd.PeekReplyType() + if err != nil { + return err + } + + if replyType == proto.RespMap { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + cmd.val = make([]VectorAttrib, n) + for i := 0; i < n; i++ { + name, err := rd.ReadString() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorAttrib{Name: name, Attribs: attrib} + } + return nil + } + + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n%2 != 0 { + return fmt.Errorf("redis: got %d elements in the VSIM array, wanted a multiple of 2", n) + } + cmd.val = make([]VectorAttrib, n/2) + for i := range cmd.val { + name, err := rd.ReadString() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorAttrib{Name: name, Attribs: attrib} + } + return nil +} + +func (cmd *VectorAttribSliceCmd) Clone() Cmder { + return &VectorAttribSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +type VectorScoreAttribSliceCmd struct { + baseCmd + + val []VectorScoreAttrib +} + +var _ Cmder = (*VectorScoreAttribSliceCmd)(nil) + +func NewVectorScoreAttribSliceCmd(ctx context.Context, args ...any) *VectorScoreAttribSliceCmd { + return &VectorScoreAttribSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorScoreAttribSliceCmd) SetVal(val []VectorScoreAttrib) { + cmd.val = val +} + +func (cmd *VectorScoreAttribSliceCmd) Val() []VectorScoreAttrib { + return cmd.val +} + +func (cmd *VectorScoreAttribSliceCmd) Result() ([]VectorScoreAttrib, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorScoreAttribSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorScoreAttribSliceCmd) readReply(rd *proto.Reader) error { + replyType, err := rd.PeekReplyType() + if err != nil { + return err + } + + if replyType == proto.RespMap { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + cmd.val = make([]VectorScoreAttrib, n) + for i := 0; i < n; i++ { + name, err := rd.ReadString() + if err != nil { + return err + } + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorScoreAttrib{Name: name, Score: score, Attribs: attrib} + } + return nil + } + + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n%3 != 0 { + return fmt.Errorf("redis: got %d elements in the VSIM array, wanted a multiple of 3", n) + } + cmd.val = make([]VectorScoreAttrib, n/3) + for i := range cmd.val { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorScoreAttrib{Name: name, Score: score, Attribs: attrib} + } + return nil +} + +func (cmd *VectorScoreAttribSliceCmd) Clone() Cmder { + return &VectorScoreAttribSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +func (cmd *MonitorCmd) Clone() Cmder { + // MonitorCmd cannot be safely cloned due to channels and goroutines + // Return a new MonitorCmd with the same channel + return newMonitorCmd(cmd.ctx, cmd.ch) +} + +// ExtractCommandValue extracts the value from a command result using the fast enum-based approach +func ExtractCommandValue(cmd interface{}) (interface{}, error) { + // First try to get the command type using the interface + if cmdTypeGetter, ok := cmd.(CmdTypeGetter); ok { + cmdType := cmdTypeGetter.GetCmdType() + + // Use fast type-based extraction + switch cmdType { + case CmdTypeGeneric: + if genericCmd, ok := cmd.(interface { + Val() interface{} + Err() error + }); ok { + return genericCmd.Val(), genericCmd.Err() + } + case CmdTypeString: + if stringCmd, ok := cmd.(interface { + Val() string + Err() error + }); ok { + return stringCmd.Val(), stringCmd.Err() + } + case CmdTypeInt: + if intCmd, ok := cmd.(interface { + Val() int64 + Err() error + }); ok { + return intCmd.Val(), intCmd.Err() + } + case CmdTypeUint: + if uintCmd, ok := cmd.(interface { + Val() uint64 + Err() error + }); ok { + return uintCmd.Val(), uintCmd.Err() + } + case CmdTypeBool: + if boolCmd, ok := cmd.(interface { + Val() bool + Err() error + }); ok { + return boolCmd.Val(), boolCmd.Err() + } + case CmdTypeFloat: + if floatCmd, ok := cmd.(interface { + Val() float64 + Err() error + }); ok { + return floatCmd.Val(), floatCmd.Err() + } + case CmdTypeStatus: + if statusCmd, ok := cmd.(interface { + Val() string + Err() error + }); ok { + return statusCmd.Val(), statusCmd.Err() + } + case CmdTypeDuration: + if durationCmd, ok := cmd.(interface { + Val() time.Duration + Err() error + }); ok { + return durationCmd.Val(), durationCmd.Err() + } + case CmdTypeTime: + if timeCmd, ok := cmd.(interface { + Val() time.Time + Err() error + }); ok { + return timeCmd.Val(), timeCmd.Err() + } + case CmdTypeStringStructMap: + if structMapCmd, ok := cmd.(interface { + Val() map[string]struct{} + Err() error + }); ok { + return structMapCmd.Val(), structMapCmd.Err() + } + case CmdTypeXMessageSlice: + if xMessageSliceCmd, ok := cmd.(interface { + Val() []XMessage + Err() error + }); ok { + return xMessageSliceCmd.Val(), xMessageSliceCmd.Err() + } + case CmdTypeXStreamSlice: + if xStreamSliceCmd, ok := cmd.(interface { + Val() []XStream + Err() error + }); ok { + return xStreamSliceCmd.Val(), xStreamSliceCmd.Err() + } + case CmdTypeXPending: + if xPendingCmd, ok := cmd.(interface { + Val() *XPending + Err() error + }); ok { + return xPendingCmd.Val(), xPendingCmd.Err() + } + case CmdTypeXPendingExt: + if xPendingExtCmd, ok := cmd.(interface { + Val() []XPendingExt + Err() error + }); ok { + return xPendingExtCmd.Val(), xPendingExtCmd.Err() + } + case CmdTypeXAutoClaim: + if xAutoClaimCmd, ok := cmd.(interface { + Val() ([]XMessage, string) + Err() error + }); ok { + messages, start := xAutoClaimCmd.Val() + return CmdTypeXAutoClaimValue{messages: messages, start: start}, xAutoClaimCmd.Err() + } + case CmdTypeXAutoClaimWithDeleted: + if xAutoClaimWithDeletedCmd, ok := cmd.(interface { + Val() ([]XMessage, string, []string) + Err() error + }); ok { + messages, start, deletedIDs := xAutoClaimWithDeletedCmd.Val() + return CmdTypeXAutoClaimWithDeletedValue{messages: messages, start: start, deletedIDs: deletedIDs}, xAutoClaimWithDeletedCmd.Err() + } + case CmdTypeXAutoClaimJustID: + if xAutoClaimJustIDCmd, ok := cmd.(interface { + Val() ([]string, string) + Err() error + }); ok { + ids, start := xAutoClaimJustIDCmd.Val() + return CmdTypeXAutoClaimJustIDValue{ids: ids, start: start}, xAutoClaimJustIDCmd.Err() + } + case CmdTypeXInfoConsumers: + if xInfoConsumersCmd, ok := cmd.(interface { + Val() []XInfoConsumer + Err() error + }); ok { + return xInfoConsumersCmd.Val(), xInfoConsumersCmd.Err() + } + case CmdTypeXInfoGroups: + if xInfoGroupsCmd, ok := cmd.(interface { + Val() []XInfoGroup + Err() error + }); ok { + return xInfoGroupsCmd.Val(), xInfoGroupsCmd.Err() + } + case CmdTypeXInfoStream: + if xInfoStreamCmd, ok := cmd.(interface { + Val() *XInfoStream + Err() error + }); ok { + return xInfoStreamCmd.Val(), xInfoStreamCmd.Err() + } + case CmdTypeXInfoStreamFull: + if xInfoStreamFullCmd, ok := cmd.(interface { + Val() *XInfoStreamFull + Err() error + }); ok { + return xInfoStreamFullCmd.Val(), xInfoStreamFullCmd.Err() + } + case CmdTypeZSlice: + if zSliceCmd, ok := cmd.(interface { + Val() []Z + Err() error + }); ok { + return zSliceCmd.Val(), zSliceCmd.Err() + } + case CmdTypeZWithKey: + if zWithKeyCmd, ok := cmd.(interface { + Val() *ZWithKey + Err() error + }); ok { + return zWithKeyCmd.Val(), zWithKeyCmd.Err() + } + case CmdTypeScan: + if scanCmd, ok := cmd.(interface { + Val() ([]string, uint64) + Err() error + }); ok { + keys, cursor := scanCmd.Val() + return CmdTypeScanValue{keys: keys, cursor: cursor}, scanCmd.Err() + } + case CmdTypeClusterSlots: + if clusterSlotsCmd, ok := cmd.(interface { + Val() []ClusterSlot + Err() error + }); ok { + return clusterSlotsCmd.Val(), clusterSlotsCmd.Err() + } + case CmdTypeGeoLocation: + if geoLocationCmd, ok := cmd.(interface { + Val() []GeoLocation + Err() error + }); ok { + return geoLocationCmd.Val(), geoLocationCmd.Err() + } + case CmdTypeGeoSearchLocation: + if geoSearchLocationCmd, ok := cmd.(interface { + Val() []GeoLocation + Err() error + }); ok { + return geoSearchLocationCmd.Val(), geoSearchLocationCmd.Err() + } + case CmdTypeGeoPos: + if geoPosCmd, ok := cmd.(interface { + Val() []*GeoPos + Err() error + }); ok { + return geoPosCmd.Val(), geoPosCmd.Err() + } + case CmdTypeCommandsInfo: + if commandsInfoCmd, ok := cmd.(interface { + Val() map[string]*CommandInfo + Err() error + }); ok { + return commandsInfoCmd.Val(), commandsInfoCmd.Err() + } + case CmdTypeSlowLog: + if slowLogCmd, ok := cmd.(interface { + Val() []SlowLog + Err() error + }); ok { + return slowLogCmd.Val(), slowLogCmd.Err() + } + case CmdTypeHotKeys: + if hotKeysCmd, ok := cmd.(interface { + Val() *HotKeysResult + Err() error + }); ok { + return hotKeysCmd.Val(), hotKeysCmd.Err() + } + case CmdTypeIncrEXInt: + if incrEXCmd, ok := cmd.(interface { + Val() IncrEXIntResult + Err() error + }); ok { + return incrEXCmd.Val(), incrEXCmd.Err() + } + case CmdTypeIncrEXFloat: + if incrEXCmd, ok := cmd.(interface { + Val() IncrEXFloatResult + Err() error + }); ok { + return incrEXCmd.Val(), incrEXCmd.Err() + } + case CmdTypeKeyValues: + if keyValuesCmd, ok := cmd.(interface { + Val() (string, []string) + Err() error + }); ok { + key, values := keyValuesCmd.Val() + return CmdTypeKeyValuesValue{key: key, values: values}, keyValuesCmd.Err() + } + case CmdTypeZSliceWithKey: + if zSliceWithKeyCmd, ok := cmd.(interface { + Val() (string, []Z) + Err() error + }); ok { + key, zSlice := zSliceWithKeyCmd.Val() + return CmdTypeZSliceWithKeyValue{key: key, zSlice: zSlice}, zSliceWithKeyCmd.Err() + } + case CmdTypeFunctionList: + if functionListCmd, ok := cmd.(interface { + Val() []Library + Err() error + }); ok { + return functionListCmd.Val(), functionListCmd.Err() + } + case CmdTypeFunctionStats: + if functionStatsCmd, ok := cmd.(interface { + Val() FunctionStats + Err() error + }); ok { + return functionStatsCmd.Val(), functionStatsCmd.Err() + } + case CmdTypeLCS: + if lcsCmd, ok := cmd.(interface { + Val() *LCSMatch + Err() error + }); ok { + return lcsCmd.Val(), lcsCmd.Err() + } + case CmdTypeKeyFlags: + if keyFlagsCmd, ok := cmd.(interface { + Val() []KeyFlags + Err() error + }); ok { + return keyFlagsCmd.Val(), keyFlagsCmd.Err() + } + case CmdTypeClusterLinks: + if clusterLinksCmd, ok := cmd.(interface { + Val() []ClusterLink + Err() error + }); ok { + return clusterLinksCmd.Val(), clusterLinksCmd.Err() + } + case CmdTypeClusterShards: + if clusterShardsCmd, ok := cmd.(interface { + Val() []ClusterShard + Err() error + }); ok { + return clusterShardsCmd.Val(), clusterShardsCmd.Err() + } + case CmdTypeRankWithScore: + if rankWithScoreCmd, ok := cmd.(interface { + Val() RankScore + Err() error + }); ok { + return rankWithScoreCmd.Val(), rankWithScoreCmd.Err() + } + case CmdTypeClientInfo: + if clientInfoCmd, ok := cmd.(interface { + Val() *ClientInfo + Err() error + }); ok { + return clientInfoCmd.Val(), clientInfoCmd.Err() + } + case CmdTypeACLLog: + if aclLogCmd, ok := cmd.(interface { + Val() []*ACLLogEntry + Err() error + }); ok { + return aclLogCmd.Val(), aclLogCmd.Err() + } + case CmdTypeInfo: + if infoCmd, ok := cmd.(interface { + Val() string + Err() error + }); ok { + return infoCmd.Val(), infoCmd.Err() + } + case CmdTypeMonitor: + if monitorCmd, ok := cmd.(interface { + Val() string + Err() error + }); ok { + return monitorCmd.Val(), monitorCmd.Err() + } + case CmdTypeJSON: + if jsonCmd, ok := cmd.(interface { + Val() string + Err() error + }); ok { + return jsonCmd.Val(), jsonCmd.Err() + } + case CmdTypeJSONSlice: + if jsonSliceCmd, ok := cmd.(interface { + Val() []interface{} + Err() error + }); ok { + return jsonSliceCmd.Val(), jsonSliceCmd.Err() + } + case CmdTypeIntPointerSlice: + if intPointerSliceCmd, ok := cmd.(interface { + Val() []*int64 + Err() error + }); ok { + return intPointerSliceCmd.Val(), intPointerSliceCmd.Err() + } + case CmdTypeScanDump: + if scanDumpCmd, ok := cmd.(interface { + Val() ScanDump + Err() error + }); ok { + return scanDumpCmd.Val(), scanDumpCmd.Err() + } + case CmdTypeBFInfo: + if bfInfoCmd, ok := cmd.(interface { + Val() BFInfo + Err() error + }); ok { + return bfInfoCmd.Val(), bfInfoCmd.Err() + } + case CmdTypeCFInfo: + if cfInfoCmd, ok := cmd.(interface { + Val() CFInfo + Err() error + }); ok { + return cfInfoCmd.Val(), cfInfoCmd.Err() + } + case CmdTypeCMSInfo: + if cmsInfoCmd, ok := cmd.(interface { + Val() CMSInfo + Err() error + }); ok { + return cmsInfoCmd.Val(), cmsInfoCmd.Err() + } + case CmdTypeTopKInfo: + if topKInfoCmd, ok := cmd.(interface { + Val() TopKInfo + Err() error + }); ok { + return topKInfoCmd.Val(), topKInfoCmd.Err() + } + case CmdTypeTDigestInfo: + if tDigestInfoCmd, ok := cmd.(interface { + Val() TDigestInfo + Err() error + }); ok { + return tDigestInfoCmd.Val(), tDigestInfoCmd.Err() + } + case CmdTypeFTSearch: + if ftSearchCmd, ok := cmd.(interface { + Val() FTSearchResult + Err() error + }); ok { + return ftSearchCmd.Val(), ftSearchCmd.Err() + } + case CmdTypeFTInfo: + if ftInfoCmd, ok := cmd.(interface { + Val() FTInfoResult + Err() error + }); ok { + return ftInfoCmd.Val(), ftInfoCmd.Err() + } + case CmdTypeFTSpellCheck: + if ftSpellCheckCmd, ok := cmd.(interface { + Val() []SpellCheckResult + Err() error + }); ok { + return ftSpellCheckCmd.Val(), ftSpellCheckCmd.Err() + } + case CmdTypeFTSynDump: + if ftSynDumpCmd, ok := cmd.(interface { + Val() []FTSynDumpResult + Err() error + }); ok { + return ftSynDumpCmd.Val(), ftSynDumpCmd.Err() + } + case CmdTypeAggregate: + if aggregateCmd, ok := cmd.(interface { + Val() *FTAggregateResult + Err() error + }); ok { + return aggregateCmd.Val(), aggregateCmd.Err() + } + case CmdTypeTSTimestampValue: + if tsTimestampValueCmd, ok := cmd.(interface { + Val() TSTimestampValue + Err() error + }); ok { + return tsTimestampValueCmd.Val(), tsTimestampValueCmd.Err() + } + case CmdTypeTSTimestampValueSlice: + if tsTimestampValueSliceCmd, ok := cmd.(interface { + Val() []TSTimestampValue + Err() error + }); ok { + return tsTimestampValueSliceCmd.Val(), tsTimestampValueSliceCmd.Err() + } + case CmdTypeStringSlice: + if stringSliceCmd, ok := cmd.(interface { + Val() []string + Err() error + }); ok { + return stringSliceCmd.Val(), stringSliceCmd.Err() + } + case CmdTypeIntSlice: + if intSliceCmd, ok := cmd.(interface { + Val() []int64 + Err() error + }); ok { + return intSliceCmd.Val(), intSliceCmd.Err() + } + case CmdTypeUintSlice: + if uintSliceCmd, ok := cmd.(interface { + Val() []uint64 + Err() error + }); ok { + return uintSliceCmd.Val(), uintSliceCmd.Err() + } + case CmdTypeBoolSlice: + if boolSliceCmd, ok := cmd.(interface { + Val() []bool + Err() error + }); ok { + return boolSliceCmd.Val(), boolSliceCmd.Err() + } + case CmdTypeFloatSlice: + if floatSliceCmd, ok := cmd.(interface { + Val() []float64 + Err() error + }); ok { + return floatSliceCmd.Val(), floatSliceCmd.Err() + } + case CmdTypeSlice: + if sliceCmd, ok := cmd.(interface { + Val() []interface{} + Err() error + }); ok { + return sliceCmd.Val(), sliceCmd.Err() + } + case CmdTypeKeyValueSlice: + if keyValueSliceCmd, ok := cmd.(interface { + Val() []KeyValue + Err() error + }); ok { + return keyValueSliceCmd.Val(), keyValueSliceCmd.Err() + } + case CmdTypeAREntrySlice: + if arEntrySliceCmd, ok := cmd.(interface { + Val() []AREntry + Err() error + }); ok { + return arEntrySliceCmd.Val(), arEntrySliceCmd.Err() + } + case CmdTypeMapStringString: + if mapCmd, ok := cmd.(interface { + Val() map[string]string + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + case CmdTypeMapStringInt: + if mapCmd, ok := cmd.(interface { + Val() map[string]int64 + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + case CmdTypeMapStringInterfaceSlice: + if mapCmd, ok := cmd.(interface { + Val() []map[string]interface{} + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + case CmdTypeMapStringInterface: + if mapCmd, ok := cmd.(interface { + Val() map[string]interface{} + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + case CmdTypeMapStringStringSlice: + if mapCmd, ok := cmd.(interface { + Val() []map[string]string + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + case CmdTypeMapMapStringInterface: + if mapCmd, ok := cmd.(interface { + Val() map[string]interface{} + Err() error + }); ok { + return mapCmd.Val(), mapCmd.Err() + } + default: + // For unknown command types, return nil + return nil, nil + } + } + + // If we can't get the command type, return nil + return nil, nil +} + +//------------------------------------------------------------------------------ + +// IncrEXIntResult is the reply of an INCREX command issued via IncrEXInt. +// Value is the new value of the key; AppliedIncrement is the increment that +// the server actually applied (0 when an out-of-bounds operation was +// rejected, clamped when SATURATE was set). +type IncrEXIntResult struct { + Value int64 + AppliedIncrement int64 +} + +type IncrEXIntCmd struct { + baseCmd + + val IncrEXIntResult +} + +var _ Cmder = (*IncrEXIntCmd)(nil) + +func NewIncrEXIntCmd(ctx context.Context, args ...interface{}) *IncrEXIntCmd { + return &IncrEXIntCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeIncrEXInt, + }, + } +} + +func (cmd *IncrEXIntCmd) SetVal(val IncrEXIntResult) { cmd.val = val } +func (cmd *IncrEXIntCmd) Val() IncrEXIntResult { return cmd.val } +func (cmd *IncrEXIntCmd) Result() (IncrEXIntResult, error) { + return cmd.val, cmd.err +} +func (cmd *IncrEXIntCmd) String() string { return cmdString(cmd, cmd.val) } + +func (cmd *IncrEXIntCmd) readReply(rd *proto.Reader) error { + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + value, err := rd.ReadInt() + if err != nil { + return err + } + applied, err := rd.ReadInt() + if err != nil { + return err + } + cmd.val = IncrEXIntResult{Value: value, AppliedIncrement: applied} + return nil +} + +func (cmd *IncrEXIntCmd) Clone() Cmder { + return &IncrEXIntCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +// IncrEXFloatResult is the reply of an INCREX command issued via IncrEXFloat. +type IncrEXFloatResult struct { + Value float64 + AppliedIncrement float64 +} + +type IncrEXFloatCmd struct { + baseCmd + + val IncrEXFloatResult +} + +var _ Cmder = (*IncrEXFloatCmd)(nil) + +func NewIncrEXFloatCmd(ctx context.Context, args ...interface{}) *IncrEXFloatCmd { + return &IncrEXFloatCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeIncrEXFloat, + }, + } +} + +func (cmd *IncrEXFloatCmd) SetVal(val IncrEXFloatResult) { cmd.val = val } +func (cmd *IncrEXFloatCmd) Val() IncrEXFloatResult { return cmd.val } +func (cmd *IncrEXFloatCmd) Result() (IncrEXFloatResult, error) { + return cmd.val, cmd.err +} +func (cmd *IncrEXFloatCmd) String() string { return cmdString(cmd, cmd.val) } + +func (cmd *IncrEXFloatCmd) readReply(rd *proto.Reader) error { + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + value, err := rd.ReadFloat() + if err != nil { + return err + } + applied, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val = IncrEXFloatResult{Value: value, AppliedIncrement: applied} + return nil +} + +func (cmd *IncrEXFloatCmd) Clone() Cmder { + return &IncrEXFloatCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +//------------------------------------------------------------------------------ + +// AREntrySliceCmd is a command that returns index-value pairs from ARSCAN or ARGREP. +type AREntrySliceCmd struct { + baseCmd + val []AREntry +} + +var _ Cmder = (*AREntrySliceCmd)(nil) + +func NewAREntrySliceCmd(ctx context.Context, args ...any) *AREntrySliceCmd { + return &AREntrySliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeAREntrySlice, + }, + } +} + +func (cmd *AREntrySliceCmd) SetVal(val []AREntry) { + cmd.val = val +} + +func (cmd *AREntrySliceCmd) Val() []AREntry { + return cmd.val +} + +func (cmd *AREntrySliceCmd) Result() ([]AREntry, error) { + return cmd.val, cmd.err +} + +func (cmd *AREntrySliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *AREntrySliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n == 0 { + cmd.val = make([]AREntry, 0) + return nil + } + + cmd.val = make([]AREntry, n) + for i := range n { + if err = rd.ReadFixedArrayLen(2); err != nil { + return err + } + + cmd.val[i].Index, err = rd.ReadUint() + if err != nil { + return err + } + + cmd.val[i].Value, err = rd.ReadString() + if err != nil { + return err + } + } + return nil +} + +func (cmd *AREntrySliceCmd) Clone() Cmder { + var val []AREntry + if cmd.val != nil { + val = make([]AREntry, len(cmd.val)) + copy(val, cmd.val) + } + return &AREntrySliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} diff --git a/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go b/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go new file mode 100644 index 00000000000..da8c6d314c0 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go @@ -0,0 +1,209 @@ +package redis + +import ( + "context" + "strings" + + "github.com/redis/go-redis/v9/internal/routing" +) + +type ( + module = string + commandName = string +) + +var defaultPolicies = map[module]map[commandName]*routing.CommandPolicy{ + "ft": { + "create": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "search": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "aggregate": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "dictadd": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "dictdump": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "dictdel": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "suglen": { + Request: routing.ReqDefault, + Response: routing.RespDefaultHashSlot, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "cursor": { + Request: routing.ReqSpecial, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "sugadd": { + Request: routing.ReqDefault, + Response: routing.RespDefaultHashSlot, + }, + "sugget": { + Request: routing.ReqDefault, + Response: routing.RespDefaultHashSlot, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "sugdel": { + Request: routing.ReqDefault, + Response: routing.RespDefaultHashSlot, + }, + "spellcheck": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "explain": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "explaincli": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "aliasadd": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "aliasupdate": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "aliasdel": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "info": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "tagvals": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "syndump": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "synupdate": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "profile": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + Tips: map[string]string{ + routing.ReadOnlyCMD: "", + }, + }, + "alter": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "dropindex": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + "drop": { + Request: routing.ReqDefault, + Response: routing.RespDefaultKeyless, + }, + }, +} + +type CommandInfoResolveFunc func(ctx context.Context, cmd Cmder) *routing.CommandPolicy + +type commandInfoResolver struct { + resolveFunc CommandInfoResolveFunc + fallBackResolver *commandInfoResolver +} + +func NewCommandInfoResolver(resolveFunc CommandInfoResolveFunc) *commandInfoResolver { + return &commandInfoResolver{ + resolveFunc: resolveFunc, + } +} + +func NewDefaultCommandPolicyResolver() *commandInfoResolver { + return NewCommandInfoResolver(func(ctx context.Context, cmd Cmder) *routing.CommandPolicy { + module := "core" + command := cmd.Name() + cmdParts := strings.Split(command, ".") + if len(cmdParts) == 2 { + module = cmdParts[0] + command = cmdParts[1] + } + + if policy, ok := defaultPolicies[module][command]; ok { + return policy + } + + return nil + }) +} + +func (r *commandInfoResolver) GetCommandPolicy(ctx context.Context, cmd Cmder) *routing.CommandPolicy { + if r.resolveFunc == nil { + return nil + } + + policy := r.resolveFunc(ctx, cmd) + if policy != nil { + return policy + } + + if r.fallBackResolver != nil { + return r.fallBackResolver.GetCommandPolicy(ctx, cmd) + } + + return nil +} + +func (r *commandInfoResolver) SetFallbackResolver(fallbackResolver *commandInfoResolver) { + r.fallBackResolver = fallbackResolver +} diff --git a/vendor/github.com/redis/go-redis/v9/commands.go b/vendor/github.com/redis/go-redis/v9/commands.go index c0358001d1f..d347ffeb53d 100644 --- a/vendor/github.com/redis/go-redis/v9/commands.go +++ b/vendor/github.com/redis/go-redis/v9/commands.go @@ -55,6 +55,11 @@ func appendArgs(dst, src []interface{}) []interface{} { return appendArg(dst, src[0]) } + if cap(dst) < len(dst)+len(src) { + newDst := make([]interface{}, len(dst), len(dst)+len(src)) + copy(newDst, dst) + dst = newDst + } dst = append(dst, src...) return dst } @@ -193,6 +198,7 @@ type Cmdable interface { ClientID(ctx context.Context) *IntCmd ClientUnblock(ctx context.Context, id int64) *IntCmd ClientUnblockWithError(ctx context.Context, id int64) *IntCmd + ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd ConfigResetStat(ctx context.Context) *StatusCmd ConfigSet(ctx context.Context, parameter, value string) *StatusCmd @@ -209,14 +215,20 @@ type Cmdable interface { ShutdownSave(ctx context.Context) *StatusCmd ShutdownNoSave(ctx context.Context) *StatusCmd SlaveOf(ctx context.Context, host, port string) *StatusCmd + ReplicaOf(ctx context.Context, host, port string) *StatusCmd SlowLogGet(ctx context.Context, num int64) *SlowLogCmd + SlowLogLen(ctx context.Context) *IntCmd + SlowLogReset(ctx context.Context) *StatusCmd Time(ctx context.Context) *TimeCmd DebugObject(ctx context.Context, key string) *StringCmd MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd + Latency(ctx context.Context) *LatencyCmd + LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd ACLCmdable + ArrayCmdable BitMapCmdable ClusterCmdable GenericCmdable @@ -253,6 +265,7 @@ var ( _ Cmdable = (*Tx)(nil) _ Cmdable = (*Ring)(nil) _ Cmdable = (*ClusterClient)(nil) + _ Cmdable = (*Pipeline)(nil) ) type cmdable func(ctx context.Context, cmd Cmder) error @@ -437,6 +450,23 @@ func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd { return cmd } +// DoRaw executes a command and returns the raw RESP protocol bytes without parsing. +func (c cmdable) DoRaw(ctx context.Context, args ...interface{}) *RawCmd { + cmd := NewRawCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// DoRawWriteTo executes a command and streams raw RESP bytes directly to w without intermediate allocations. +func (c cmdable) DoRawWriteTo(ctx context.Context, w io.Writer, args ...interface{}) *RawWriteToCmd { + cmd := NewRawWriteToCmd(ctx, w, args...) + _ = c(ctx, cmd) + return cmd +} + +// Quit closes the connection. +// +// Deprecated: Just close the connection instead as of Redis 7.2.0. func (c cmdable) Quit(_ context.Context) *StatusCmd { panic("not implemented") } @@ -518,6 +548,23 @@ func (c cmdable) ClientInfo(ctx context.Context) *ClientInfoCmd { return cmd } +// ClientMaintNotifications enables or disables maintenance notifications for maintenance upgrades. +// When enabled, the client will receive push notifications about Redis maintenance events. +func (c cmdable) ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd { + args := []interface{}{"client", "maint_notifications"} + if enabled { + if endpointType == "" { + endpointType = "none" + } + args = append(args, "on", "moving-endpoint-type", endpointType) + } else { + args = append(args, "off") + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + // ------------------------------------------------------------------------------------------------ func (c cmdable) ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd { @@ -642,18 +689,56 @@ func (c cmdable) ShutdownNoSave(ctx context.Context) *StatusCmd { return c.shutdown(ctx, "nosave") } +// SlaveOf sets a Redis server as a replica of another, or promotes it to being a master. +// +// Deprecated: Use ReplicaOf instead as of Redis 5.0.0. func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd { cmd := NewStatusCmd(ctx, "slaveof", host, port) _ = c(ctx, cmd) return cmd } +// ReplicaOf sets a Redis server as a replica of another, or promotes it to being a master. +func (c cmdable) ReplicaOf(ctx context.Context, host, port string) *StatusCmd { + cmd := NewStatusCmd(ctx, "replicaof", host, port) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd { cmd := NewSlowLogCmd(context.Background(), "slowlog", "get", num) _ = c(ctx, cmd) return cmd } +func (c cmdable) SlowLogLen(ctx context.Context) *IntCmd { + cmd := NewIntCmd(ctx, "slowlog", "len") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SlowLogReset(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "slowlog", "reset") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Latency(ctx context.Context) *LatencyCmd { + cmd := NewLatencyCmd(ctx, "latency", "latest") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd { + args := make([]interface{}, 2+len(events)) + args[0] = "latency" + args[1] = "reset" + copy(args[2:], events) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) Sync(_ context.Context) { panic("not implemented") } @@ -674,7 +759,9 @@ func (c cmdable) MemoryUsage(ctx context.Context, key string, samples ...int) *I args := []interface{}{"memory", "usage", key} if len(samples) > 0 { if len(samples) != 1 { - panic("MemoryUsage expects single sample count") + cmd := NewIntCmd(ctx) + cmd.SetErr(errors.New("MemoryUsage expects single sample count")) + return cmd } args = append(args, "SAMPLES", samples[0]) } diff --git a/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go b/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go new file mode 100644 index 00000000000..bb3e8bf2ae1 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go @@ -0,0 +1,39 @@ +package redis + +import ( + "time" + + "github.com/redis/go-redis/v9/internal" +) + +// DialRetryBackoffConstant returns a dial retry backoff function that always returns d. +// attempt is 0-based: attempt=0 is the delay after the 1st failed dial. +func DialRetryBackoffConstant(d time.Duration) func(attempt int) time.Duration { + if d < 0 { + d = 0 + } + return func(int) time.Duration { return d } +} + +// DialRetryBackoffExponential returns a dial retry backoff function that uses exponential +// backoff with jitter and a cap, using internal.RetryBackoff. +// +// attempt is 0-based: attempt=0 is the delay after the 1st failed dial. +func DialRetryBackoffExponential(minBackoff, maxBackoff time.Duration) func(attempt int) time.Duration { + if minBackoff < 0 { + minBackoff = 0 + } + if maxBackoff < 0 { + maxBackoff = 0 + } + if minBackoff > maxBackoff { + minBackoff = maxBackoff + } + return func(attempt int) time.Duration { + // internal.RetryBackoff expects retry >= 0. + if attempt < 0 { + attempt = 0 + } + return internal.RetryBackoff(attempt, minBackoff, maxBackoff) + } +} diff --git a/vendor/github.com/redis/go-redis/v9/docker-compose.yml b/vendor/github.com/redis/go-redis/v9/docker-compose.yml index 3d4347bf210..fed908bead2 100644 --- a/vendor/github.com/redis/go-redis/v9/docker-compose.yml +++ b/vendor/github.com/redis/go-redis/v9/docker-compose.yml @@ -1,12 +1,16 @@ --- +x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} + services: redis: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + image: *default-image platform: linux/amd64 container_name: redis-standalone environment: - TLS_ENABLED=yes + - TLS_CLIENT_CNS=testcertuser + - TLS_AUTH_CLIENTS_USER=CN - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 @@ -21,9 +25,10 @@ services: - sentinel - all-stack - all + - e2e osscluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + image: *default-image platform: linux/amd64 container_name: redis-osscluster environment: @@ -39,14 +44,77 @@ services: - all-stack - all + cae-resp-proxy: + image: redislabs/client-resp-proxy:latest + container_name: cae-resp-proxy + environment: + - TARGET_HOST=redis + - TARGET_PORT=6379 + - LISTEN_PORT=17000,17001,17002,17003 # 4 proxy nodes: initially show 3, swap in 4th during SMIGRATED + - LISTEN_HOST=0.0.0.0 + - API_PORT=3000 + - DEFAULT_INTERCEPTORS=cluster,hitless + ports: + - "17000:17000" # Proxy node 1 (host:container) + - "17001:17001" # Proxy node 2 (host:container) + - "17002:17002" # Proxy node 3 (host:container) + - "17003:17003" # Proxy node 4 (host:container) - hidden initially, swapped in during SMIGRATED + - "18100:3000" # HTTP API port (host:container) + depends_on: + - redis + profiles: + - e2e + - all + + proxy-fault-injector: + build: + context: . + dockerfile: maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile + container_name: proxy-fault-injector + ports: + - "15000:5000" # Fault injector API port (host:container) + depends_on: + - cae-resp-proxy + environment: + - PROXY_API_URL=http://cae-resp-proxy:3000 + profiles: + - e2e + - all + + osscluster-tls: + image: *default-image + platform: linux/amd64 + container_name: redis-osscluster-tls + environment: + - NODES=6 + - PORT=6430 + - TLS_PORT=5430 + - TLS_ENABLED=yes + - TLS_CLIENT_CNS=testcertuser + - TLS_AUTH_CLIENTS_USER=CN + - REDIS_CLUSTER=yes + - REPLICAS=1 + command: "--tls-auth-clients optional --cluster-announce-ip 127.0.0.1" + ports: + - "6430-6435:6430-6435" # Regular ports + - "5430-5435:5430-5435" # TLS ports (set via TLS_PORT env var) + - "16430-16435:16430-16435" # Cluster bus ports (PORT + 10000) + volumes: + - "./dockers/osscluster-tls:/redis/work" + profiles: + - cluster-tls + - all + sentinel-cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + image: *default-image platform: linux/amd64 container_name: redis-sentinel-cluster network_mode: "host" environment: - NODES=3 - TLS_ENABLED=yes + - TLS_CLIENT_CNS=testcertuser + - TLS_AUTH_CLIENTS_USER=CN - REDIS_CLUSTER=no - PORT=9121 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} @@ -60,7 +128,7 @@ services: - all sentinel: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + image: *default-image platform: linux/amd64 container_name: redis-sentinel depends_on: @@ -84,19 +152,21 @@ services: - all ring-cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + image: *default-image platform: linux/amd64 container_name: redis-ring-cluster environment: - NODES=3 - TLS_ENABLED=yes + - TLS_CLIENT_CNS=testcertuser + - TLS_AUTH_CLIENTS_USER=CN - REDIS_CLUSTER=no - PORT=6390 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} ports: - - 6390:6390 - - 6391:6391 - - 6392:6392 + - "6390:6390" + - "6391:6391" + - "6392:6392" volumes: - "./dockers/ring:/redis/work" profiles: diff --git a/vendor/github.com/redis/go-redis/v9/error.go b/vendor/github.com/redis/go-redis/v9/error.go index 8c811966fb6..06ecca740ab 100644 --- a/vendor/github.com/redis/go-redis/v9/error.go +++ b/vendor/github.com/redis/go-redis/v9/error.go @@ -22,6 +22,17 @@ var ErrPoolExhausted = pool.ErrPoolExhausted // ErrPoolTimeout timed out waiting to get a connection from the connection pool. var ErrPoolTimeout = pool.ErrPoolTimeout +// ErrCrossSlot is returned when keys are used in the same Redis command and +// the keys are not in the same hash slot. This error is returned by Redis +// Cluster and will be returned by the client when TxPipeline or TxPipelined +// is used on a ClusterClient with keys in different slots. +var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot") + +// ErrNoScript is returned when EVALSHA is requested for a script digest that +// is not available in the script cache. Note that this error text is reproduced +// literally from that used by Redis. +var ErrNoScript = proto.RedisError("NOSCRIPT No matching script. Please use EVAL.") + // HasErrorPrefix checks if the err is a Redis error and the message contains a prefix. func HasErrorPrefix(err error, prefix string) bool { var rErr Error @@ -46,34 +57,93 @@ type Error interface { var _ Error = proto.RedisError("") func isContextError(err error) bool { - switch err { - case context.Canceled, context.DeadlineExceeded: - return true - default: - return false + // Check for wrapped context errors using errors.Is + return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) +} + +// isTimeoutError checks if an error is a timeout error, even if wrapped. +// Returns (isTimeout, shouldRetryOnTimeout) where: +// - isTimeout: true if the error is any kind of timeout error +// - shouldRetryOnTimeout: true if Timeout() method returns true +func isTimeoutError(err error) (isTimeout bool, hasTimeoutFlag bool) { + // Check for timeoutError interface (works with wrapped errors) + var te timeoutError + if errors.As(err, &te) { + return true, te.Timeout() } + + // Check for net.Error specifically (common case for network timeouts) + var netErr net.Error + if errors.As(err, &netErr) { + return true, netErr.Timeout() + } + + return false, false } func shouldRetry(err error, retryTimeout bool) bool { - switch err { - case io.EOF, io.ErrUnexpectedEOF: + if err == nil { + return false + } + + // Check for EOF errors (works with wrapped errors) + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return true + } + + // Dial errors mean TCP connection was never established — safe to retry even + // when wrapped inside context.DeadlineExceeded (from DialTimeout context). + // Must be checked before the context error check below. + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Op == "dial" { return true - case nil, context.Canceled, context.DeadlineExceeded: + } + + // Check for context errors (works with wrapped errors) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return false - case pool.ErrPoolTimeout: + } + + // Check for pool timeout (works with wrapped errors) + if errors.Is(err, pool.ErrPoolTimeout) { // connection pool timeout, increase retries. #3289 return true } - if v, ok := err.(timeoutError); ok { - if v.Timeout() { + // Check for timeout errors (works with wrapped errors) + if isTimeout, hasTimeoutFlag := isTimeoutError(err); isTimeout { + if hasTimeoutFlag { return retryTimeout } return true } + // Check for typed Redis errors using errors.As (works with wrapped errors) + if proto.IsMaxClientsError(err) { + return true + } + if proto.IsLoadingError(err) { + return true + } + if proto.IsReadOnlyError(err) { + return true + } + if proto.IsMasterDownError(err) { + return true + } + if proto.IsClusterDownError(err) { + return true + } + if proto.IsTryAgainError(err) { + return true + } + if proto.IsNoReplicasError(err) { + return true + } + + // Fallback to string checking for backward compatibility with plain errors s := err.Error() - if s == "ERR max number of clients reached" { + if strings.HasPrefix(s, "ERR max number of clients reached") { return true } if strings.HasPrefix(s, "LOADING ") { @@ -82,7 +152,7 @@ func shouldRetry(err error, retryTimeout bool) bool { if strings.HasPrefix(s, "READONLY ") { return true } - if strings.HasPrefix(s, "MASTERDOWN ") { + if strings.Contains(s, "-READONLY You can't write against a read only replica") { return true } if strings.HasPrefix(s, "CLUSTERDOWN ") { @@ -91,20 +161,39 @@ func shouldRetry(err error, retryTimeout bool) bool { if strings.HasPrefix(s, "TRYAGAIN ") { return true } + if strings.HasPrefix(s, "MASTERDOWN ") { + return true + } + if strings.HasPrefix(s, "NOREPLICAS ") { + return true + } return false } func isRedisError(err error) bool { - _, ok := err.(proto.RedisError) - return ok + // Check if error implements the Error interface (works with wrapped errors) + var redisErr Error + if errors.As(err, &redisErr) { + return true + } + // Also check for proto.RedisError specifically + var protoRedisErr proto.RedisError + return errors.As(err, &protoRedisErr) } func isBadConn(err error, allowTimeout bool, addr string) bool { - switch err { - case nil: + if err == nil { return false - case context.Canceled, context.DeadlineExceeded: + } + + // Check for context errors (works with wrapped errors) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return true + } + + // Check for pool timeout errors (works with wrapped errors) + if errors.Is(err, pool.ErrConnUnusableTimeout) { return true } @@ -125,7 +214,9 @@ func isBadConn(err error, allowTimeout bool, addr string) bool { } if allowTimeout { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + // Check for network timeout errors (works with wrapped errors) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { return false } } @@ -134,44 +225,151 @@ func isBadConn(err error, allowTimeout bool, addr string) bool { } func isMovedError(err error) (moved bool, ask bool, addr string) { - if !isRedisError(err) { - return + // Check for typed MovedError + if movedErr, ok := proto.IsMovedError(err); ok { + addr = movedErr.Addr() + addr = internal.GetAddr(addr) + return true, false, addr } - s := err.Error() - switch { - case strings.HasPrefix(s, "MOVED "): - moved = true - case strings.HasPrefix(s, "ASK "): - ask = true - default: - return + // Check for typed AskError + if askErr, ok := proto.IsAskError(err); ok { + addr = askErr.Addr() + addr = internal.GetAddr(addr) + return false, true, addr } - ind := strings.LastIndex(s, " ") - if ind == -1 { - return false, false, "" + // Fallback to string checking for backward compatibility + s := err.Error() + if strings.HasPrefix(s, "MOVED ") { + // Parse: MOVED 3999 127.0.0.1:6381 + parts := strings.Split(s, " ") + if len(parts) == 3 { + addr = internal.GetAddr(parts[2]) + return true, false, addr + } + } + if strings.HasPrefix(s, "ASK ") { + // Parse: ASK 3999 127.0.0.1:6381 + parts := strings.Split(s, " ") + if len(parts) == 3 { + addr = internal.GetAddr(parts[2]) + return false, true, addr + } } - addr = s[ind+1:] - addr = internal.GetAddr(addr) - return + return false, false, "" } func isLoadingError(err error) bool { - return strings.HasPrefix(err.Error(), "LOADING ") + return proto.IsLoadingError(err) } func isReadOnlyError(err error) bool { - return strings.HasPrefix(err.Error(), "READONLY ") + return proto.IsReadOnlyError(err) } func isMovedSameConnAddr(err error, addr string) bool { - redisError := err.Error() - if !strings.HasPrefix(redisError, "MOVED ") { - return false + if movedErr, ok := proto.IsMovedError(err); ok { + return strings.HasSuffix(movedErr.Addr(), addr) + } + return false +} + +//------------------------------------------------------------------------------ + +// Typed error checking functions for public use. +// These functions work correctly even when errors are wrapped in hooks. + +// IsLoadingError checks if an error is a Redis LOADING error, even if wrapped. +// LOADING errors occur when Redis is loading the dataset in memory. +func IsLoadingError(err error) bool { + return proto.IsLoadingError(err) +} + +// IsReadOnlyError checks if an error is a Redis READONLY error, even if wrapped. +// READONLY errors occur when trying to write to a read-only replica. +func IsReadOnlyError(err error) bool { + return proto.IsReadOnlyError(err) +} + +// IsClusterDownError checks if an error is a Redis CLUSTERDOWN error, even if wrapped. +// CLUSTERDOWN errors occur when the cluster is down. +func IsClusterDownError(err error) bool { + return proto.IsClusterDownError(err) +} + +// IsTryAgainError checks if an error is a Redis TRYAGAIN error, even if wrapped. +// TRYAGAIN errors occur when a command cannot be processed and should be retried. +func IsTryAgainError(err error) bool { + return proto.IsTryAgainError(err) +} + +// IsMasterDownError checks if an error is a Redis MASTERDOWN error, even if wrapped. +// MASTERDOWN errors occur when the master is down. +func IsMasterDownError(err error) bool { + return proto.IsMasterDownError(err) +} + +// IsMaxClientsError checks if an error is a Redis max clients error, even if wrapped. +// This error occurs when the maximum number of clients has been reached. +func IsMaxClientsError(err error) bool { + return proto.IsMaxClientsError(err) +} + +// IsMovedError checks if an error is a Redis MOVED error, even if wrapped. +// MOVED errors occur in cluster mode when a key has been moved to a different node. +// Returns the address of the node where the key has been moved and a boolean indicating if it's a MOVED error. +func IsMovedError(err error) (addr string, ok bool) { + if movedErr, isMovedErr := proto.IsMovedError(err); isMovedErr { + return movedErr.Addr(), true + } + return "", false +} + +// IsAskError checks if an error is a Redis ASK error, even if wrapped. +// ASK errors occur in cluster mode when a key is being migrated and the client should ask another node. +// Returns the address of the node to ask and a boolean indicating if it's an ASK error. +func IsAskError(err error) (addr string, ok bool) { + if askErr, isAskErr := proto.IsAskError(err); isAskErr { + return askErr.Addr(), true } - return strings.HasSuffix(redisError, " "+addr) + return "", false +} + +// IsAuthError checks if an error is a Redis authentication error, even if wrapped. +// Authentication errors occur when: +// - NOAUTH: Redis requires authentication but none was provided +// - WRONGPASS: Redis authentication failed due to incorrect password +// - unauthenticated: Error returned when password changed +func IsAuthError(err error) bool { + return proto.IsAuthError(err) +} + +// IsPermissionError checks if an error is a Redis permission error, even if wrapped. +// Permission errors (NOPERM) occur when a user does not have permission to execute a command. +func IsPermissionError(err error) bool { + return proto.IsPermissionError(err) +} + +// IsExecAbortError checks if an error is a Redis EXECABORT error, even if wrapped. +// EXECABORT errors occur when a transaction is aborted. +func IsExecAbortError(err error) bool { + return proto.IsExecAbortError(err) +} + +// IsOOMError checks if an error is a Redis OOM (Out Of Memory) error, even if wrapped. +// OOM errors occur when Redis is out of memory. +func IsOOMError(err error) bool { + return proto.IsOOMError(err) +} + +// IsNoReplicasError checks if an error is a Redis NOREPLICAS error, even if wrapped. +// NOREPLICAS errors occur when not enough replicas acknowledge a write operation. +// This typically happens with WAIT/WAITAOF commands or CLUSTER SETSLOT with synchronous +// replication when the required number of replicas cannot confirm the write within the timeout. +func IsNoReplicasError(err error) bool { + return proto.IsNoReplicasError(err) } //------------------------------------------------------------------------------ diff --git a/vendor/github.com/redis/go-redis/v9/generic_commands.go b/vendor/github.com/redis/go-redis/v9/generic_commands.go index dc6c3fe0146..c7100222cd8 100644 --- a/vendor/github.com/redis/go-redis/v9/generic_commands.go +++ b/vendor/github.com/redis/go-redis/v9/generic_commands.go @@ -3,6 +3,8 @@ package redis import ( "context" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type GenericCmdable interface { @@ -363,6 +365,9 @@ func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count in args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(3) + } _ = c(ctx, cmd) return cmd } @@ -379,6 +384,9 @@ func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, coun args = append(args, "type", keyType) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(3) + } _ = c(ctx, cmd) return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/geo_commands.go b/vendor/github.com/redis/go-redis/v9/geo_commands.go index f047b98aaf9..0f274289314 100644 --- a/vendor/github.com/redis/go-redis/v9/geo_commands.go +++ b/vendor/github.com/redis/go-redis/v9/geo_commands.go @@ -33,7 +33,10 @@ func (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLoca return cmd } -// GeoRadius is a read-only GEORADIUS_RO command. +// GeoRadius queries a geospatial index for members within a distance from a coordinate. +// This is a read-only variant that does not support Store or StoreDist options. +// +// Deprecated: Use GeoSearch with BYRADIUS argument instead as of Redis 6.2.0. func (c cmdable) GeoRadius( ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery, ) *GeoLocationCmd { @@ -60,7 +63,10 @@ func (c cmdable) GeoRadiusStore( return cmd } -// GeoRadiusByMember is a read-only GEORADIUSBYMEMBER_RO command. +// GeoRadiusByMember queries a geospatial index for members within a distance from a member. +// This is a read-only variant that does not support Store or StoreDist options. +// +// Deprecated: Use GeoSearch with BYRADIUS and FROMMEMBER arguments instead as of Redis 6.2.0. func (c cmdable) GeoRadiusByMember( ctx context.Context, key, member string, query *GeoRadiusQuery, ) *GeoLocationCmd { diff --git a/vendor/github.com/redis/go-redis/v9/hash_commands.go b/vendor/github.com/redis/go-redis/v9/hash_commands.go index 98a361b3ef1..256b8746b43 100644 --- a/vendor/github.com/redis/go-redis/v9/hash_commands.go +++ b/vendor/github.com/redis/go-redis/v9/hash_commands.go @@ -3,6 +3,8 @@ package redis import ( "context" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type HashCmdable interface { @@ -68,6 +70,13 @@ func (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd { return cmd } +// HGetAll returns a map of all fields and values stored at key. +// +// Returns an empty map when key does not exist. +// +// Time complexity: O(N) where N is the size of the hash. +// +// See https://redis.io/commands/hgetall/ func (c cmdable) HGetAll(ctx context.Context, key string) *MapStringStringCmd { cmd := NewMapStringStringCmd(ctx, "hgetall", key) _ = c(ctx, cmd) @@ -114,16 +123,16 @@ func (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *Slice // HSet accepts values in following formats: // -// - HSet("myhash", "key1", "value1", "key2", "value2") +// - HSet(ctx, "myhash", "key1", "value1", "key2", "value2") // -// - HSet("myhash", []string{"key1", "value1", "key2", "value2"}) +// - HSet(ctx, "myhash", []string{"key1", "value1", "key2", "value2"}) // -// - HSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"}) +// - HSet(ctx, "myhash", map[string]interface{}{"key1": "value1", "key2": "value2"}) // // Playing struct With "redis" tag. // type MyHash struct { Key1 string `redis:"key1"`; Key2 int `redis:"key2"` } // -// - HSet("myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0 +// - HSet(ctx, "myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0 // // For struct, can be a structure pointer type, we only parse the field whose tag is redis. // if you don't want the field to be read, you can use the `redis:"-"` flag to ignore it, @@ -192,6 +201,9 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } @@ -211,6 +223,9 @@ func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, m } args = append(args, "novalues") cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go b/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go new file mode 100644 index 00000000000..024db3ffe9b --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go @@ -0,0 +1,122 @@ +package redis + +import ( + "context" + "errors" + "strings" +) + +// HOTKEYS commands are only available on standalone *Client instances. +// They are NOT available on ClusterClient, Ring, or UniversalClient because +// HOTKEYS is a stateful command requiring session affinity - all operations +// (START, GET, STOP, RESET) must be sent to the same Redis node. +// +// If you are using UniversalClient and need HOTKEYS functionality, you must +// type assert to *Client first: +// +// if client, ok := universalClient.(*redis.Client); ok { +// result, err := client.HotKeysStart(ctx, args) +// // ... +// } + +// HotKeysMetric represents the metrics that can be tracked by the HOTKEYS command. +type HotKeysMetric string + +const ( + // HotKeysMetricCPU tracks CPU time spent on the key (in microseconds). + HotKeysMetricCPU HotKeysMetric = "CPU" + // HotKeysMetricNET tracks network bytes used by the key (ingress + egress + replication). + HotKeysMetricNET HotKeysMetric = "NET" +) + +// HotKeysStartArgs contains the arguments for the HOTKEYS START command. +// This command is only available on standalone clients due to its stateful nature +// requiring session affinity. It must NOT be used on cluster or pooled clients. +type HotKeysStartArgs struct { + // Metrics to track. At least one must be specified. + Metrics []HotKeysMetric + // Count is the number of top keys to report. + // Default: 10, Min: 10, Max: 64 + Count uint8 + // Duration is the auto-stop tracking after this many seconds. + // Default: 0 (no auto-stop) + Duration int64 + // Sample is the sample ratio - track keys with probability 1/sample. + // Default: 1 (track every key), Min: 1 + Sample int64 + // Slots specifies specific hash slots to track (0-16383). + // All specified slots must be hosted by the receiving node. + // If not specified, all slots are tracked. + Slots []uint16 +} + +// ErrHotKeysNoMetrics is returned when HotKeysStart is called without any metrics specified. +var ErrHotKeysNoMetrics = errors.New("redis: at least one metric must be specified for HOTKEYS START") + +// HotKeysStart starts collecting hotkeys data. +// At least one metric must be specified in args.Metrics. +// This command is only available on standalone clients. +func (c *Client) HotKeysStart(ctx context.Context, args *HotKeysStartArgs) *StatusCmd { + cmdArgs := make([]interface{}, 0, 16) + cmdArgs = append(cmdArgs, "hotkeys", "start") + + // Validate that at least one metric is specified + if len(args.Metrics) == 0 { + cmd := NewStatusCmd(ctx, cmdArgs...) + cmd.SetErr(ErrHotKeysNoMetrics) + return cmd + } + + cmdArgs = append(cmdArgs, "metrics", len(args.Metrics)) + for _, metric := range args.Metrics { + cmdArgs = append(cmdArgs, strings.ToLower(string(metric))) + } + + if args.Count > 0 { + cmdArgs = append(cmdArgs, "count", args.Count) + } + + if args.Duration > 0 { + cmdArgs = append(cmdArgs, "duration", args.Duration) + } + + if args.Sample > 0 { + cmdArgs = append(cmdArgs, "sample", args.Sample) + } + + if len(args.Slots) > 0 { + cmdArgs = append(cmdArgs, "slots", len(args.Slots)) + for _, slot := range args.Slots { + cmdArgs = append(cmdArgs, slot) + } + } + + cmd := NewStatusCmd(ctx, cmdArgs...) + _ = c.Process(ctx, cmd) + return cmd +} + +// HotKeysStop stops the ongoing hotkeys collection session. +// This command is only available on standalone clients. +func (c *Client) HotKeysStop(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "hotkeys", "stop") + _ = c.Process(ctx, cmd) + return cmd +} + +// HotKeysReset discards the last hotkeys collection session results. +// Returns an error if tracking is currently active. +// This command is only available on standalone clients. +func (c *Client) HotKeysReset(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "hotkeys", "reset") + _ = c.Process(ctx, cmd) + return cmd +} + +// HotKeysGet retrieves the results of the ongoing or last hotkeys collection session. +// This command is only available on standalone clients. +func (c *Client) HotKeysGet(ctx context.Context) *HotKeysCmd { + cmd := NewHotKeysCmd(ctx, "hotkeys", "get") + _ = c.Process(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/conn_reauth_credentials_listener.go b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/conn_reauth_credentials_listener.go new file mode 100644 index 00000000000..22bfedf713b --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/conn_reauth_credentials_listener.go @@ -0,0 +1,100 @@ +package streaming + +import ( + "github.com/redis/go-redis/v9/auth" + "github.com/redis/go-redis/v9/internal/pool" +) + +// ConnReAuthCredentialsListener is a credentials listener for a specific connection +// that triggers re-authentication when credentials change. +// +// This listener implements the auth.CredentialsListener interface and is subscribed +// to a StreamingCredentialsProvider. When new credentials are received via OnNext, +// it marks the connection for re-authentication through the manager. +// +// The re-authentication is always performed asynchronously to avoid blocking the +// credentials provider and to prevent potential deadlocks with the pool semaphore. +// The actual re-auth happens when the connection is returned to the pool in an idle state. +// +// Lifecycle: +// - Created during connection initialization via Manager.Listener() +// - Subscribed to the StreamingCredentialsProvider +// - Receives credential updates via OnNext() +// - Cleaned up when connection is removed from pool via Manager.RemoveListener() +type ConnReAuthCredentialsListener struct { + // reAuth is the function to re-authenticate the connection with new credentials + reAuth func(conn *pool.Conn, credentials auth.Credentials) error + + // onErr is the function to call when re-authentication or acquisition fails + onErr func(conn *pool.Conn, err error) + + // conn is the connection this listener is associated with + conn *pool.Conn + + // manager is the streaming credentials manager for coordinating re-auth + manager *Manager +} + +// OnNext is called when new credentials are received from the StreamingCredentialsProvider. +// +// This method marks the connection for asynchronous re-authentication. The actual +// re-authentication happens in the background when the connection is returned to the +// pool and is in an idle state. +// +// Asynchronous re-auth is used to: +// - Avoid blocking the credentials provider's notification goroutine +// - Prevent deadlocks with the pool's semaphore (especially with small pool sizes) +// - Ensure re-auth happens when the connection is safe to use (not processing commands) +// +// The reAuthFn callback receives: +// - nil if the connection was successfully acquired for re-auth +// - error if acquisition timed out or failed +// +// Thread-safe: Called by the credentials provider's notification goroutine. +func (c *ConnReAuthCredentialsListener) OnNext(credentials auth.Credentials) { + if c.conn == nil || c.conn.IsClosed() || c.manager == nil || c.reAuth == nil { + return + } + + // Always use async reauth to avoid complex pool semaphore issues + // The synchronous path can cause deadlocks in the pool's semaphore mechanism + // when called from the Subscribe goroutine, especially with small pool sizes. + // The connection pool hook will re-authenticate the connection when it is + // returned to the pool in a clean, idle state. + c.manager.MarkForReAuth(c.conn, func(err error) { + // err is from connection acquisition (timeout, etc.) + if err != nil { + // Log the error + c.OnError(err) + return + } + // err is from reauth command execution + err = c.reAuth(c.conn, credentials) + if err != nil { + // Log the error + c.OnError(err) + return + } + }) +} + +// OnError is called when an error occurs during credential streaming or re-authentication. +// +// This method can be called from: +// - The StreamingCredentialsProvider when there's an error in the credentials stream +// - The re-auth process when connection acquisition times out +// - The re-auth process when the AUTH command fails +// +// The error is delegated to the onErr callback provided during listener creation. +// +// Thread-safe: Can be called from multiple goroutines (provider, re-auth worker). +func (c *ConnReAuthCredentialsListener) OnError(err error) { + if c.onErr == nil { + return + } + + c.onErr(c.conn, err) +} + +// Ensure ConnReAuthCredentialsListener implements the CredentialsListener interface. +var _ auth.CredentialsListener = (*ConnReAuthCredentialsListener)(nil) diff --git a/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/cred_listeners.go b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/cred_listeners.go new file mode 100644 index 00000000000..66e6eafdce8 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/cred_listeners.go @@ -0,0 +1,77 @@ +package streaming + +import ( + "sync" + + "github.com/redis/go-redis/v9/auth" +) + +// CredentialsListeners is a thread-safe collection of credentials listeners +// indexed by connection ID. +// +// This collection is used by the Manager to maintain a registry of listeners +// for each connection in the pool. Listeners are reused when connections are +// reinitialized (e.g., after a handoff) to avoid creating duplicate subscriptions +// to the StreamingCredentialsProvider. +// +// The collection supports concurrent access from multiple goroutines during +// connection initialization, credential updates, and connection removal. +type CredentialsListeners struct { + // listeners maps connection ID to credentials listener + listeners map[uint64]auth.CredentialsListener + + // lock protects concurrent access to the listeners map + lock sync.RWMutex +} + +// NewCredentialsListeners creates a new thread-safe credentials listeners collection. +func NewCredentialsListeners() *CredentialsListeners { + return &CredentialsListeners{ + listeners: make(map[uint64]auth.CredentialsListener), + } +} + +// Add adds or updates a credentials listener for a connection. +// +// If a listener already exists for the connection ID, it is replaced. +// This is safe because the old listener should have been unsubscribed +// before the connection was reinitialized. +// +// Thread-safe: Can be called concurrently from multiple goroutines. +func (c *CredentialsListeners) Add(connID uint64, listener auth.CredentialsListener) { + c.lock.Lock() + defer c.lock.Unlock() + if c.listeners == nil { + c.listeners = make(map[uint64]auth.CredentialsListener) + } + c.listeners[connID] = listener +} + +// Get retrieves the credentials listener for a connection. +// +// Returns: +// - listener: The credentials listener for the connection, or nil if not found +// - ok: true if a listener exists for the connection ID, false otherwise +// +// Thread-safe: Can be called concurrently from multiple goroutines. +func (c *CredentialsListeners) Get(connID uint64) (auth.CredentialsListener, bool) { + c.lock.RLock() + defer c.lock.RUnlock() + if len(c.listeners) == 0 { + return nil, false + } + listener, ok := c.listeners[connID] + return listener, ok +} + +// Remove removes the credentials listener for a connection. +// +// This is called when a connection is removed from the pool to prevent +// memory leaks. If no listener exists for the connection ID, this is a no-op. +// +// Thread-safe: Can be called concurrently from multiple goroutines. +func (c *CredentialsListeners) Remove(connID uint64) { + c.lock.Lock() + defer c.lock.Unlock() + delete(c.listeners, connID) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/manager.go b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/manager.go new file mode 100644 index 00000000000..f785927ee32 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/manager.go @@ -0,0 +1,137 @@ +package streaming + +import ( + "errors" + "time" + + "github.com/redis/go-redis/v9/auth" + "github.com/redis/go-redis/v9/internal/pool" +) + +// Manager coordinates streaming credentials and re-authentication for a connection pool. +// +// The manager is responsible for: +// - Creating and managing per-connection credentials listeners +// - Providing the pool hook for re-authentication +// - Coordinating between credentials updates and pool operations +// +// When credentials change via a StreamingCredentialsProvider: +// 1. The credentials listener (ConnReAuthCredentialsListener) receives the update +// 2. It calls MarkForReAuth on the manager +// 3. The manager delegates to the pool hook +// 4. The pool hook schedules background re-authentication +// +// The manager maintains a registry of credentials listeners indexed by connection ID, +// allowing listener reuse when connections are reinitialized (e.g., after handoff). +type Manager struct { + // credentialsListeners maps connection ID to credentials listener + credentialsListeners *CredentialsListeners + + // pool is the connection pool being managed + pool pool.Pooler + + // poolHookRef is the re-authentication pool hook + poolHookRef *ReAuthPoolHook +} + +// NewManager creates a new streaming credentials manager. +// +// Parameters: +// - pl: The connection pool to manage +// - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication +// +// The manager creates a ReAuthPoolHook sized to match the pool size, ensuring that +// re-auth operations don't exhaust the connection pool. +func NewManager(pl pool.Pooler, reAuthTimeout time.Duration) *Manager { + m := &Manager{ + pool: pl, + poolHookRef: NewReAuthPoolHook(pl.Size(), reAuthTimeout), + credentialsListeners: NewCredentialsListeners(), + } + m.poolHookRef.manager = m + return m +} + +// PoolHook returns the pool hook for re-authentication. +// +// This hook should be registered with the connection pool to enable +// automatic re-authentication when credentials change. +func (m *Manager) PoolHook() pool.PoolHook { + return m.poolHookRef +} + +// Listener returns or creates a credentials listener for a connection. +// +// This method is called during connection initialization to set up the +// credentials listener. If a listener already exists for the connection ID +// (e.g., after a handoff), it is reused. +// +// Parameters: +// - poolCn: The connection to create/get a listener for +// - reAuth: Function to re-authenticate the connection with new credentials +// - onErr: Function to call when re-authentication fails +// +// Returns: +// - auth.CredentialsListener: The listener to subscribe to the credentials provider +// - error: Non-nil if poolCn is nil +// +// Note: The reAuth and onErr callbacks are captured once when the listener is +// created and reused for the connection's lifetime. They should not change. +// +// Thread-safe: Can be called concurrently during connection initialization. +func (m *Manager) Listener( + poolCn *pool.Conn, + reAuth func(*pool.Conn, auth.Credentials) error, + onErr func(*pool.Conn, error), +) (auth.CredentialsListener, error) { + if poolCn == nil { + return nil, errors.New("poolCn cannot be nil") + } + connID := poolCn.GetID() + // if we reconnect the underlying network connection, the streaming credentials listener will continue to work + // so we can get the old listener from the cache and use it. + // subscribing the same (an already subscribed) listener for a StreamingCredentialsProvider SHOULD be a no-op + listener, ok := m.credentialsListeners.Get(connID) + if !ok || listener == nil { + // Create new listener for this connection + // Note: Callbacks (reAuth, onErr) are captured once and reused for the connection's lifetime + newCredListener := &ConnReAuthCredentialsListener{ + conn: poolCn, + reAuth: reAuth, + onErr: onErr, + manager: m, + } + + m.credentialsListeners.Add(connID, newCredListener) + listener = newCredListener + } + return listener, nil +} + +// MarkForReAuth marks a connection for re-authentication. +// +// This method is called by the credentials listener when new credentials are +// received. It delegates to the pool hook to schedule background re-authentication. +// +// Parameters: +// - poolCn: The connection to re-authenticate +// - reAuthFn: Function to call for re-authentication, receives error if acquisition fails +// +// Thread-safe: Called by credentials listeners when credentials change. +func (m *Manager) MarkForReAuth(poolCn *pool.Conn, reAuthFn func(error)) { + connID := poolCn.GetID() + m.poolHookRef.MarkForReAuth(connID, reAuthFn) +} + +// RemoveListener removes the credentials listener for a connection. +// +// This method is called by the pool hook's OnRemove to clean up listeners +// when connections are removed from the pool. +// +// Parameters: +// - connID: The connection ID whose listener should be removed +// +// Thread-safe: Called during connection removal. +func (m *Manager) RemoveListener(connID uint64) { + m.credentialsListeners.Remove(connID) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/pool_hook.go b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/pool_hook.go new file mode 100644 index 00000000000..aaf4f6099f7 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/auth/streaming/pool_hook.go @@ -0,0 +1,241 @@ +package streaming + +import ( + "context" + "sync" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/pool" +) + +// ReAuthPoolHook is a pool hook that manages background re-authentication of connections +// when credentials change via a streaming credentials provider. +// +// The hook uses a semaphore-based worker pool to limit concurrent re-authentication +// operations and prevent pool exhaustion. When credentials change, connections are +// marked for re-authentication and processed asynchronously in the background. +// +// The re-authentication process: +// 1. OnPut: When a connection is returned to the pool, check if it needs re-auth +// 2. If yes, schedule it for background processing (move from shouldReAuth to scheduledReAuth) +// 3. A worker goroutine acquires the connection (waits until it's not in use) +// 4. Executes the re-auth function while holding the connection +// 5. Releases the connection back to the pool +// +// The hook ensures that: +// - Only one re-auth operation runs per connection at a time +// - Connections are not used for commands during re-authentication +// - Re-auth operations timeout if they can't acquire the connection +// - Resources are properly cleaned up on connection removal +type ReAuthPoolHook struct { + // shouldReAuth maps connection ID to re-auth function + // Connections in this map need re-authentication but haven't been scheduled yet + shouldReAuth map[uint64]func(error) + shouldReAuthLock sync.RWMutex + + // workers is a semaphore limiting concurrent re-auth operations + // Initialized with poolSize tokens to prevent pool exhaustion + // Uses FastSemaphore for better performance with eventual fairness + workers *internal.FastSemaphore + + // reAuthTimeout is the maximum time to wait for acquiring a connection for re-auth + reAuthTimeout time.Duration + + // scheduledReAuth maps connection ID to scheduled status + // Connections in this map have a background worker attempting re-authentication + scheduledReAuth map[uint64]bool + scheduledLock sync.RWMutex + + // manager is a back-reference for cleanup operations + manager *Manager +} + +// NewReAuthPoolHook creates a new re-authentication pool hook. +// +// Parameters: +// - poolSize: Maximum number of concurrent re-auth operations (typically matches pool size) +// - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication +// +// The poolSize parameter is used to initialize the worker semaphore, ensuring that +// re-auth operations don't exhaust the connection pool. +func NewReAuthPoolHook(poolSize int, reAuthTimeout time.Duration) *ReAuthPoolHook { + return &ReAuthPoolHook{ + shouldReAuth: make(map[uint64]func(error)), + scheduledReAuth: make(map[uint64]bool), + workers: internal.NewFastSemaphore(int32(poolSize)), + reAuthTimeout: reAuthTimeout, + } +} + +// MarkForReAuth marks a connection for re-authentication. +// +// This method is called when credentials change and a connection needs to be +// re-authenticated. The actual re-authentication happens asynchronously when +// the connection is returned to the pool (in OnPut). +// +// Parameters: +// - connID: The connection ID to mark for re-authentication +// - reAuthFn: Function to call for re-authentication, receives error if acquisition fails +// +// Thread-safe: Can be called concurrently from multiple goroutines. +func (r *ReAuthPoolHook) MarkForReAuth(connID uint64, reAuthFn func(error)) { + r.shouldReAuthLock.Lock() + defer r.shouldReAuthLock.Unlock() + r.shouldReAuth[connID] = reAuthFn +} + +// OnGet is called when a connection is retrieved from the pool. +// +// This hook checks if the connection needs re-authentication or has a scheduled +// re-auth operation. If so, it rejects the connection (returns accept=false), +// causing the pool to try another connection. +// +// Returns: +// - accept: false if connection needs re-auth, true otherwise +// - err: always nil (errors are not used in this hook) +// +// Thread-safe: Called concurrently by multiple goroutines getting connections. +func (r *ReAuthPoolHook) OnGet(_ context.Context, conn *pool.Conn, _ bool) (accept bool, err error) { + connID := conn.GetID() + r.shouldReAuthLock.RLock() + _, shouldReAuth := r.shouldReAuth[connID] + r.shouldReAuthLock.RUnlock() + // This connection was marked for reauth while in the pool, + // reject the connection + if shouldReAuth { + // simply reject the connection, it will be re-authenticated in OnPut + return false, nil + } + r.scheduledLock.RLock() + _, hasScheduled := r.scheduledReAuth[connID] + r.scheduledLock.RUnlock() + // has scheduled reauth, reject the connection + if hasScheduled { + // simply reject the connection, it currently has a reauth scheduled + // and the worker is waiting for slot to execute the reauth + return false, nil + } + return true, nil +} + +// OnPut is called when a connection is returned to the pool. +// +// This hook checks if the connection needs re-authentication. If so, it schedules +// a background goroutine to perform the re-auth asynchronously. The goroutine: +// 1. Waits for a worker slot (semaphore) +// 2. Acquires the connection (waits until not in use) +// 3. Executes the re-auth function +// 4. Releases the connection and worker slot +// +// The connection is always pooled (not removed) since re-auth happens in background. +// +// Returns: +// - shouldPool: always true (connection stays in pool during background re-auth) +// - shouldRemove: always false +// - err: always nil +// +// Thread-safe: Called concurrently by multiple goroutines returning connections. +func (r *ReAuthPoolHook) OnPut(_ context.Context, conn *pool.Conn) (bool, bool, error) { + if conn == nil { + // noop + return true, false, nil + } + connID := conn.GetID() + // Check if reauth is needed and get the function with proper locking + r.shouldReAuthLock.RLock() + reAuthFn, ok := r.shouldReAuth[connID] + r.shouldReAuthLock.RUnlock() + + if ok { + // Acquire both locks to atomically move from shouldReAuth to scheduledReAuth + // This prevents race conditions where OnGet might miss the transition + r.shouldReAuthLock.Lock() + r.scheduledLock.Lock() + r.scheduledReAuth[connID] = true + delete(r.shouldReAuth, connID) + r.scheduledLock.Unlock() + r.shouldReAuthLock.Unlock() + go func() { + r.workers.AcquireBlocking() + // safety first + if conn == nil || (conn != nil && conn.IsClosed()) { + r.workers.Release() + return + } + defer func() { + if rec := recover(); rec != nil { + // once again - safety first + internal.Logger.Printf(context.Background(), "panic in reauth worker: %v", rec) + } + r.scheduledLock.Lock() + delete(r.scheduledReAuth, connID) + r.scheduledLock.Unlock() + r.workers.Release() + }() + + // Create timeout context for connection acquisition + // This prevents indefinite waiting if the connection is stuck + ctx, cancel := context.WithTimeout(context.Background(), r.reAuthTimeout) + defer cancel() + + // Try to acquire the connection for re-authentication + // We need to ensure the connection is IDLE (not IN_USE) before transitioning to UNUSABLE + // This prevents re-authentication from interfering with active commands + // Use AwaitAndTransition to wait for the connection to become IDLE + stateMachine := conn.GetStateMachine() + if stateMachine == nil { + // No state machine - should not happen, but handle gracefully + reAuthFn(pool.ErrConnUnusableTimeout) + return + } + + // Use predefined slice to avoid allocation + _, err := stateMachine.AwaitAndTransition(ctx, pool.ValidFromIdle(), pool.StateUnusable) + if err != nil { + // Timeout or other error occurred, cannot acquire connection + reAuthFn(err) + return + } + + // safety first + if !conn.IsClosed() { + // Successfully acquired the connection, perform reauth + reAuthFn(nil) + } + + // Release the connection: transition from UNUSABLE back to IDLE + stateMachine.Transition(pool.StateIdle) + }() + } + + // the reauth will happen in background, as far as the pool is concerned: + // pool the connection, don't remove it, no error + return true, false, nil +} + +// OnRemove is called when a connection is removed from the pool. +// +// This hook cleans up all state associated with the connection: +// - Removes from shouldReAuth map (pending re-auth) +// - Removes from scheduledReAuth map (active re-auth) +// - Removes credentials listener from manager +// +// This prevents memory leaks and ensures that removed connections don't have +// lingering re-auth operations or listeners. +// +// Thread-safe: Called when connections are removed due to errors, timeouts, or pool closure. +func (r *ReAuthPoolHook) OnRemove(_ context.Context, conn *pool.Conn, _ error) { + connID := conn.GetID() + r.shouldReAuthLock.Lock() + r.scheduledLock.Lock() + delete(r.scheduledReAuth, connID) + delete(r.shouldReAuth, connID) + r.scheduledLock.Unlock() + r.shouldReAuthLock.Unlock() + if r.manager != nil { + r.manager.RemoveListener(connID) + } +} + +var _ pool.PoolHook = (*ReAuthPoolHook)(nil) diff --git a/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go b/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go index f13ee816d6e..8aa87db3d42 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go +++ b/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go @@ -1,9 +1,8 @@ package hashtag import ( + "math/rand" "strings" - - "github.com/redis/go-redis/v9/internal/rand" ) const slotNumber = 16384 @@ -11,7 +10,7 @@ const slotNumber = 16384 // CRC16 implementation according to CCITT standards. // Copyright 2001-2010 Georges Menie (www.menie.org) // Copyright 2013 The Go Authors. All rights reserved. -// http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c +// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c. var crc16tab = [256]uint16{ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, @@ -56,6 +55,18 @@ func Key(key string) string { return key } +func Present(key string) bool { + if key == "" { + return false + } + if s := strings.IndexByte(key, '{'); s > -1 { + if e := strings.IndexByte(key[s+1:], '}'); e > 0 { + return true + } + } + return false +} + func RandomSlot() int { return rand.Intn(slotNumber) } diff --git a/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go b/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go new file mode 100644 index 00000000000..214f7e8ea95 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go @@ -0,0 +1,54 @@ +package hashtag + +import "github.com/cespare/xxhash/v2" + +// RendezvousHash implements HRW (Highest Random Weight) hashing. +type RendezvousHash struct { + nodes []node +} + +type node struct { + name string + hash uint64 +} + +// NewRendezvousHash builds a hash from shard names. +func NewRendezvousHash(shards []string) *RendezvousHash { + n := make([]node, len(shards)) + for i, s := range shards { + n[i] = node{ + name: s, + hash: xxhash.Sum64String(s), + } + } + return &RendezvousHash{nodes: n} +} + +// Get returns the shard name for the given key. +func (r *RendezvousHash) Get(key string) string { + if len(r.nodes) == 0 { + return "" + } + + kh := xxhash.Sum64String(key) + + bestIdx := 0 + bestScore := mix64(kh ^ r.nodes[0].hash) + + for i := 1; i < len(r.nodes); i++ { + if score := mix64(kh ^ r.nodes[i].hash); score > bestScore { + bestScore = score + bestIdx = i + } + } + + return r.nodes[bestIdx].name +} + +// mix64 is a xorshift-based mixing function. +func mix64(x uint64) uint64 { + x ^= x >> 12 + x ^= x << 25 + x ^= x >> 27 + return x * 2685821657736338717 +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go b/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go index 1a560e4a399..408ce0e4b3c 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go +++ b/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go @@ -109,6 +109,8 @@ func (s StructValue) Scan(key string, value string) error { return scan.ScanRedis(value) case encoding.TextUnmarshaler: return scan.UnmarshalText(util.StringToBytes(value)) + case encoding.BinaryUnmarshaler: + return scan.UnmarshalBinary(util.StringToBytes(value)) } } diff --git a/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go b/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go new file mode 100644 index 00000000000..8f8569719e4 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go @@ -0,0 +1,59 @@ +// Package interfaces provides shared interfaces used by both the main redis package +// and the maintnotifications upgrade package to avoid circular dependencies. +package interfaces + +import ( + "context" + "net" + "time" +) + +// NotificationProcessor is (most probably) a push.NotificationProcessor +// forward declaration to avoid circular imports +type NotificationProcessor interface { + RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error + UnregisterHandler(pushNotificationName string) error + GetHandler(pushNotificationName string) interface{} +} + +// ClientInterface defines the interface that clients must implement for maintnotifications upgrades. +type ClientInterface interface { + // GetOptions returns the client options. + GetOptions() OptionsInterface + + // GetPushProcessor returns the client's push notification processor. + GetPushProcessor() NotificationProcessor +} + +// OptionsInterface defines the interface for client options. +// Uses an adapter pattern to avoid circular dependencies. +type OptionsInterface interface { + // GetReadTimeout returns the read timeout. + GetReadTimeout() time.Duration + + // GetWriteTimeout returns the write timeout. + GetWriteTimeout() time.Duration + + // GetNetwork returns the network type. + GetNetwork() string + + // GetAddr returns the connection address. + GetAddr() string + + // GetNodeAddress returns the address of the Redis node as reported by the server. + // For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation. + // For standalone clients, this defaults to Addr. + GetNodeAddress() string + + // IsTLSEnabled returns true if TLS is enabled. + IsTLSEnabled() bool + + // GetProtocol returns the protocol version. + GetProtocol() int + + // GetPoolSize returns the connection pool size. + GetPoolSize() int + + // NewDialer returns a new dialer function for the connection. + NewDialer() func(context.Context) (net.Conn, error) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/internal.go b/vendor/github.com/redis/go-redis/v9/internal/internal.go index e783d139a55..403db56cae0 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/internal.go +++ b/vendor/github.com/redis/go-redis/v9/internal/internal.go @@ -1,9 +1,8 @@ package internal import ( + "math/rand" "time" - - "github.com/redis/go-redis/v9/internal/rand" ) func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration { diff --git a/vendor/github.com/redis/go-redis/v9/internal/log.go b/vendor/github.com/redis/go-redis/v9/internal/log.go index c8b9213de48..0bfffc311b4 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/log.go +++ b/vendor/github.com/redis/go-redis/v9/internal/log.go @@ -7,20 +7,73 @@ import ( "os" ) +// TODO (ned): Revisit logging +// Add more standardized approach with log levels and configurability + type Logging interface { Printf(ctx context.Context, format string, v ...interface{}) } -type logger struct { +type DefaultLogger struct { log *log.Logger } -func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) { +func (l *DefaultLogger) Printf(ctx context.Context, format string, v ...interface{}) { _ = l.log.Output(2, fmt.Sprintf(format, v...)) } +func NewDefaultLogger() Logging { + return &DefaultLogger{ + log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile), + } +} + // Logger calls Output to print to the stderr. // Arguments are handled in the manner of fmt.Print. -var Logger Logging = &logger{ - log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile), +var Logger Logging = NewDefaultLogger() + +var LogLevel LogLevelT = LogLevelError + +// LogLevelT represents the logging level +type LogLevelT int + +// Log level constants for the entire go-redis library +const ( + LogLevelError LogLevelT = iota // 0 - errors only + LogLevelWarn // 1 - warnings and errors + LogLevelInfo // 2 - info, warnings, and errors + LogLevelDebug // 3 - debug, info, warnings, and errors +) + +// String returns the string representation of the log level +func (l LogLevelT) String() string { + switch l { + case LogLevelError: + return "ERROR" + case LogLevelWarn: + return "WARN" + case LogLevelInfo: + return "INFO" + case LogLevelDebug: + return "DEBUG" + default: + return "UNKNOWN" + } +} + +// IsValid returns true if the log level is valid +func (l LogLevelT) IsValid() bool { + return l >= LogLevelError && l <= LogLevelDebug +} + +func (l LogLevelT) WarnOrAbove() bool { + return l >= LogLevelWarn +} + +func (l LogLevelT) InfoOrAbove() bool { + return l >= LogLevelInfo +} + +func (l LogLevelT) DebugOrAbove() bool { + return l >= LogLevelDebug } diff --git a/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go b/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go new file mode 100644 index 00000000000..93e5bded8de --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go @@ -0,0 +1,663 @@ +package logs + +import ( + "encoding/json" + "fmt" + "regexp" + + "github.com/redis/go-redis/v9/internal" +) + +// appendJSONIfDebug appends JSON data to a message only if the global log level is Debug +func appendJSONIfDebug(message string, data map[string]interface{}) string { + if internal.LogLevel.DebugOrAbove() { + jsonData, _ := json.Marshal(data) + return fmt.Sprintf("%s %s", message, string(jsonData)) + } + return message +} + +const ( + // ======================================== + // CIRCUIT_BREAKER.GO - Circuit breaker management + // ======================================== + CircuitBreakerTransitioningToHalfOpenMessage = "circuit breaker transitioning to half-open" + CircuitBreakerOpenedMessage = "circuit breaker opened" + CircuitBreakerReopenedMessage = "circuit breaker reopened" + CircuitBreakerClosedMessage = "circuit breaker closed" + CircuitBreakerCleanupMessage = "circuit breaker cleanup" + CircuitBreakerOpenMessage = "circuit breaker is open, failing fast" + + // ======================================== + // CONFIG.GO - Configuration and debug + // ======================================== + DebugLoggingEnabledMessage = "debug logging enabled" + ConfigDebugMessage = "config debug" + + // ======================================== + // ERRORS.GO - Error message constants + // ======================================== + InvalidRelaxedTimeoutErrorMessage = "relaxed timeout must be greater than 0" + InvalidHandoffTimeoutErrorMessage = "handoff timeout must be greater than 0" + InvalidHandoffWorkersErrorMessage = "MaxWorkers must be greater than or equal to 0" + InvalidHandoffQueueSizeErrorMessage = "handoff queue size must be greater than 0" + InvalidPostHandoffRelaxedDurationErrorMessage = "post-handoff relaxed duration must be greater than or equal to 0" + InvalidEndpointTypeErrorMessage = "invalid endpoint type" + InvalidMaintNotificationsErrorMessage = "invalid maintenance notifications setting (must be 'disabled', 'enabled', or 'auto')" + InvalidHandoffRetriesErrorMessage = "MaxHandoffRetries must be between 1 and 10" + InvalidClientErrorMessage = "invalid client type" + InvalidNotificationErrorMessage = "invalid notification format" + MaxHandoffRetriesReachedErrorMessage = "max handoff retries reached" + HandoffQueueFullErrorMessage = "handoff queue is full, cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration" + InvalidCircuitBreakerFailureThresholdErrorMessage = "circuit breaker failure threshold must be >= 1" + InvalidCircuitBreakerResetTimeoutErrorMessage = "circuit breaker reset timeout must be >= 0" + InvalidCircuitBreakerMaxRequestsErrorMessage = "circuit breaker max requests must be >= 1" + ConnectionMarkedForHandoffErrorMessage = "connection marked for handoff" + ConnectionInvalidHandoffStateErrorMessage = "connection is in invalid state for handoff" + ShutdownErrorMessage = "shutdown" + CircuitBreakerOpenErrorMessage = "circuit breaker is open, failing fast" + + // ======================================== + // EXAMPLE_HOOKS.GO - Example metrics hooks + // ======================================== + MetricsHookProcessingNotificationMessage = "metrics hook processing" + MetricsHookRecordedErrorMessage = "metrics hook recorded error" + + // ======================================== + // HANDOFF_WORKER.GO - Connection handoff processing + // ======================================== + HandoffStartedMessage = "handoff started" + HandoffFailedMessage = "handoff failed" + ConnectionNotMarkedForHandoffMessage = "is not marked for handoff and has no retries" + ConnectionNotMarkedForHandoffErrorMessage = "is not marked for handoff" + HandoffRetryAttemptMessage = "Performing handoff" + CannotQueueHandoffForRetryMessage = "can't queue handoff for retry" + HandoffQueueFullMessage = "handoff queue is full" + FailedToDialNewEndpointMessage = "failed to dial new endpoint" + ApplyingRelaxedTimeoutDueToPostHandoffMessage = "applying relaxed timeout due to post-handoff" + HandoffSuccessMessage = "handoff succeeded" + RemovingConnectionFromPoolMessage = "removing connection from pool" + NoPoolProvidedMessageCannotRemoveMessage = "no pool provided, cannot remove connection, closing it" + WorkerExitingDueToShutdownMessage = "worker exiting due to shutdown" + WorkerExitingDueToShutdownWhileProcessingMessage = "worker exiting due to shutdown while processing request" + WorkerPanicRecoveredMessage = "worker panic recovered" + WorkerExitingDueToInactivityTimeoutMessage = "worker exiting due to inactivity timeout" + ReachedMaxHandoffRetriesMessage = "reached max handoff retries" + + // ======================================== + // MANAGER.GO - Moving operation tracking and handler registration + // ======================================== + DuplicateMovingOperationMessage = "duplicate MOVING operation ignored" + TrackingMovingOperationMessage = "tracking MOVING operation" + UntrackingMovingOperationMessage = "untracking MOVING operation" + OperationNotTrackedMessage = "operation not tracked" + FailedToRegisterHandlerMessage = "failed to register handler" + + // ======================================== + // HOOKS.GO - Notification processing hooks + // ======================================== + ProcessingNotificationMessage = "processing notification started" + ProcessingNotificationFailedMessage = "proccessing notification failed" + ProcessingNotificationSucceededMessage = "processing notification succeeded" + + // ======================================== + // POOL_HOOK.GO - Pool connection management + // ======================================== + FailedToQueueHandoffMessage = "failed to queue handoff" + MarkedForHandoffMessage = "connection marked for handoff" + + // ======================================== + // PUSH_NOTIFICATION_HANDLER.GO - Push notification validation and processing + // ======================================== + InvalidNotificationFormatMessage = "invalid notification format" + InvalidNotificationTypeFormatMessage = "invalid notification type format" + InvalidSeqIDInMovingNotificationMessage = "invalid seqID in MOVING notification" + InvalidTimeSInMovingNotificationMessage = "invalid timeS in MOVING notification" + InvalidNewEndpointInMovingNotificationMessage = "invalid newEndpoint in MOVING notification" + NoConnectionInHandlerContextMessage = "no connection in handler context" + InvalidConnectionTypeInHandlerContextMessage = "invalid connection type in handler context" + SchedulingHandoffToCurrentEndpointMessage = "scheduling handoff to current endpoint" + RelaxedTimeoutDueToNotificationMessage = "applying relaxed timeout due to notification" + UnrelaxedTimeoutMessage = "clearing relaxed timeout" + ManagerNotInitializedMessage = "manager not initialized" + FailedToMarkForHandoffMessage = "failed to mark connection for handoff" + InvalidSeqIDInSMigratingNotificationMessage = "invalid SeqID in SMIGRATING notification" + InvalidSeqIDInSMigratedNotificationMessage = "invalid SeqID in SMIGRATED notification" + TriggeringClusterStateReloadMessage = "triggering cluster state reload" + + // ======================================== + // used in pool/conn + // ======================================== + UnrelaxedTimeoutAfterDeadlineMessage = "clearing relaxed timeout after deadline" +) + +func HandoffStarted(connID uint64, newEndpoint string) string { + message := fmt.Sprintf("conn[%d] %s to %s", connID, HandoffStartedMessage, newEndpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": newEndpoint, + }) +} + +func HandoffFailed(connID uint64, newEndpoint string, attempt int, maxAttempts int, err error) string { + message := fmt.Sprintf("conn[%d] %s to %s (attempt %d/%d): %v", connID, HandoffFailedMessage, newEndpoint, attempt, maxAttempts, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": newEndpoint, + "attempt": attempt, + "maxAttempts": maxAttempts, + "error": err.Error(), + }) +} + +func HandoffSucceeded(connID uint64, newEndpoint string) string { + message := fmt.Sprintf("conn[%d] %s to %s", connID, HandoffSuccessMessage, newEndpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": newEndpoint, + }) +} + +// Timeout-related log functions +func RelaxedTimeoutDueToNotification(connID uint64, notificationType string, timeout interface{}) string { + message := fmt.Sprintf("conn[%d] %s %s (%v)", connID, RelaxedTimeoutDueToNotificationMessage, notificationType, timeout) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "notificationType": notificationType, + "timeout": fmt.Sprintf("%v", timeout), + }) +} + +func UnrelaxedTimeout(connID uint64) string { + message := fmt.Sprintf("conn[%d] %s", connID, UnrelaxedTimeoutMessage) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + }) +} + +func UnrelaxedTimeoutAfterDeadline(connID uint64) string { + message := fmt.Sprintf("conn[%d] %s", connID, UnrelaxedTimeoutAfterDeadlineMessage) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + }) +} + +// Handoff queue and marking functions +func HandoffQueueFull(queueLen, queueCap int) string { + message := fmt.Sprintf("%s (%d/%d), cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration", HandoffQueueFullMessage, queueLen, queueCap) + return appendJSONIfDebug(message, map[string]interface{}{ + "queueLen": queueLen, + "queueCap": queueCap, + }) +} + +func FailedToQueueHandoff(connID uint64, err error) string { + message := fmt.Sprintf("conn[%d] %s: %v", connID, FailedToQueueHandoffMessage, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "error": err.Error(), + }) +} + +func FailedToMarkForHandoff(connID uint64, err error) string { + message := fmt.Sprintf("conn[%d] %s: %v", connID, FailedToMarkForHandoffMessage, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "error": err.Error(), + }) +} + +func FailedToDialNewEndpoint(connID uint64, endpoint string, err error) string { + message := fmt.Sprintf("conn[%d] %s %s: %v", connID, FailedToDialNewEndpointMessage, endpoint, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": endpoint, + "error": err.Error(), + }) +} + +func ReachedMaxHandoffRetries(connID uint64, endpoint string, maxRetries int) string { + message := fmt.Sprintf("conn[%d] %s to %s (max retries: %d)", connID, ReachedMaxHandoffRetriesMessage, endpoint, maxRetries) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": endpoint, + "maxRetries": maxRetries, + }) +} + +// Notification processing functions +func ProcessingNotification(connID uint64, seqID int64, notificationType string, notification interface{}) string { + message := fmt.Sprintf("conn[%d] seqID[%d] %s %s: %v", connID, seqID, ProcessingNotificationMessage, notificationType, notification) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "seqID": seqID, + "notificationType": notificationType, + "notification": fmt.Sprintf("%v", notification), + }) +} + +func ProcessingNotificationFailed(connID uint64, notificationType string, err error, notification interface{}) string { + message := fmt.Sprintf("conn[%d] %s %s: %v - %v", connID, ProcessingNotificationFailedMessage, notificationType, err, notification) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "notificationType": notificationType, + "error": err.Error(), + "notification": fmt.Sprintf("%v", notification), + }) +} + +func ProcessingNotificationSucceeded(connID uint64, notificationType string) string { + message := fmt.Sprintf("conn[%d] %s %s", connID, ProcessingNotificationSucceededMessage, notificationType) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "notificationType": notificationType, + }) +} + +// Moving operation tracking functions +func DuplicateMovingOperation(connID uint64, endpoint string, seqID int64) string { + message := fmt.Sprintf("conn[%d] %s for %s seqID[%d]", connID, DuplicateMovingOperationMessage, endpoint, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": endpoint, + "seqID": seqID, + }) +} + +func TrackingMovingOperation(connID uint64, endpoint string, seqID int64) string { + message := fmt.Sprintf("conn[%d] %s for %s seqID[%d]", connID, TrackingMovingOperationMessage, endpoint, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": endpoint, + "seqID": seqID, + }) +} + +func UntrackingMovingOperation(connID uint64, seqID int64) string { + message := fmt.Sprintf("conn[%d] %s seqID[%d]", connID, UntrackingMovingOperationMessage, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "seqID": seqID, + }) +} + +func OperationNotTracked(connID uint64, seqID int64) string { + message := fmt.Sprintf("conn[%d] %s seqID[%d]", connID, OperationNotTrackedMessage, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "seqID": seqID, + }) +} + +// Connection pool functions +func RemovingConnectionFromPool(connID uint64, reason error) string { + metadata := map[string]interface{}{ + "connID": connID, + "reason": "unknown", // this will be overwritten if reason is not nil + } + if reason != nil { + metadata["reason"] = reason.Error() + } + + message := fmt.Sprintf("conn[%d] %s due to: %v", connID, RemovingConnectionFromPoolMessage, reason) + return appendJSONIfDebug(message, metadata) +} + +func NoPoolProvidedCannotRemove(connID uint64, reason error) string { + metadata := map[string]interface{}{ + "connID": connID, + "reason": "unknown", // this will be overwritten if reason is not nil + } + if reason != nil { + metadata["reason"] = reason.Error() + } + + message := fmt.Sprintf("conn[%d] %s due to: %v", connID, NoPoolProvidedMessageCannotRemoveMessage, reason) + return appendJSONIfDebug(message, metadata) +} + +// Circuit breaker functions +func CircuitBreakerOpen(connID uint64, endpoint string) string { + message := fmt.Sprintf("conn[%d] %s for %s", connID, CircuitBreakerOpenMessage, endpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "endpoint": endpoint, + }) +} + +// Additional handoff functions for specific cases +func ConnectionNotMarkedForHandoff(connID uint64) string { + message := fmt.Sprintf("conn[%d] %s", connID, ConnectionNotMarkedForHandoffMessage) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + }) +} + +func ConnectionNotMarkedForHandoffError(connID uint64) string { + return fmt.Sprintf("conn[%d] %s", connID, ConnectionNotMarkedForHandoffErrorMessage) +} + +func HandoffRetryAttempt(connID uint64, retries int, newEndpoint string, oldEndpoint string) string { + message := fmt.Sprintf("conn[%d] Retry %d: %s to %s(was %s)", connID, retries, HandoffRetryAttemptMessage, newEndpoint, oldEndpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "retries": retries, + "newEndpoint": newEndpoint, + "oldEndpoint": oldEndpoint, + }) +} + +func CannotQueueHandoffForRetry(err error) string { + message := fmt.Sprintf("%s: %v", CannotQueueHandoffForRetryMessage, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "error": err.Error(), + }) +} + +// Validation and error functions +func InvalidNotificationFormat(notification interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidNotificationFormatMessage, notification) + return appendJSONIfDebug(message, map[string]interface{}{ + "notification": fmt.Sprintf("%v", notification), + }) +} + +func InvalidNotificationTypeFormat(notificationType interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidNotificationTypeFormatMessage, notificationType) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": fmt.Sprintf("%v", notificationType), + }) +} + +// InvalidNotification creates a log message for invalid notifications of any type +func InvalidNotification(notificationType string, notification interface{}) string { + message := fmt.Sprintf("invalid %s notification: %v", notificationType, notification) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + "notification": fmt.Sprintf("%v", notification), + }) +} + +func InvalidSeqIDInMovingNotification(seqID interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidSeqIDInMovingNotificationMessage, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "seqID": fmt.Sprintf("%v", seqID), + }) +} + +func InvalidTimeSInMovingNotification(timeS interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidTimeSInMovingNotificationMessage, timeS) + return appendJSONIfDebug(message, map[string]interface{}{ + "timeS": fmt.Sprintf("%v", timeS), + }) +} + +func InvalidNewEndpointInMovingNotification(newEndpoint interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidNewEndpointInMovingNotificationMessage, newEndpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "newEndpoint": fmt.Sprintf("%v", newEndpoint), + }) +} + +func NoConnectionInHandlerContext(notificationType string) string { + message := fmt.Sprintf("%s for %s notification", NoConnectionInHandlerContextMessage, notificationType) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + }) +} + +func InvalidConnectionTypeInHandlerContext(notificationType string, conn interface{}, handlerCtx interface{}) string { + message := fmt.Sprintf("%s for %s notification - %T %#v", InvalidConnectionTypeInHandlerContextMessage, notificationType, conn, handlerCtx) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + "connType": fmt.Sprintf("%T", conn), + }) +} + +func SchedulingHandoffToCurrentEndpoint(connID uint64, seconds float64) string { + message := fmt.Sprintf("conn[%d] %s in %v seconds", connID, SchedulingHandoffToCurrentEndpointMessage, seconds) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "seconds": seconds, + }) +} + +func ManagerNotInitialized() string { + return appendJSONIfDebug(ManagerNotInitializedMessage, map[string]interface{}{}) +} + +func FailedToRegisterHandler(notificationType string, err error) string { + message := fmt.Sprintf("%s for %s: %v", FailedToRegisterHandlerMessage, notificationType, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + "error": err.Error(), + }) +} + +func ShutdownError() string { + return appendJSONIfDebug(ShutdownErrorMessage, map[string]interface{}{}) +} + +// Configuration validation error functions +func InvalidRelaxedTimeoutError() string { + return appendJSONIfDebug(InvalidRelaxedTimeoutErrorMessage, map[string]interface{}{}) +} + +func InvalidHandoffTimeoutError() string { + return appendJSONIfDebug(InvalidHandoffTimeoutErrorMessage, map[string]interface{}{}) +} + +func InvalidHandoffWorkersError() string { + return appendJSONIfDebug(InvalidHandoffWorkersErrorMessage, map[string]interface{}{}) +} + +func InvalidHandoffQueueSizeError() string { + return appendJSONIfDebug(InvalidHandoffQueueSizeErrorMessage, map[string]interface{}{}) +} + +func InvalidPostHandoffRelaxedDurationError() string { + return appendJSONIfDebug(InvalidPostHandoffRelaxedDurationErrorMessage, map[string]interface{}{}) +} + +func InvalidEndpointTypeError() string { + return appendJSONIfDebug(InvalidEndpointTypeErrorMessage, map[string]interface{}{}) +} + +func InvalidMaintNotificationsError() string { + return appendJSONIfDebug(InvalidMaintNotificationsErrorMessage, map[string]interface{}{}) +} + +func InvalidHandoffRetriesError() string { + return appendJSONIfDebug(InvalidHandoffRetriesErrorMessage, map[string]interface{}{}) +} + +func InvalidClientError() string { + return appendJSONIfDebug(InvalidClientErrorMessage, map[string]interface{}{}) +} + +func InvalidNotificationError() string { + return appendJSONIfDebug(InvalidNotificationErrorMessage, map[string]interface{}{}) +} + +func MaxHandoffRetriesReachedError() string { + return appendJSONIfDebug(MaxHandoffRetriesReachedErrorMessage, map[string]interface{}{}) +} + +func HandoffQueueFullError() string { + return appendJSONIfDebug(HandoffQueueFullErrorMessage, map[string]interface{}{}) +} + +func InvalidCircuitBreakerFailureThresholdError() string { + return appendJSONIfDebug(InvalidCircuitBreakerFailureThresholdErrorMessage, map[string]interface{}{}) +} + +func InvalidCircuitBreakerResetTimeoutError() string { + return appendJSONIfDebug(InvalidCircuitBreakerResetTimeoutErrorMessage, map[string]interface{}{}) +} + +func InvalidCircuitBreakerMaxRequestsError() string { + return appendJSONIfDebug(InvalidCircuitBreakerMaxRequestsErrorMessage, map[string]interface{}{}) +} + +// Configuration and debug functions +func DebugLoggingEnabled() string { + return appendJSONIfDebug(DebugLoggingEnabledMessage, map[string]interface{}{}) +} + +func ConfigDebug(config interface{}) string { + message := fmt.Sprintf("%s: %+v", ConfigDebugMessage, config) + return appendJSONIfDebug(message, map[string]interface{}{ + "config": fmt.Sprintf("%+v", config), + }) +} + +// Handoff worker functions +func WorkerExitingDueToShutdown() string { + return appendJSONIfDebug(WorkerExitingDueToShutdownMessage, map[string]interface{}{}) +} + +func WorkerExitingDueToShutdownWhileProcessing() string { + return appendJSONIfDebug(WorkerExitingDueToShutdownWhileProcessingMessage, map[string]interface{}{}) +} + +func WorkerPanicRecovered(panicValue interface{}) string { + message := fmt.Sprintf("%s: %v", WorkerPanicRecoveredMessage, panicValue) + return appendJSONIfDebug(message, map[string]interface{}{ + "panic": fmt.Sprintf("%v", panicValue), + }) +} + +func WorkerExitingDueToInactivityTimeout(timeout interface{}) string { + message := fmt.Sprintf("%s (%v)", WorkerExitingDueToInactivityTimeoutMessage, timeout) + return appendJSONIfDebug(message, map[string]interface{}{ + "timeout": fmt.Sprintf("%v", timeout), + }) +} + +func ApplyingRelaxedTimeoutDueToPostHandoff(connID uint64, timeout interface{}, until string) string { + message := fmt.Sprintf("conn[%d] %s (%v) until %s", connID, ApplyingRelaxedTimeoutDueToPostHandoffMessage, timeout, until) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + "timeout": fmt.Sprintf("%v", timeout), + "until": until, + }) +} + +// Example hooks functions +func MetricsHookProcessingNotification(notificationType string, connID uint64) string { + message := fmt.Sprintf("%s %s notification on conn[%d]", MetricsHookProcessingNotificationMessage, notificationType, connID) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + "connID": connID, + }) +} + +func MetricsHookRecordedError(notificationType string, connID uint64, err error) string { + message := fmt.Sprintf("%s for %s notification on conn[%d]: %v", MetricsHookRecordedErrorMessage, notificationType, connID, err) + return appendJSONIfDebug(message, map[string]interface{}{ + "notificationType": notificationType, + "connID": connID, + "error": err.Error(), + }) +} + +// Pool hook functions +func MarkedForHandoff(connID uint64) string { + message := fmt.Sprintf("conn[%d] %s", connID, MarkedForHandoffMessage) + return appendJSONIfDebug(message, map[string]interface{}{ + "connID": connID, + }) +} + +// Circuit breaker additional functions +func CircuitBreakerTransitioningToHalfOpen(endpoint string) string { + message := fmt.Sprintf("%s for %s", CircuitBreakerTransitioningToHalfOpenMessage, endpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "endpoint": endpoint, + }) +} + +func CircuitBreakerOpened(endpoint string, failures int64) string { + message := fmt.Sprintf("%s for endpoint %s after %d failures", CircuitBreakerOpenedMessage, endpoint, failures) + return appendJSONIfDebug(message, map[string]interface{}{ + "endpoint": endpoint, + "failures": failures, + }) +} + +func CircuitBreakerReopened(endpoint string) string { + message := fmt.Sprintf("%s for endpoint %s due to failure in half-open state", CircuitBreakerReopenedMessage, endpoint) + return appendJSONIfDebug(message, map[string]interface{}{ + "endpoint": endpoint, + }) +} + +func CircuitBreakerClosed(endpoint string, successes int64) string { + message := fmt.Sprintf("%s for endpoint %s after %d successful requests", CircuitBreakerClosedMessage, endpoint, successes) + return appendJSONIfDebug(message, map[string]interface{}{ + "endpoint": endpoint, + "successes": successes, + }) +} + +func CircuitBreakerCleanup(removed int, total int) string { + message := fmt.Sprintf("%s removed %d/%d entries", CircuitBreakerCleanupMessage, removed, total) + return appendJSONIfDebug(message, map[string]interface{}{ + "removed": removed, + "total": total, + }) +} + +// ExtractDataFromLogMessage extracts structured data from maintnotifications log messages +// Returns a map containing the parsed key-value pairs from the structured data section +// Example: "conn[123] handoff started to localhost:6379 {"connID":123,"endpoint":"localhost:6379"}" +// Returns: map[string]interface{}{"connID": 123, "endpoint": "localhost:6379"} +func ExtractDataFromLogMessage(logMessage string) map[string]interface{} { + result := make(map[string]interface{}) + + // Find the JSON data section at the end of the message + re := regexp.MustCompile(`(\{.*\})$`) + matches := re.FindStringSubmatch(logMessage) + if len(matches) < 2 { + return result + } + + jsonStr := matches[1] + if jsonStr == "" { + return result + } + + // Parse the JSON directly + var jsonResult map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &jsonResult); err == nil { + return jsonResult + } + + // If JSON parsing fails, return empty map + return result +} + +// Cluster notification functions +func InvalidSeqIDInSMigratingNotification(seqID interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidSeqIDInSMigratingNotificationMessage, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "seqID": fmt.Sprintf("%v", seqID), + }) +} + +func InvalidSeqIDInSMigratedNotification(seqID interface{}) string { + message := fmt.Sprintf("%s: %v", InvalidSeqIDInSMigratedNotificationMessage, seqID) + return appendJSONIfDebug(message, map[string]interface{}{ + "seqID": fmt.Sprintf("%v", seqID), + }) +} + +// TriggeringClusterStateReload logs when cluster state reload is triggered (deduplicated, once per seqID) +func TriggeringClusterStateReload(seqID int64, hostPort string, slotRanges []string) string { + message := fmt.Sprintf("%s seqID=%d host:port=%s slots=%v", TriggeringClusterStateReloadMessage, seqID, hostPort, slotRanges) + return appendJSONIfDebug(message, map[string]interface{}{ + "seqID": seqID, + "hostPort": hostPort, + "slotRanges": slotRanges, + }) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go b/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go new file mode 100644 index 00000000000..a3f23fffe5c --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go @@ -0,0 +1,298 @@ +package otel + +import ( + "context" + "crypto/rand" + "encoding/hex" + "sync" + "time" + + "github.com/redis/go-redis/v9/internal/pool" +) + +// generateUniqueID generates a short unique identifier for pool names. +func generateUniqueID() string { + b := make([]byte, 4) + if _, err := rand.Read(b); err != nil { + return "" + } + return hex.EncodeToString(b) +} + +// Cmder is a minimal interface for command information needed for metrics. +// This avoids circular dependencies with the main redis package. +type Cmder interface { + Name() string + FullName() string + Args() []interface{} + Err() error +} + +// Recorder is the interface for recording metrics. +type Recorder interface { + // RecordOperationDuration records the total operation duration (including all retries) + // dbIndex is the Redis database index (0-15) + RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) + + // RecordPipelineOperationDuration records the total pipeline/transaction duration. + // operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions. + // cmdCount is the number of commands in the pipeline. + // err is the error from the pipeline execution (can be nil). + // dbIndex is the Redis database index (0-15) + RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) + + // RecordConnectionCreateTime records the time it took to create a new connection + RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) + + // RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed + // delta: +1 for relaxed, -1 for unrelaxed + // poolName: name of the connection pool (e.g., "main", "pubsub") + // notificationType: the notification type that triggered the timeout relaxation (e.g., "MOVING") + RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) + + // RecordConnectionHandoff records when a connection is handed off to another node + // poolName: name of the connection pool (e.g., "main", "pubsub") + RecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string) + + // RecordError records client errors (ASK, MOVED, handshake failures, etc.) + // errorType: type of error (e.g., "ASK", "MOVED", "HANDSHAKE_FAILED") + // statusCode: Redis response status code if available (e.g., "MOVED", "ASK") + // isInternal: whether this is an internal error + // retryAttempts: number of retry attempts made + RecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) + + // RecordMaintenanceNotification records when a maintenance notification is received + // notificationType: the type of notification (e.g., "MOVING", "MIGRATING", etc.) + RecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string) + + // RecordConnectionWaitTime records the time spent waiting for a connection from the pool + RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn) + + // RecordConnectionClosed records when a connection is closed + // reason: reason for closing (e.g., "idle", "max_lifetime", "error", "pool_closed") + // err: the error that caused the close (nil for non-error closures) + RecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error) + + // RecordPubSubMessage records a Pub/Sub message + // direction: "sent" or "received" + // channel: channel name (may be hidden for cardinality reduction) + // sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE) + RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) + + // RecordStreamLag records the lag for stream consumer group processing + // lag: time difference between message creation and consumption + // streamName: name of the stream (may be hidden for cardinality reduction) + // consumerGroup: name of the consumer group + // consumerName: name of the consumer + RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) + + // RecordConnectionCount records a change in connection count (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) + + // RecordPendingRequests records a change in pending requests (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string) +} + +type PubSubPooler interface { + Stats() *pool.PubSubStats +} + +type PoolRegistrar interface { + // RegisterPool is called when a new client is created with its connection pools. + // poolName: identifier for the pool (e.g., "main_abc123") + // pool: the connection pool + RegisterPool(poolName string, pool pool.Pooler) + // UnregisterPool is called when a client is closed to remove its pool from the registry. + // pool: the connection pool to unregister + UnregisterPool(pool pool.Pooler) + // RegisterPubSubPool is called when a new client is created with a PubSub pool. + // poolName: identifier for the pool (e.g., "main_abc123_pubsub") + // pool: the PubSub connection pool + RegisterPubSubPool(poolName string, pool PubSubPooler) + // UnregisterPubSubPool is called when a PubSub client is closed to remove its pool. + // pool: the PubSub connection pool to unregister + UnregisterPubSubPool(pool PubSubPooler) +} + +var ( + // recorderMu protects globalRecorder and operation duration callbacks + recorderMu sync.RWMutex + + // Global recorder instance (initialized by extra/redisotel-native) + globalRecorder Recorder = noopRecorder{} + + // Callbacks for operation duration metrics + operationDurationCallback func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) + pipelineOperationDurationCallback func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) +) + +// GetOperationDurationCallback returns the callback for operation duration. +func GetOperationDurationCallback() func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) { + recorderMu.RLock() + cb := operationDurationCallback + recorderMu.RUnlock() + return cb +} + +// GetPipelineOperationDurationCallback returns the callback for pipeline operation duration. +func GetPipelineOperationDurationCallback() func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) { + recorderMu.RLock() + cb := pipelineOperationDurationCallback + recorderMu.RUnlock() + return cb +} + +// getRecorder returns the current global recorder under a read lock. +func getRecorder() Recorder { + recorderMu.RLock() + r := globalRecorder + recorderMu.RUnlock() + return r +} + +// SetGlobalRecorder sets the global recorder (called by Init() in extra/redisotel-native) +func SetGlobalRecorder(r Recorder) { + recorderMu.Lock() + if r == nil { + globalRecorder = noopRecorder{} + operationDurationCallback = nil + pipelineOperationDurationCallback = nil + recorderMu.Unlock() + // Unregister all pool metric callbacks atomically + pool.SetAllMetricCallbacks(nil) + return + } + globalRecorder = r + + // Register operation duration callbacks + // These capture r directly since we want them to use the specific recorder + // that was set at this point in time + operationDurationCallback = func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) { + getRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex) + } + pipelineOperationDurationCallback = func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) { + getRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex) + } + recorderMu.Unlock() + + // Register all pool metric callbacks atomically + // These use getRecorder() to safely access the current recorder + pool.SetAllMetricCallbacks(&pool.MetricCallbacks{ + ConnectionCreateTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) { + getRecorder().RecordConnectionCreateTime(ctx, duration, cn) + }, + ConnectionRelaxedTimeout: func(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) { + getRecorder().RecordConnectionRelaxedTimeout(ctx, delta, cn, poolName, notificationType) + }, + ConnectionHandoff: func(ctx context.Context, cn *pool.Conn, poolName string) { + getRecorder().RecordConnectionHandoff(ctx, cn, poolName) + }, + Error: func(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) { + getRecorder().RecordError(ctx, errorType, cn, statusCode, isInternal, retryAttempts) + }, + MaintenanceNotification: func(ctx context.Context, cn *pool.Conn, notificationType string) { + getRecorder().RecordMaintenanceNotification(ctx, cn, notificationType) + }, + ConnectionWaitTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) { + getRecorder().RecordConnectionWaitTime(ctx, duration, cn) + }, + ConnectionClosed: func(ctx context.Context, cn *pool.Conn, reason string, err error) { + getRecorder().RecordConnectionClosed(ctx, cn, reason, err) + }, + ConnectionCount: func(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) { + getRecorder().RecordConnectionCount(ctx, delta, cn, state, isPubSub) + }, + PendingRequests: func(ctx context.Context, delta int, cn *pool.Conn, poolName string) { + getRecorder().RecordPendingRequests(ctx, delta, cn, poolName) + }, + }) +} + +// RecordOperationDuration records the total operation duration. +// dbIndex is the Redis database index (0-15). +func RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) { + getRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex) +} + +// RecordPipelineOperationDuration records the total pipeline/transaction duration. +// This is called from redis.go after pipeline/transaction execution completes. +// operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions. +// err is the error from the pipeline execution (can be nil). +// dbIndex is the Redis database index (0-15). +func RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) { + getRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex) +} + +// RecordConnectionCreateTime records the time it took to create a new connection. +func RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) { + getRecorder().RecordConnectionCreateTime(ctx, duration, cn) +} + +// RecordPubSubMessage records a Pub/Sub message sent or received. +func RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) { + getRecorder().RecordPubSubMessage(ctx, cn, direction, channel, sharded) +} + +// RecordStreamLag records the lag between message creation and consumption in a stream. +func RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) { + getRecorder().RecordStreamLag(ctx, lag, cn, streamName, consumerGroup, consumerName) +} + +type noopRecorder struct{} + +func (noopRecorder) RecordOperationDuration(context.Context, time.Duration, Cmder, int, error, *pool.Conn, int) { +} +func (noopRecorder) RecordPipelineOperationDuration(context.Context, time.Duration, string, int, int, error, *pool.Conn, int) { +} +func (noopRecorder) RecordConnectionCreateTime(context.Context, time.Duration, *pool.Conn) {} +func (noopRecorder) RecordConnectionRelaxedTimeout(context.Context, int, *pool.Conn, string, string) { +} +func (noopRecorder) RecordConnectionHandoff(context.Context, *pool.Conn, string) {} +func (noopRecorder) RecordError(context.Context, string, *pool.Conn, string, bool, int) {} +func (noopRecorder) RecordMaintenanceNotification(context.Context, *pool.Conn, string) {} + +func (noopRecorder) RecordConnectionWaitTime(context.Context, time.Duration, *pool.Conn) {} +func (noopRecorder) RecordConnectionClosed(context.Context, *pool.Conn, string, error) {} + +func (noopRecorder) RecordPubSubMessage(context.Context, *pool.Conn, string, string, bool) {} + +func (noopRecorder) RecordStreamLag(context.Context, time.Duration, *pool.Conn, string, string, string) { +} +func (noopRecorder) RecordConnectionCount(context.Context, int, *pool.Conn, string, bool) {} +func (noopRecorder) RecordPendingRequests(context.Context, int, *pool.Conn, string) {} + +// RegisterPools registers connection pools with the global recorder. +func RegisterPools(connPool pool.Pooler, pubSubPool PubSubPooler, addr string) { + // Check if the global recorder implements PoolRegistrar + if registrar, ok := globalRecorder.(PoolRegistrar); ok { + // Generate a unique ID for this client's pools + uniqueID := generateUniqueID() + + if connPool != nil { + poolName := addr + "_" + uniqueID + registrar.RegisterPool(poolName, connPool) + } + if pubSubPool != nil { + poolName := addr + "_" + uniqueID + "_pubsub" + registrar.RegisterPubSubPool(poolName, pubSubPool) + } + } +} + +// UnregisterPools removes connection pools from the global recorder +func UnregisterPools(connPool pool.Pooler, pubSubPool PubSubPooler) { + // Check if the global recorder implements PoolRegistrar + if registrar, ok := globalRecorder.(PoolRegistrar); ok { + if connPool != nil { + registrar.UnregisterPool(connPool) + } + if pubSubPool != nil { + registrar.UnregisterPubSubPool(pubSubPool) + } + } +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go index c1087b401a6..fab54654aaf 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go @@ -1,70 +1,872 @@ +// Package pool implements the pool management package pool import ( "bufio" "context" + "errors" + "fmt" "net" + "sync" "sync/atomic" "time" + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" "github.com/redis/go-redis/v9/internal/proto" + uberatomic "go.uber.org/atomic" ) var noDeadline = time.Time{} +// Preallocated errors for hot paths to avoid allocations +var ( + errAlreadyMarkedForHandoff = errors.New("connection is already marked for handoff") + errNotMarkedForHandoff = errors.New("connection was not marked for handoff") + errHandoffStateChanged = errors.New("handoff state changed during marking") + errConnectionNotAvailable = errors.New("redis: connection not available") + errConnNotAvailableForWrite = errors.New("redis: connection not available for write operation") +) + +// getCachedTimeNs returns the current time in nanoseconds. +// This function previously used a global cache updated by a background goroutine, +// but that caused unnecessary CPU usage when the client was idle (ticker waking up +// the scheduler every 50ms). We now use time.Now() directly, which is fast enough +// on modern systems (vDSO on Linux) and only adds ~1-2% overhead in extreme +// high-concurrency benchmarks while eliminating idle CPU usage. +func getCachedTimeNs() int64 { + return time.Now().UnixNano() +} + +// GetCachedTimeNs returns the current time in nanoseconds. +// Exported for use by other packages that need fast time access. +func GetCachedTimeNs() int64 { + return getCachedTimeNs() +} + +// Global atomic counter for connection IDs +var connIDCounter uint64 + +// HandoffState represents the atomic state for connection handoffs +// This struct is stored atomically to prevent race conditions between +// checking handoff status and reading handoff parameters +type HandoffState struct { + ShouldHandoff bool // Whether connection should be handed off + Endpoint string // New endpoint for handoff + SeqID int64 // Sequence ID from MOVING notification +} + +// atomicNetConn is a wrapper to ensure consistent typing in atomic.Value +type atomicNetConn struct { + conn net.Conn +} + +// generateConnID generates a fast unique identifier for a connection with zero allocations +func generateConnID() uint64 { + return atomic.AddUint64(&connIDCounter, 1) +} + type Conn struct { - usedAt int64 // atomic - netConn net.Conn + // Connection identifier for unique tracking + id uint64 + + usedAt atomic.Int64 + lastPutAt atomic.Int64 + dialStartNs atomic.Int64 // Time when dial started (for connection create time metric) + + // Lock-free netConn access using atomic.Value + // Contains *atomicNetConn wrapper, accessed atomically for better performance + netConnAtomic atomic.Value // stores *atomicNetConn rd *proto.Reader bw *bufio.Writer wr *proto.Writer - Inited bool + // Lightweight mutex to protect reader operations during handoff and health checks + // Used during: + // - SetNetConn (write lock for resetting reader state) + // - HasBufferedData/PeekReplyTypeSafe (read lock for safe concurrent peek operations) + readerMu sync.RWMutex + + // State machine for connection state management + // Replaces: usable, Inited, used + // Provides thread-safe state transitions with FIFO waiting queue + // States: CREATED → INITIALIZING → IDLE ⇄ IN_USE + // ↓ + // UNUSABLE (handoff/reauth) + // ↓ + // IDLE/CLOSED + stateMachine *ConnStateMachine + + // Handoff metadata - managed separately from state machine + // These are atomic for lock-free access during handoff operations + handoffStateAtomic atomic.Value // stores *HandoffState + handoffRetriesAtomic atomic.Uint32 // retry counter + pooled bool + pubsub bool createdAt time.Time + expiresAt time.Time + poolName string // Name of the pool this connection belongs to (for metrics) + + // When a goroutine closes a connection, it usually knows the reason, so closeReason is not needed. + // closeReason is only used when an in-use connection is closed by another goroutine, + // to inform the goroutine using the connection why the connection was closed. + closeReason uberatomic.String + + // maintenanceNotifications upgrade support: relaxed timeouts during migrations/failovers + + // Using atomic operations for lock-free access to avoid mutex contention + relaxedReadTimeoutNs atomic.Int64 // time.Duration as nanoseconds + relaxedWriteTimeoutNs atomic.Int64 // time.Duration as nanoseconds + relaxedDeadlineNs atomic.Int64 // time.Time as nanoseconds since epoch + + // Counter to track multiple relaxed timeout setters if we have nested calls + // will be decremented when ClearRelaxedTimeout is called or deadline is reached + // if counter reaches 0, we clear the relaxed timeouts + relaxedCounter atomic.Int32 + + // Connection initialization function for reconnections + initConnFunc func(context.Context, *Conn) error onClose func() error } func NewConn(netConn net.Conn) *Conn { + return NewConnWithBufferSize(netConn, proto.DefaultBufferSize, proto.DefaultBufferSize) +} + +func NewConnWithBufferSize(netConn net.Conn, readBufSize, writeBufSize int) *Conn { + now := time.Now() cn := &Conn{ - netConn: netConn, - createdAt: time.Now(), + createdAt: now, + id: generateConnID(), // Generate unique ID for this connection + stateMachine: NewConnStateMachine(), + } + + // Use specified buffer sizes, or fall back to 32KiB defaults if 0 + if readBufSize > 0 { + cn.rd = proto.NewReaderSize(netConn, readBufSize) + } else { + cn.rd = proto.NewReader(netConn) // Uses 32KiB default } - cn.rd = proto.NewReader(netConn) - cn.bw = bufio.NewWriter(netConn) + + if writeBufSize > 0 { + cn.bw = bufio.NewWriterSize(netConn, writeBufSize) + } else { + cn.bw = bufio.NewWriterSize(netConn, proto.DefaultBufferSize) + } + + // Store netConn atomically for lock-free access using wrapper + cn.netConnAtomic.Store(&atomicNetConn{conn: netConn}) + cn.wr = proto.NewWriter(cn.bw) - cn.SetUsedAt(time.Now()) + cn.SetUsedAt(now) + // Initialize handoff state atomically + initialHandoffState := &HandoffState{ + ShouldHandoff: false, + Endpoint: "", + SeqID: 0, + } + cn.handoffStateAtomic.Store(initialHandoffState) return cn } func (cn *Conn) UsedAt() time.Time { - unix := atomic.LoadInt64(&cn.usedAt) - return time.Unix(unix, 0) + return time.Unix(0, cn.usedAt.Load()) } - func (cn *Conn) SetUsedAt(tm time.Time) { - atomic.StoreInt64(&cn.usedAt, tm.Unix()) + cn.usedAt.Store(tm.UnixNano()) +} + +func (cn *Conn) UsedAtNs() int64 { + return cn.usedAt.Load() +} +func (cn *Conn) SetUsedAtNs(ns int64) { + cn.usedAt.Store(ns) +} + +func (cn *Conn) LastPutAtNs() int64 { + return cn.lastPutAt.Load() +} +func (cn *Conn) SetLastPutAtNs(ns int64) { + cn.lastPutAt.Store(ns) +} + +// GetDialStartNs returns the time when the dial started (in nanoseconds since epoch). +// This is used to calculate the full connection creation time (TCP + handshake). +func (cn *Conn) GetDialStartNs() int64 { + return cn.dialStartNs.Load() +} + +// PoolName returns the name of the pool this connection belongs to. +// This is used for metrics to identify which pool a connection is from. +func (cn *Conn) PoolName() string { + return cn.poolName } +// SetPoolName sets the name of the pool this connection belongs to. +// This should be called when the connection is added to a pool. +func (cn *Conn) SetPoolName(name string) { + cn.poolName = name +} + +// Backward-compatible wrapper methods for state machine +// These maintain the existing API while using the new state machine internally + +// CompareAndSwapUsable atomically compares and swaps the usable flag (lock-free). +// +// This is used by background operations (handoff, re-auth) to acquire exclusive +// access to a connection. The operation sets usable to false, preventing the pool +// from returning the connection to clients. +// +// Returns true if the swap was successful (old value matched), false otherwise. +// +// Implementation note: This is a compatibility wrapper around the state machine. +// It checks if the current state is "usable" (IDLE or IN_USE) and transitions accordingly. +// Deprecated: Use GetStateMachine().TryTransition() directly for better state management. +func (cn *Conn) CompareAndSwapUsable(old, new bool) bool { + currentState := cn.stateMachine.GetState() + + // Check if current state matches the "old" usable value + currentUsable := (currentState == StateIdle || currentState == StateInUse) + if currentUsable != old { + return false + } + + // If we're trying to set to the same value, succeed immediately + if old == new { + return true + } + + // Transition based on new value + if new { + // Trying to make usable - transition from UNUSABLE to IDLE + // This should only work from UNUSABLE or INITIALIZING states + // Use predefined slice to avoid allocation + _, err := cn.stateMachine.TryTransition( + validFromInitializingOrUnusable, + StateIdle, + ) + return err == nil + } + // Trying to make unusable - transition from IDLE to UNUSABLE + // This is typically for acquiring the connection for background operations + // Use predefined slice to avoid allocation + _, err := cn.stateMachine.TryTransition( + validFromIdle, + StateUnusable, + ) + return err == nil +} + +// IsUsable returns true if the connection is safe to use for new commands (lock-free). +// +// A connection is "usable" when it's in a stable state and can be returned to clients. +// It becomes unusable during: +// - Handoff operations (network connection replacement) +// - Re-authentication (credential updates) +// - Other background operations that need exclusive access +// +// Note: CREATED state is considered usable because new connections need to pass OnGet() hook +// before initialization. The initialization happens after OnGet() in the client code. +func (cn *Conn) IsUsable() bool { + state := cn.stateMachine.GetState() + // CREATED, IDLE, and IN_USE states are considered usable + // CREATED: new connection, not yet initialized (will be initialized by client) + // IDLE: initialized and ready to be acquired + // IN_USE: usable but currently acquired by someone + return state == StateCreated || state == StateIdle || state == StateInUse +} + +// SetUsable sets the usable flag for the connection (lock-free). +// +// Deprecated: Use GetStateMachine().Transition() directly for better state management. +// This method is kept for backwards compatibility. +// +// This should be called to mark a connection as usable after initialization or +// to release it after a background operation completes. +// +// Prefer CompareAndSwapUsable() when acquiring exclusive access to avoid race conditions. +// Deprecated: Use GetStateMachine().Transition() directly for better state management. +func (cn *Conn) SetUsable(usable bool) { + if usable { + // Transition to IDLE state (ready to be acquired) + cn.stateMachine.Transition(StateIdle) + } else { + // Transition to UNUSABLE state (for background operations) + cn.stateMachine.Transition(StateUnusable) + } +} + +// IsInited returns true if the connection has been initialized. +// This is a backward-compatible wrapper around the state machine. +func (cn *Conn) IsInited() bool { + state := cn.stateMachine.GetState() + // Connection is initialized if it's in IDLE or any post-initialization state + return state != StateCreated && state != StateInitializing && state != StateClosed +} + +// Used - State machine based implementation + +// CompareAndSwapUsed atomically compares and swaps the used flag (lock-free). +// This method is kept for backwards compatibility. +// +// This is the preferred method for acquiring a connection from the pool, as it +// ensures that only one goroutine marks the connection as used. +// +// Implementation: Uses state machine transitions IDLE ⇄ IN_USE +// +// Returns true if the swap was successful (old value matched), false otherwise. +// Deprecated: Use GetStateMachine().TryTransition() directly for better state management. +func (cn *Conn) CompareAndSwapUsed(old, new bool) bool { + if old == new { + // No change needed + currentState := cn.stateMachine.GetState() + currentUsed := (currentState == StateInUse) + return currentUsed == old + } + + if !old && new { + // Acquiring: IDLE → IN_USE + // Use predefined slice to avoid allocation + _, err := cn.stateMachine.TryTransition(validFromCreatedOrIdle, StateInUse) + return err == nil + } else { + // Releasing: IN_USE → IDLE + // Use predefined slice to avoid allocation + _, err := cn.stateMachine.TryTransition(validFromInUse, StateIdle) + return err == nil + } +} + +// IsUsed returns true if the connection is currently in use (lock-free). +// +// Deprecated: Use GetStateMachine().GetState() == StateInUse directly for better clarity. +// This method is kept for backwards compatibility. +// +// A connection is "used" when it has been retrieved from the pool and is +// actively processing a command. Background operations (like re-auth) should +// wait until the connection is not used before executing commands. +func (cn *Conn) IsUsed() bool { + return cn.stateMachine.GetState() == StateInUse +} + +// SetUsed sets the used flag for the connection (lock-free). +// +// This should be called when returning a connection to the pool (set to false) +// or when a single-connection pool retrieves its connection (set to true). +// +// Prefer CompareAndSwapUsed() when acquiring from a multi-connection pool to +// avoid race conditions. +// Deprecated: Use GetStateMachine().Transition() directly for better state management. +func (cn *Conn) SetUsed(val bool) { + if val { + cn.stateMachine.Transition(StateInUse) + } else { + cn.stateMachine.Transition(StateIdle) + } +} + +// getNetConn returns the current network connection using atomic load (lock-free). +// This is the fast path for accessing netConn without mutex overhead. +func (cn *Conn) getNetConn() net.Conn { + if v := cn.netConnAtomic.Load(); v != nil { + if wrapper, ok := v.(*atomicNetConn); ok { + return wrapper.conn + } + } + return nil +} + +// setNetConn stores the network connection atomically (lock-free). +// This is used for the fast path of connection replacement. +func (cn *Conn) setNetConn(netConn net.Conn) { + cn.netConnAtomic.Store(&atomicNetConn{conn: netConn}) +} + +// Handoff state management - atomic access to handoff metadata + +// ShouldHandoff returns true if connection needs handoff (lock-free). +func (cn *Conn) ShouldHandoff() bool { + if v := cn.handoffStateAtomic.Load(); v != nil { + return v.(*HandoffState).ShouldHandoff + } + return false +} + +// GetHandoffEndpoint returns the new endpoint for handoff (lock-free). +func (cn *Conn) GetHandoffEndpoint() string { + if v := cn.handoffStateAtomic.Load(); v != nil { + return v.(*HandoffState).Endpoint + } + return "" +} + +// GetMovingSeqID returns the sequence ID from the MOVING notification (lock-free). +func (cn *Conn) GetMovingSeqID() int64 { + if v := cn.handoffStateAtomic.Load(); v != nil { + return v.(*HandoffState).SeqID + } + return 0 +} + +// GetHandoffInfo returns all handoff information atomically (lock-free). +// This method prevents race conditions by returning all handoff state in a single atomic operation. +// Returns (shouldHandoff, endpoint, seqID). +func (cn *Conn) GetHandoffInfo() (bool, string, int64) { + if v := cn.handoffStateAtomic.Load(); v != nil { + state := v.(*HandoffState) + return state.ShouldHandoff, state.Endpoint, state.SeqID + } + return false, "", 0 +} + +// HandoffRetries returns the current handoff retry count (lock-free). +func (cn *Conn) HandoffRetries() int { + return int(cn.handoffRetriesAtomic.Load()) +} + +// IncrementAndGetHandoffRetries atomically increments and returns handoff retries (lock-free). +func (cn *Conn) IncrementAndGetHandoffRetries(n int) int { + return int(cn.handoffRetriesAtomic.Add(uint32(n))) +} + +// IsPooled returns true if the connection is managed by a pool and will be pooled on Put. +func (cn *Conn) IsPooled() bool { + return cn.pooled +} + +// IsPubSub returns true if the connection is used for PubSub. +func (cn *Conn) IsPubSub() bool { + return cn.pubsub +} + +// SetRelaxedTimeout sets relaxed timeouts for this connection during maintenanceNotifications upgrades. +// These timeouts will be used for all subsequent commands until the deadline expires. +// Uses atomic operations for lock-free access. +// Note: Metrics should be recorded by the caller (notification handler) which has context about +// the notification type and pool name. +func (cn *Conn) SetRelaxedTimeout(readTimeout, writeTimeout time.Duration) { + cn.relaxedCounter.Add(1) + cn.relaxedReadTimeoutNs.Store(int64(readTimeout)) + cn.relaxedWriteTimeoutNs.Store(int64(writeTimeout)) +} + +// SetRelaxedTimeoutWithDeadline sets relaxed timeouts with an expiration deadline. +// After the deadline, timeouts automatically revert to normal values. +// Uses atomic operations for lock-free access. +func (cn *Conn) SetRelaxedTimeoutWithDeadline(readTimeout, writeTimeout time.Duration, deadline time.Time) { + cn.SetRelaxedTimeout(readTimeout, writeTimeout) + cn.relaxedDeadlineNs.Store(deadline.UnixNano()) +} + +// ClearRelaxedTimeout removes relaxed timeouts, returning to normal timeout behavior. +// Uses atomic operations for lock-free access. +func (cn *Conn) ClearRelaxedTimeout() { + // Atomically decrement counter and check if we should clear + newCount := cn.relaxedCounter.Add(-1) + deadlineNs := cn.relaxedDeadlineNs.Load() + if newCount <= 0 && (deadlineNs == 0 || time.Now().UnixNano() >= deadlineNs) { + // Use atomic load to get current value for CAS to avoid stale value race + current := cn.relaxedCounter.Load() + if current <= 0 && cn.relaxedCounter.CompareAndSwap(current, 0) { + cn.clearRelaxedTimeout() + } + } +} + +func (cn *Conn) clearRelaxedTimeout() { + cn.relaxedReadTimeoutNs.Store(0) + cn.relaxedWriteTimeoutNs.Store(0) + cn.relaxedDeadlineNs.Store(0) + cn.relaxedCounter.Store(0) + + // Note: Metrics for timeout unrelaxing are not recorded here because we don't have + // context about which notification type or pool triggered the relaxation. + // In practice, relaxed timeouts expire automatically via deadline, so explicit + // unrelaxing metrics are less critical than the initial relaxation metrics. +} + +// HasRelaxedTimeout returns true if relaxed timeouts are currently active on this connection. +// This checks both the timeout values and the deadline (if set). +// Uses atomic operations for lock-free access. +func (cn *Conn) HasRelaxedTimeout() bool { + // Fast path: no relaxed timeouts are set + if cn.relaxedCounter.Load() <= 0 { + return false + } + + readTimeoutNs := cn.relaxedReadTimeoutNs.Load() + writeTimeoutNs := cn.relaxedWriteTimeoutNs.Load() + + // If no relaxed timeouts are set, return false + if readTimeoutNs <= 0 && writeTimeoutNs <= 0 { + return false + } + + deadlineNs := cn.relaxedDeadlineNs.Load() + // If no deadline is set, relaxed timeouts are active + if deadlineNs == 0 { + return true + } + + // If deadline is set, check if it's still in the future + return time.Now().UnixNano() < deadlineNs +} + +// getEffectiveReadTimeout returns the timeout to use for read operations. +// If relaxed timeout is set and not expired, it takes precedence over the provided timeout. +// This method automatically clears expired relaxed timeouts using atomic operations. +func (cn *Conn) getEffectiveReadTimeout(normalTimeout time.Duration) time.Duration { + readTimeoutNs := cn.relaxedReadTimeoutNs.Load() + + // Fast path: no relaxed timeout set + if readTimeoutNs <= 0 { + return normalTimeout + } + + deadlineNs := cn.relaxedDeadlineNs.Load() + // If no deadline is set, use relaxed timeout + if deadlineNs == 0 { + return time.Duration(readTimeoutNs) + } + + // Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) + nowNs := getCachedTimeNs() + // Check if deadline has passed + if nowNs < deadlineNs { + // Deadline is in the future, use relaxed timeout + return time.Duration(readTimeoutNs) + } else { + // Deadline has passed, clear relaxed timeouts atomically and use normal timeout + newCount := cn.relaxedCounter.Add(-1) + if newCount <= 0 { + internal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID())) + cn.clearRelaxedTimeout() + } + return normalTimeout + } +} + +// getEffectiveWriteTimeout returns the timeout to use for write operations. +// If relaxed timeout is set and not expired, it takes precedence over the provided timeout. +// This method automatically clears expired relaxed timeouts using atomic operations. +func (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Duration { + writeTimeoutNs := cn.relaxedWriteTimeoutNs.Load() + + // Fast path: no relaxed timeout set + if writeTimeoutNs <= 0 { + return normalTimeout + } + + deadlineNs := cn.relaxedDeadlineNs.Load() + // If no deadline is set, use relaxed timeout + if deadlineNs == 0 { + return time.Duration(writeTimeoutNs) + } + + // Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) + nowNs := getCachedTimeNs() + // Check if deadline has passed + if nowNs < deadlineNs { + // Deadline is in the future, use relaxed timeout + return time.Duration(writeTimeoutNs) + } else { + // Deadline has passed, clear relaxed timeouts atomically and use normal timeout + newCount := cn.relaxedCounter.Add(-1) + if newCount <= 0 { + internal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID())) + cn.clearRelaxedTimeout() + } + return normalTimeout + } +} + +// SetOnClose installs fn as the callback invoked exactly once when this +// connection is closed (via Conn.Close). +// +// IMPORTANT: SetOnClose OVERWRITES any previously installed callback — it +// does not compose, chain, or deduplicate. A Conn has room for a single +// onClose hook by design, because its lifecycle is bounded (a Conn is +// created, optionally re-initialized on its own net.Conn, and then closed +// once) and the pool's OnRemove hooks handle any registry-level cleanup +// that must survive the net.Conn being swapped. +// +// This has a subtle implication for per-connection subscriptions such as +// the unsubscribe function returned by StreamingCredentialsProvider +// (e.g. EntraID token rotation): if SetOnClose is called twice on the +// same Conn with DIFFERENT unsubscribe closures — for example because +// initConn ran a second time and obtained a fresh Subscribe() — +// the previous unsubscribe is dropped and will NEVER run, leaking a +// subscription on the provider. Callers must therefore ensure either: +// +// - the provider's Subscribe is idempotent for the same listener (the +// streaming credentials Manager deduplicates listeners by connection +// id, so re-Subscribe returns an equivalent unsubscribe), OR +// - the previous callback has already been invoked before SetOnClose is +// called again. +// +// Design note: unlike the client-level onCloseHooks registry (see +// redis.baseClient), there is intentionally NO named-hook dedup or +// multi-callback support on Conn. This is a deliberate trade-off to keep +// the Conn object slim — a pool can hold thousands of Conn values and +// each one is a hot allocation, so paying for a sync.Mutex plus a +// map[string]func() error per connection to support a feature that would +// only be used by at most one subsystem today (streaming credentials) is +// not worth the per-connection memory and allocation cost. For a single +// Conn there is at most one meaningful close callback at any point in +// time, and a richer registry here would not even solve the "stale +// closure" hazard described above. func (cn *Conn) SetOnClose(fn func() error) { cn.onClose = fn } +// SetInitConnFunc sets the connection initialization function to be called on reconnections. +func (cn *Conn) SetInitConnFunc(fn func(context.Context, *Conn) error) { + cn.initConnFunc = fn +} + +// ExecuteInitConn runs the stored connection initialization function if available. +func (cn *Conn) ExecuteInitConn(ctx context.Context) error { + if cn.initConnFunc != nil { + return cn.initConnFunc(ctx, cn) + } + return fmt.Errorf("redis: no initConnFunc set for conn[%d]", cn.GetID()) +} + func (cn *Conn) SetNetConn(netConn net.Conn) { - cn.netConn = netConn + // Store the new connection atomically first (lock-free) + cn.setNetConn(netConn) + // Protect reader reset operations to avoid data races + // Use write lock since we're modifying the reader state + cn.readerMu.Lock() cn.rd.Reset(netConn) + cn.readerMu.Unlock() + cn.bw.Reset(netConn) } +// GetNetConn safely returns the current network connection using atomic load (lock-free). +// This method is used by the pool for health checks and provides better performance. +func (cn *Conn) GetNetConn() net.Conn { + return cn.getNetConn() +} + +// SetNetConnAndInitConn replaces the underlying connection and executes the initialization. +// This method ensures only one initialization can happen at a time by using atomic state transitions. +// If another goroutine is currently initializing, this will wait for it to complete. +func (cn *Conn) SetNetConnAndInitConn(ctx context.Context, netConn net.Conn) error { + // Wait for and transition to INITIALIZING state - this prevents concurrent initializations + // Valid from states: CREATED (first init), IDLE (reconnect), UNUSABLE (handoff/reauth) + // If another goroutine is initializing, we'll wait for it to finish + // if the context has a deadline, use that, otherwise use the connection read (relaxed) timeout + // which should be set during handoff. If it is not set, use a 5 second default + deadline, ok := ctx.Deadline() + if !ok { + deadline = time.Now().Add(cn.getEffectiveReadTimeout(5 * time.Second)) + } + waitCtx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + // Use predefined slice to avoid allocation + finalState, err := cn.stateMachine.AwaitAndTransition( + waitCtx, + validFromCreatedIdleOrUnusable, + StateInitializing, + ) + if err != nil { + return fmt.Errorf("cannot initialize connection from state %s: %w", finalState, err) + } + + // Replace the underlying connection + cn.SetNetConn(netConn) + + // Execute initialization + // NOTE: ExecuteInitConn (via baseClient.initConn) will transition to IDLE on success + // or CLOSED on failure. We don't need to do it here. + // NOTE: Initconn returns conn in IDLE state + initErr := cn.ExecuteInitConn(ctx) + if initErr != nil { + // ExecuteInitConn already transitioned to CLOSED, just return the error + return initErr + } + + // ExecuteInitConn already transitioned to IDLE + return nil +} + +// MarkForHandoff marks the connection for handoff due to MOVING notification. +// Returns an error if the connection is already marked for handoff. +// Note: This only sets metadata - the connection state is not changed until OnPut. +// This allows the current user to finish using the connection before handoff. +func (cn *Conn) MarkForHandoff(newEndpoint string, seqID int64) error { + // Check if already marked for handoff + if cn.ShouldHandoff() { + return errAlreadyMarkedForHandoff + } + + // Set handoff metadata atomically + cn.handoffStateAtomic.Store(&HandoffState{ + ShouldHandoff: true, + Endpoint: newEndpoint, + SeqID: seqID, + }) + return nil +} + +// MarkQueuedForHandoff marks the connection as queued for handoff processing. +// This makes the connection unusable until handoff completes. +// This is called from OnPut hook, where the connection is typically in IN_USE state. +// The pool will preserve the UNUSABLE state and not overwrite it with IDLE. +func (cn *Conn) MarkQueuedForHandoff() error { + // Get current handoff state + currentState := cn.handoffStateAtomic.Load() + if currentState == nil { + return errNotMarkedForHandoff + } + + state := currentState.(*HandoffState) + if !state.ShouldHandoff { + return errNotMarkedForHandoff + } + + // Create new state with ShouldHandoff=false but preserve endpoint and seqID + // This prevents the connection from being queued multiple times while still + // allowing the worker to access the handoff metadata + newState := &HandoffState{ + ShouldHandoff: false, + Endpoint: state.Endpoint, // Preserve endpoint for handoff processing + SeqID: state.SeqID, // Preserve seqID for handoff processing + } + + // Atomic compare-and-swap to update state + if !cn.handoffStateAtomic.CompareAndSwap(currentState, newState) { + // State changed between load and CAS - retry or return error + return errHandoffStateChanged + } + + // Transition to UNUSABLE from IN_USE (normal flow), IDLE (edge cases), or CREATED (tests/uninitialized) + // The connection is typically in IN_USE state when OnPut is called (normal Put flow) + // But in some edge cases or tests, it might be in IDLE or CREATED state + // The pool will detect this state change and preserve it (not overwrite with IDLE) + // Use predefined slice to avoid allocation + finalState, err := cn.stateMachine.TryTransition(validFromCreatedInUseOrIdle, StateUnusable) + if err != nil { + // Check if already in UNUSABLE state (race condition or retry) + // ShouldHandoff should be false now, but check just in case + if finalState == StateUnusable && !cn.ShouldHandoff() { + // Already unusable - this is fine, keep the new handoff state + return nil + } + // Restore the original state if transition fails for other reasons + cn.handoffStateAtomic.Store(currentState) + return fmt.Errorf("failed to mark connection as unusable: %w", err) + } + return nil +} + +// GetID returns the unique identifier for this connection. +func (cn *Conn) GetID() uint64 { + return cn.id +} + +// GetStateMachine returns the connection's state machine for advanced state management. +// This is primarily used by internal packages like maintnotifications for handoff processing. +func (cn *Conn) GetStateMachine() *ConnStateMachine { + return cn.stateMachine +} + +// TryAcquire attempts to acquire the connection for use. +// This is an optimized inline method for the hot path (Get operation). +// +// It tries to transition from IDLE -> IN_USE or CREATED -> CREATED. +// Returns true if the connection was successfully acquired, false otherwise. +// The CREATED->CREATED is done so we can keep the state correct for later +// initialization of the connection in initConn. +// +// Performance: This is faster than calling GetStateMachine() + TryTransitionFast() +// +// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's +// methods. This breaks encapsulation but is necessary for performance. +// The IDLE->IN_USE and CREATED->CREATED transitions don't need +// waiter notification, and benchmarks show 1-3% improvement. If the state machine ever +// needs to notify waiters on these transitions, update this to use TryTransitionFast(). +func (cn *Conn) TryAcquire() bool { + // The || operator short-circuits, so only 1 CAS in the common case + return cn.stateMachine.state.CompareAndSwap(uint32(StateIdle), uint32(StateInUse)) || + cn.stateMachine.state.CompareAndSwap(uint32(StateCreated), uint32(StateCreated)) +} + +// Release releases the connection back to the pool. +// This is an optimized inline method for the hot path (Put operation). +// +// It tries to transition from IN_USE -> IDLE. +// Returns true if the connection was successfully released, false otherwise. +// +// Performance: This is faster than calling GetStateMachine() + TryTransitionFast(). +// +// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's +// methods. This breaks encapsulation but is necessary for performance. +// If the state machine ever needs to notify waiters +// on this transition, update this to use TryTransitionFast(). +func (cn *Conn) Release() bool { + // Inline the hot path - single CAS operation + return cn.stateMachine.state.CompareAndSwap(uint32(StateInUse), uint32(StateIdle)) +} + +// ClearHandoffState clears the handoff state after successful handoff. +// Makes the connection usable again. +func (cn *Conn) ClearHandoffState() { + // Clear handoff metadata + cn.handoffStateAtomic.Store(&HandoffState{ + ShouldHandoff: false, + Endpoint: "", + SeqID: 0, + }) + + // Reset retry counter + cn.handoffRetriesAtomic.Store(0) + + // Mark connection as usable again + // Use state machine directly instead of deprecated SetUsable + // probably done by initConn + cn.stateMachine.Transition(StateIdle) +} + +// HasBufferedData safely checks if the connection has buffered data. +// This method is used to avoid data races when checking for push notifications. +func (cn *Conn) HasBufferedData() bool { + // Use read lock for concurrent access to reader state + cn.readerMu.RLock() + defer cn.readerMu.RUnlock() + return cn.rd.Buffered() > 0 +} + +// PeekReplyTypeSafe safely peeks at the reply type. +// This method is used to avoid data races when checking for push notifications. +func (cn *Conn) PeekReplyTypeSafe() (byte, error) { + // Use read lock for concurrent access to reader state + cn.readerMu.RLock() + defer cn.readerMu.RUnlock() + + if cn.rd.Buffered() <= 0 { + return 0, fmt.Errorf("redis: can't peek reply type, no data available") + } + return cn.rd.PeekReplyType() +} + func (cn *Conn) Write(b []byte) (int, error) { - return cn.netConn.Write(b) + // Lock-free netConn access for better performance + if netConn := cn.getNetConn(); netConn != nil { + return netConn.Write(b) + } + return 0, net.ErrClosed } func (cn *Conn) RemoteAddr() net.Addr { - if cn.netConn != nil { - return cn.netConn.RemoteAddr() + // Lock-free netConn access for better performance + if netConn := cn.getNetConn(); netConn != nil { + return netConn.RemoteAddr() } return nil } @@ -73,7 +875,16 @@ func (cn *Conn) WithReader( ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error, ) error { if timeout >= 0 { - if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil { + // Use relaxed timeout if set, otherwise use provided timeout + effectiveTimeout := cn.getEffectiveReadTimeout(timeout) + + // Get the connection directly from atomic storage + netConn := cn.getNetConn() + if netConn == nil { + return errConnectionNotAvailable + } + + if err := netConn.SetReadDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil { return err } } @@ -84,13 +895,25 @@ func (cn *Conn) WithWriter( ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error, ) error { if timeout >= 0 { - if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil { - return err + // Use relaxed timeout if set, otherwise use provided timeout + effectiveTimeout := cn.getEffectiveWriteTimeout(timeout) + + // Set write deadline on the connection + if netConn := cn.getNetConn(); netConn != nil { + if err := netConn.SetWriteDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil { + return err + } + } else { + // Connection is not available - return preallocated error + return errConnNotAvailableForWrite } } + // Reset the buffered writer if needed, should not happen if cn.bw.Buffered() > 0 { - cn.bw.Reset(cn.netConn) + if netConn := cn.getNetConn(); netConn != nil { + cn.bw.Reset(netConn) + } } if err := fn(cn.wr); err != nil { @@ -100,17 +923,49 @@ func (cn *Conn) WithWriter( return cn.bw.Flush() } +func (cn *Conn) IsClosed() bool { + return cn.stateMachine.GetState() == StateClosed +} + func (cn *Conn) Close() error { + if cn.IsClosed() { + return nil + } + // Transition to CLOSED state + cn.stateMachine.Transition(StateClosed) + if cn.onClose != nil { // ignore error _ = cn.onClose() + cn.onClose = nil + } + + // Lock-free netConn access for better performance + if netConn := cn.getNetConn(); netConn != nil { + return netConn.Close() + } + return nil +} + +// MaybeHasData tries to peek at the next byte in the socket without consuming it +// This is used to check if there are push notifications available +// Important: This will work on Linux, but not on Windows +func (cn *Conn) MaybeHasData() bool { + // Lock-free netConn access for better performance + if netConn := cn.getNetConn(); netConn != nil { + return maybeHasData(netConn) } - return cn.netConn.Close() + return false } +// deadline computes the effective deadline time based on context and timeout. +// It updates the usedAt timestamp to now. +// Uses cached time to avoid expensive syscall (max 50ms staleness is acceptable for deadline calculation). func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time { - tm := time.Now() - cn.SetUsedAt(tm) + // Use cached time for deadline calculation (called 2x per command: read + write) + nowNs := getCachedTimeNs() + cn.SetUsedAtNs(nowNs) + tm := time.Unix(0, nowNs) if timeout > 0 { tm = tm.Add(timeout) diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check.go index 83190d39482..9e83dd833e5 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check.go @@ -12,6 +12,9 @@ import ( var errUnexpectedRead = errors.New("unexpected read from socket") +// connCheck checks if the connection is still alive and if there is data in the socket +// it will try to peek at the next byte without consuming it since we may want to work with it +// later on (e.g. push notifications) func connCheck(conn net.Conn) error { // Reset previous timeout. _ = conn.SetDeadline(time.Time{}) @@ -29,7 +32,9 @@ func connCheck(conn net.Conn) error { if err := rawConn.Read(func(fd uintptr) bool { var buf [1]byte - n, err := syscall.Read(int(fd), buf[:]) + // Use MSG_PEEK to peek at data without consuming it + n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT) + switch { case n == 0 && err == nil: sysErr = io.EOF @@ -47,3 +52,8 @@ func connCheck(conn net.Conn) error { return sysErr } + +// maybeHasData checks if there is data in the socket without consuming it +func maybeHasData(conn net.Conn) bool { + return connCheck(conn) == errUnexpectedRead +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go index 295da1268e7..f971d94c472 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go @@ -2,8 +2,19 @@ package pool -import "net" +import ( + "errors" + "net" +) -func connCheck(conn net.Conn) error { +// errUnexpectedRead is placeholder error variable for non-unix build constraints +var errUnexpectedRead = errors.New("unexpected read from socket") + +func connCheck(_ net.Conn) error { return nil } + +// since we can't check for data on the socket, we just assume there is some +func maybeHasData(_ net.Conn) bool { + return true +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go new file mode 100644 index 00000000000..7dee4c49dc5 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go @@ -0,0 +1,336 @@ +package pool + +import ( + "container/list" + "context" + "errors" + "fmt" + "sync" + "sync/atomic" +) + +// ConnState represents the connection state in the state machine. +// States are designed to be lightweight and fast to check. +// +// State Transitions: +// +// CREATED → INITIALIZING → IDLE ⇄ IN_USE +// ↓ +// UNUSABLE (handoff/reauth) +// ↓ +// IDLE/CLOSED +type ConnState uint32 + +const ( + // StateCreated - Connection just created, not yet initialized + StateCreated ConnState = iota + + // StateInitializing - Connection initialization in progress + StateInitializing + + // StateIdle - Connection initialized and idle in pool, ready to be acquired + StateIdle + + // StateInUse - Connection actively processing a command (retrieved from pool) + StateInUse + + // StateUnusable - Connection temporarily unusable due to background operation + // (handoff, reauth, etc.). Cannot be acquired from pool. + StateUnusable + + // StateClosed - Connection closed + StateClosed +) + +// Predefined state slices to avoid allocations in hot paths +var ( + validFromInUse = []ConnState{StateInUse} + validFromCreatedOrIdle = []ConnState{StateCreated, StateIdle} + validFromCreatedInUseOrIdle = []ConnState{StateCreated, StateInUse, StateIdle} + // For AwaitAndTransition calls + validFromCreatedIdleOrUnusable = []ConnState{StateCreated, StateIdle, StateUnusable} + validFromIdle = []ConnState{StateIdle} + // For CompareAndSwapUsable + validFromInitializingOrUnusable = []ConnState{StateInitializing, StateUnusable} +) + +// Accessor functions for predefined slices to avoid allocations in external packages +// These return the same slice instance, so they're zero-allocation + +// ValidFromIdle returns a predefined slice containing only StateIdle. +// Use this to avoid allocations when calling AwaitAndTransition or TryTransition. +func ValidFromIdle() []ConnState { + return validFromIdle +} + +// ValidFromCreatedIdleOrUnusable returns a predefined slice for initialization transitions. +// Use this to avoid allocations when calling AwaitAndTransition or TryTransition. +func ValidFromCreatedIdleOrUnusable() []ConnState { + return validFromCreatedIdleOrUnusable +} + +// String returns a human-readable string representation of the state. +func (s ConnState) String() string { + switch s { + case StateCreated: + return "CREATED" + case StateInitializing: + return "INITIALIZING" + case StateIdle: + return "IDLE" + case StateInUse: + return "IN_USE" + case StateUnusable: + return "UNUSABLE" + case StateClosed: + return "CLOSED" + default: + return fmt.Sprintf("UNKNOWN(%d)", s) + } +} + +var ( + // ErrInvalidStateTransition is returned when a state transition is not allowed + ErrInvalidStateTransition = errors.New("invalid state transition") + + // ErrStateMachineClosed is returned when operating on a closed state machine + ErrStateMachineClosed = errors.New("state machine is closed") + + // ErrTimeout is returned when a state transition times out + ErrTimeout = errors.New("state transition timeout") +) + +// waiter represents a goroutine waiting for a state transition. +// Designed for minimal allocations and fast processing. +type waiter struct { + validStates map[ConnState]struct{} // States we're waiting for + targetState ConnState // State to transition to + done chan error // Signaled when transition completes or times out +} + +// ConnStateMachine manages connection state transitions with FIFO waiting queue. +// Optimized for: +// - Lock-free reads (hot path) +// - Minimal allocations +// - Fast state transitions +// - FIFO fairness for waiters +// Note: Handoff metadata (endpoint, seqID, retries) is managed separately in the Conn struct. +type ConnStateMachine struct { + // Current state - atomic for lock-free reads + state atomic.Uint32 + + // FIFO queue for waiters - only locked during waiter add/remove/notify + mu sync.Mutex + waiters *list.List // List of *waiter + waiterCount atomic.Int32 // Fast lock-free check for waiters (avoids mutex in hot path) +} + +// NewConnStateMachine creates a new connection state machine. +// Initial state is StateCreated. +func NewConnStateMachine() *ConnStateMachine { + sm := &ConnStateMachine{ + waiters: list.New(), + } + sm.state.Store(uint32(StateCreated)) + return sm +} + +// GetState returns the current state (lock-free read). +// This is the hot path - optimized for zero allocations and minimal overhead. +// Note: Zero allocations applies to state reads; converting the returned state to a string +// (via String()) may allocate if the state is unknown. +func (sm *ConnStateMachine) GetState() ConnState { + return ConnState(sm.state.Load()) +} + +// TryTransitionFast is an optimized version for the hot path (Get/Put operations). +// It only handles simple state transitions without waiter notification. +// This is safe because: +// 1. Get/Put don't need to wait for state changes +// 2. Background operations (handoff/reauth) use UNUSABLE state, which this won't match +// 3. If a background operation is in progress (state is UNUSABLE), this fails fast +// +// Returns true if transition succeeded, false otherwise. +// Use this for performance-critical paths where you don't need error details. +// +// Performance: Single CAS operation - as fast as the old atomic bool! +// For multiple from states, use: sm.TryTransitionFast(State1, Target) || sm.TryTransitionFast(State2, Target) +// The || operator short-circuits, so only 1 CAS is executed in the common case. +func (sm *ConnStateMachine) TryTransitionFast(fromState, targetState ConnState) bool { + return sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) +} + +// TryTransition attempts an immediate state transition without waiting. +// Returns the current state after the transition attempt and an error if the transition failed. +// The returned state is the CURRENT state (after the attempt), not the previous state. +// This is faster than AwaitAndTransition when you don't need to wait. +// Uses compare-and-swap to atomically transition, preventing concurrent transitions. +// This method does NOT wait - it fails immediately if the transition cannot be performed. +// +// Performance: Zero allocations on success path (hot path). +func (sm *ConnStateMachine) TryTransition(validFromStates []ConnState, targetState ConnState) (ConnState, error) { + // Try each valid from state with CAS + // This ensures only ONE goroutine can successfully transition at a time + for _, fromState := range validFromStates { + // Try to atomically swap from fromState to targetState + // If successful, we won the race and can proceed + if sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) { + // Success! We transitioned atomically + // Hot path optimization: only check for waiters if transition succeeded + // This avoids atomic load on every Get/Put when no waiters exist + if sm.waiterCount.Load() > 0 { + sm.notifyWaiters() + } + return targetState, nil + } + } + + // All CAS attempts failed - state is not valid for this transition + // Return the current state so caller can decide what to do + // Note: This error path allocates, but it's the exceptional case + currentState := sm.GetState() + return currentState, fmt.Errorf("%w: cannot transition from %s to %s (valid from: %v)", + ErrInvalidStateTransition, currentState, targetState, validFromStates) +} + +// Transition unconditionally transitions to the target state. +// Use with caution - prefer AwaitAndTransition or TryTransition for safety. +// This is useful for error paths or when you know the transition is valid. +func (sm *ConnStateMachine) Transition(targetState ConnState) { + sm.state.Store(uint32(targetState)) + sm.notifyWaiters() +} + +// AwaitAndTransition waits for the connection to reach one of the valid states, +// then atomically transitions to the target state. +// Returns the current state after the transition attempt and an error if the operation failed. +// The returned state is the CURRENT state (after the attempt), not the previous state. +// Returns error if timeout expires or context is cancelled. +// +// This method implements FIFO fairness - the first caller to wait gets priority +// when the state becomes available. +// +// Performance notes: +// - If already in a valid state, this is very fast (no allocation, no waiting) +// - If waiting is required, allocates one waiter struct and one channel +func (sm *ConnStateMachine) AwaitAndTransition( + ctx context.Context, + validFromStates []ConnState, + targetState ConnState, +) (ConnState, error) { + // Fast path: try immediate transition with CAS to prevent race conditions + // BUT: only if there are no waiters in the queue (to maintain FIFO ordering) + if sm.waiterCount.Load() == 0 { + for _, fromState := range validFromStates { + // Check if we're already in target state + if fromState == targetState && sm.GetState() == targetState { + return targetState, nil + } + + // Try to atomically swap from fromState to targetState + if sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) { + // Success! We transitioned atomically + sm.notifyWaiters() + return targetState, nil + } + } + } + + // Fast path failed - check if we should wait or fail + currentState := sm.GetState() + + // Check if closed + if currentState == StateClosed { + return currentState, ErrStateMachineClosed + } + + // Slow path: need to wait for state change + // Create waiter with valid states map for fast lookup + validStatesMap := make(map[ConnState]struct{}, len(validFromStates)) + for _, s := range validFromStates { + validStatesMap[s] = struct{}{} + } + + w := &waiter{ + validStates: validStatesMap, + targetState: targetState, + done: make(chan error, 1), // Buffered to avoid goroutine leak + } + + // Add to FIFO queue + sm.mu.Lock() + elem := sm.waiters.PushBack(w) + sm.waiterCount.Add(1) + sm.mu.Unlock() + + // Wait for state change or timeout + select { + case <-ctx.Done(): + // Timeout or cancellation - remove from queue + sm.mu.Lock() + sm.waiters.Remove(elem) + sm.waiterCount.Add(-1) + sm.mu.Unlock() + return sm.GetState(), ctx.Err() + case err := <-w.done: + // Transition completed (or failed) + // Note: waiterCount is decremented either in notifyWaiters (when the waiter is notified and removed) + // or here (on timeout/cancellation). + return sm.GetState(), err + } +} + +// notifyWaiters checks if any waiters can proceed and notifies them in FIFO order. +// This is called after every state transition. +func (sm *ConnStateMachine) notifyWaiters() { + // Fast path: check atomic counter without acquiring lock + // This eliminates mutex overhead in the common case (no waiters) + if sm.waiterCount.Load() == 0 { + return + } + + sm.mu.Lock() + defer sm.mu.Unlock() + + // Double-check after acquiring lock (waiters might have been processed) + if sm.waiters.Len() == 0 { + return + } + + // Track state locally so we only consider transitions made within this + // call, not concurrent transitions from woken goroutines. Re-reading the + // atomic would let a fast goroutine's Transition(StateIdle) leak into our + // view, causing us to wake multiple waiters at once and breaking FIFO + // execution ordering. + currentState := sm.GetState() + + for { + processed := false + + for elem := sm.waiters.Front(); elem != nil; elem = elem.Next() { + w := elem.Value.(*waiter) + + if _, valid := w.validStates[currentState]; valid { + sm.waiters.Remove(elem) + sm.waiterCount.Add(-1) + + if sm.state.CompareAndSwap(uint32(currentState), uint32(w.targetState)) { + w.done <- nil + currentState = w.targetState + processed = true + break + } else { + sm.waiters.PushFront(w) + sm.waiterCount.Add(1) + currentState = sm.GetState() + processed = true + break + } + } + } + + if !processed { + break + } + } +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/hooks.go b/vendor/github.com/redis/go-redis/v9/internal/pool/hooks.go new file mode 100644 index 00000000000..a26e1976d53 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/hooks.go @@ -0,0 +1,165 @@ +package pool + +import ( + "context" + "sync" +) + +// PoolHook defines the interface for connection lifecycle hooks. +type PoolHook interface { + // OnGet is called when a connection is retrieved from the pool. + // It can modify the connection or return an error to prevent its use. + // The accept flag can be used to prevent the connection from being used. + // On Accept = false the connection is rejected and returned to the pool. + // The error can be used to prevent the connection from being used and returned to the pool. + // On Errors, the connection is removed from the pool. + // It has isNewConn flag to indicate if this is a new connection (rather than idle from the pool) + // The flag can be used for gathering metrics on pool hit/miss ratio. + OnGet(ctx context.Context, conn *Conn, isNewConn bool) (accept bool, err error) + + // OnPut is called when a connection is returned to the pool. + // It returns whether the connection should be pooled and whether it should be removed. + OnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error) + + // OnRemove is called when a connection is removed from the pool. + // This happens when: + // - Connection fails health check + // - Connection exceeds max lifetime + // - Pool is being closed + // - Connection encounters an error + // Implementations should clean up any per-connection state. + // The reason parameter indicates why the connection was removed. + OnRemove(ctx context.Context, conn *Conn, reason error) +} + +// PoolHookManager manages multiple pool hooks. +type PoolHookManager struct { + hooks []PoolHook + hooksMu sync.RWMutex +} + +// NewPoolHookManager creates a new pool hook manager. +func NewPoolHookManager() *PoolHookManager { + return &PoolHookManager{ + hooks: make([]PoolHook, 0), + } +} + +// AddHook adds a pool hook to the manager. +// Hooks are called in the order they were added. +func (phm *PoolHookManager) AddHook(hook PoolHook) { + phm.hooksMu.Lock() + defer phm.hooksMu.Unlock() + phm.hooks = append(phm.hooks, hook) +} + +// RemoveHook removes a pool hook from the manager. +func (phm *PoolHookManager) RemoveHook(hook PoolHook) { + phm.hooksMu.Lock() + defer phm.hooksMu.Unlock() + + for i, h := range phm.hooks { + if h == hook { + // Remove hook by swapping with last element and truncating + phm.hooks[i] = phm.hooks[len(phm.hooks)-1] + phm.hooks = phm.hooks[:len(phm.hooks)-1] + break + } + } +} + +// ProcessOnGet calls all OnGet hooks in order. +// If any hook returns an error, processing stops and the error is returned. +func (phm *PoolHookManager) ProcessOnGet(ctx context.Context, conn *Conn, isNewConn bool) (acceptConn bool, err error) { + // Copy slice reference while holding lock (fast) + phm.hooksMu.RLock() + hooks := phm.hooks + phm.hooksMu.RUnlock() + + // Call hooks without holding lock (slow operations) + for _, hook := range hooks { + acceptConn, err := hook.OnGet(ctx, conn, isNewConn) + if err != nil { + return false, err + } + + if !acceptConn { + return false, nil + } + } + return true, nil +} + +// ProcessOnPut calls all OnPut hooks in order. +// The first hook that returns shouldRemove=true or shouldPool=false will stop processing. +func (phm *PoolHookManager) ProcessOnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error) { + // Copy slice reference while holding lock (fast) + phm.hooksMu.RLock() + hooks := phm.hooks + phm.hooksMu.RUnlock() + + shouldPool = true // Default to pooling the connection + + // Call hooks without holding lock (slow operations) + for _, hook := range hooks { + hookShouldPool, hookShouldRemove, hookErr := hook.OnPut(ctx, conn) + + if hookErr != nil { + return false, true, hookErr + } + + // If any hook says to remove or not pool, respect that decision + if hookShouldRemove { + return false, true, nil + } + + if !hookShouldPool { + shouldPool = false + } + } + + return shouldPool, false, nil +} + +// ProcessOnRemove calls all OnRemove hooks in order. +func (phm *PoolHookManager) ProcessOnRemove(ctx context.Context, conn *Conn, reason error) { + // Copy slice reference while holding lock (fast) + phm.hooksMu.RLock() + hooks := phm.hooks + phm.hooksMu.RUnlock() + + // Call hooks without holding lock (slow operations) + for _, hook := range hooks { + hook.OnRemove(ctx, conn, reason) + } +} + +// GetHookCount returns the number of registered hooks (for testing). +func (phm *PoolHookManager) GetHookCount() int { + phm.hooksMu.RLock() + defer phm.hooksMu.RUnlock() + return len(phm.hooks) +} + +// GetHooks returns a copy of all registered hooks. +func (phm *PoolHookManager) GetHooks() []PoolHook { + phm.hooksMu.RLock() + defer phm.hooksMu.RUnlock() + + hooks := make([]PoolHook, len(phm.hooks)) + copy(hooks, phm.hooks) + return hooks +} + +// Clone creates a copy of the hook manager with the same hooks. +// This is used for lock-free atomic updates of the hook manager. +func (phm *PoolHookManager) Clone() *PoolHookManager { + phm.hooksMu.RLock() + defer phm.hooksMu.RUnlock() + + newManager := &PoolHookManager{ + hooks: make([]PoolHook, len(phm.hooks)), + } + copy(newManager.hooks, phm.hooks) + return newManager +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go index e7d951e268e..d551fbb1768 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go @@ -3,12 +3,49 @@ package pool import ( "context" "errors" + "math/rand" "net" "sync" "sync/atomic" "time" "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/proto" +) + +// Connection close reason constants for metrics. +// These are used as the "reason" parameter in CloseConn() calls. +const ( + // CloseReasonStale indicates the connection was closed because it exceeded + // the idle timeout or max lifetime. + CloseReasonStale = "stale" + + // CloseReasonHookError indicates the connection was closed due to an error + // in a pool hook (OnGet or OnPut). + CloseReasonHookError = "hook_error" + + // CloseReasonAuthError indicates the connection was closed due to an + // authentication error during re-authentication. + CloseReasonAuthError = "auth_error" + + // CloseReasonTest is used in tests when closing connections. + CloseReasonTest = "test" + + // CloseReasonFailover indicates the connection was closed due to a failover event. + CloseReasonFailover = "failover" +) + +// Metric state constants for connection state tracking. +// These represent the logical state of a connection from a metrics perspective, +// not the internal state machine state (ConnState). +const ( + // MetricStateIdle indicates the connection is idle in the pool, + // ready to be acquired. + MetricStateIdle = "idle" + + // MetricStateUsed indicates the connection is currently being used + // by a client operation. + MetricStateUsed = "used" ) var ( @@ -21,14 +58,244 @@ var ( // ErrPoolTimeout timed out waiting to get a connection from the connection pool. ErrPoolTimeout = errors.New("redis: connection pool timeout") + + // ErrConnUnusableTimeout is returned when a connection is not usable and we timed out trying to mark it as unusable. + ErrConnUnusableTimeout = errors.New("redis: timed out trying to mark connection as unusable") + + // errHookRequestedRemoval is returned when a hook requests connection removal. + errHookRequestedRemoval = errors.New("hook requested removal") + + // errConnNotPooled is returned when trying to return a non-pooled connection to the pool. + errConnNotPooled = errors.New("connection not pooled") + // metricCallbackMu protects all global metric callback functions for thread-safe access. + metricCallbackMu sync.RWMutex + + // Global metric callbacks for connection state changes + metricConnectionStateChangeCallback func(ctx context.Context, cn *Conn, fromState, toState string) + + // Global metric callback for connection creation time + metricConnectionCreateTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn) + + // Global metric callback for connection relaxed timeout changes + // Parameters: ctx, delta (+1/-1), cn, poolName, notificationType + metricConnectionRelaxedTimeoutCallback func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string) + + // Global metric callback for connection handoff + // Parameters: ctx, cn, poolName + metricConnectionHandoffCallback func(ctx context.Context, cn *Conn, poolName string) + + // Global metric callback for error tracking + // Parameters: ctx, errorType, cn, statusCode, isInternal, retryAttempts + metricErrorCallback func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int) + + // Global metric callback for maintenance notifications + // Parameters: ctx, cn, notificationType + metricMaintenanceNotificationCallback func(ctx context.Context, cn *Conn, notificationType string) + + // Global metric callback for connection wait time + // Parameters: ctx, duration, cn + metricConnectionWaitTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn) + + // Global metric callback for connection timeouts + // Parameters: ctx, cn, timeoutType + metricConnectionTimeoutCallback func(ctx context.Context, cn *Conn, timeoutType string) + + // Global metric callback for connection closed + // Parameters: ctx, cn, reason, err + metricConnectionClosedCallback func(ctx context.Context, cn *Conn, reason string, err error) + + // Global metric callback for connection count changes (UpDownCounter) + // Parameters: ctx, delta (+1/-1), cn, state, isPubSub + metricConnectionCountCallback func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) + + // Global metric callback for pending requests changes (UpDownCounter) + // Parameters: ctx, delta (+1/-1), cn, poolName + // poolName is passed explicitly because we may not have a connection yet when request starts + metricPendingRequestsCallback func(ctx context.Context, delta int, cn *Conn, poolName string) + + // errPanicInDial is returned when a panic occurs in the dial function. + errPanicInQueuedNewConn = errors.New("panic in queuedNewConn") + + // popAttempts is the maximum number of attempts to find a usable connection + // when popping from the idle connection pool. This handles cases where connections + // are temporarily marked as unusable (e.g., during maintenanceNotifications upgrades or network issues). + // Value of 50 provides sufficient resilience without excessive overhead. + // This is capped by the idle connection count, so we won't loop excessively. + popAttempts = 50 + + // getAttempts is the maximum number of attempts to get a connection that passes + // hook validation (e.g., maintenanceNotifications upgrade hooks). This protects against race conditions + // where hooks might temporarily reject connections during cluster transitions. + // Value of 3 balances resilience with performance - most hook rejections resolve quickly. + getAttempts = 3 + + minTime = time.Unix(-2208988800, 0) // Jan 1, 1900 + maxTime = minTime.Add(1<<63 - 1) + noExpiration = maxTime ) -var timers = sync.Pool{ - New: func() interface{} { - t := time.NewTimer(time.Hour) - t.Stop() - return t - }, +// MetricCallbacks holds all metric callback functions. +// Use SetAllMetricCallbacks to register all callbacks atomically. +type MetricCallbacks struct { + // ConnectionCreateTime is called when a new connection is created + ConnectionCreateTime func(ctx context.Context, duration time.Duration, cn *Conn) + + // ConnectionRelaxedTimeout is called when connection timeout is relaxed/unrelaxed + // delta: +1 for relaxed, -1 for unrelaxed + ConnectionRelaxedTimeout func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string) + + // ConnectionHandoff is called when a connection is handed off to another node + ConnectionHandoff func(ctx context.Context, cn *Conn, poolName string) + + // Error is called when an error occurs + Error func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int) + + // MaintenanceNotification is called when a maintenance notification is received + MaintenanceNotification func(ctx context.Context, cn *Conn, notificationType string) + + // ConnectionWaitTime is called to record time spent waiting for a connection + ConnectionWaitTime func(ctx context.Context, duration time.Duration, cn *Conn) + + // ConnectionClosed is called when a connection is closed + ConnectionClosed func(ctx context.Context, cn *Conn, reason string, err error) + + // ConnectionCount is called when connection count changes (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + ConnectionCount func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) + + // PendingRequests is called when pending requests count changes (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + PendingRequests func(ctx context.Context, delta int, cn *Conn, poolName string) +} + +// SetAllMetricCallbacks sets all metric callbacks atomically. +// Pass nil to clear all callbacks (disable metrics). +// This ensures all callbacks are set together under a single lock, +// preventing inconsistent state during registration. +// +// Note on thread safety: After returning, there is a small window where +// concurrent getMetric* calls may return the old callback value. This is +// acceptable for metrics - at most one event may go to the old recorder +// or be missed during the transition. The callbacks themselves are immutable +// function pointers, so calling an "old" callback is safe. +func SetAllMetricCallbacks(callbacks *MetricCallbacks) { + metricCallbackMu.Lock() + defer metricCallbackMu.Unlock() + + if callbacks == nil { + metricConnectionCreateTimeCallback = nil + metricConnectionRelaxedTimeoutCallback = nil + metricConnectionHandoffCallback = nil + metricErrorCallback = nil + metricMaintenanceNotificationCallback = nil + metricConnectionWaitTimeCallback = nil + metricConnectionClosedCallback = nil + metricConnectionCountCallback = nil + metricPendingRequestsCallback = nil + return + } + + metricConnectionCreateTimeCallback = callbacks.ConnectionCreateTime + metricConnectionRelaxedTimeoutCallback = callbacks.ConnectionRelaxedTimeout + metricConnectionHandoffCallback = callbacks.ConnectionHandoff + metricErrorCallback = callbacks.Error + metricMaintenanceNotificationCallback = callbacks.MaintenanceNotification + metricConnectionWaitTimeCallback = callbacks.ConnectionWaitTime + metricConnectionClosedCallback = callbacks.ConnectionClosed + metricConnectionCountCallback = callbacks.ConnectionCount + metricPendingRequestsCallback = callbacks.PendingRequests +} + +// getMetricConnectionStateChangeCallback returns the metric callback for connection state changes. +func getMetricConnectionStateChangeCallback() func(ctx context.Context, cn *Conn, fromState, toState string) { + metricCallbackMu.RLock() + cb := metricConnectionStateChangeCallback + metricCallbackMu.RUnlock() + return cb +} + +// GetMetricConnectionCreateTimeCallback returns the metric callback for connection creation time. +func GetMetricConnectionCreateTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) { + metricCallbackMu.RLock() + cb := metricConnectionCreateTimeCallback + metricCallbackMu.RUnlock() + return cb +} + +// GetMetricConnectionRelaxedTimeoutCallback returns the metric callback for connection relaxed timeout changes. +// This is used by maintnotifications to record relaxed timeout metrics. +func GetMetricConnectionRelaxedTimeoutCallback() func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string) { + metricCallbackMu.RLock() + cb := metricConnectionRelaxedTimeoutCallback + metricCallbackMu.RUnlock() + return cb +} + +// GetMetricConnectionHandoffCallback returns the metric callback for connection handoffs. +// This is used by maintnotifications to record handoff metrics. +func GetMetricConnectionHandoffCallback() func(ctx context.Context, cn *Conn, poolName string) { + metricCallbackMu.RLock() + cb := metricConnectionHandoffCallback + metricCallbackMu.RUnlock() + return cb +} + +// GetMetricErrorCallback returns the metric callback for error tracking. +// This is used by cluster and client code to record error metrics. +func GetMetricErrorCallback() func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int) { + metricCallbackMu.RLock() + cb := metricErrorCallback + metricCallbackMu.RUnlock() + return cb +} + +// GetMetricMaintenanceNotificationCallback returns the metric callback for maintenance notifications. +// This is used by maintnotifications to record notification metrics. +func GetMetricMaintenanceNotificationCallback() func(ctx context.Context, cn *Conn, notificationType string) { + metricCallbackMu.RLock() + cb := metricMaintenanceNotificationCallback + metricCallbackMu.RUnlock() + return cb +} + +func getMetricConnectionWaitTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) { + metricCallbackMu.RLock() + cb := metricConnectionWaitTimeCallback + metricCallbackMu.RUnlock() + return cb +} + +func getMetricConnectionTimeoutCallback() func(ctx context.Context, cn *Conn, timeoutType string) { + metricCallbackMu.RLock() + cb := metricConnectionTimeoutCallback + metricCallbackMu.RUnlock() + return cb +} + +func getMetricConnectionClosedCallback() func(ctx context.Context, cn *Conn, reason string, err error) { + metricCallbackMu.RLock() + cb := metricConnectionClosedCallback + metricCallbackMu.RUnlock() + return cb +} + +// getMetricConnectionCountCallback returns the metric callback for connection count changes (UpDownCounter). +func getMetricConnectionCountCallback() func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) { + metricCallbackMu.RLock() + cb := metricConnectionCountCallback + metricCallbackMu.RUnlock() + return cb +} + +// getMetricPendingRequestsCallback returns the metric callback for pending requests changes (UpDownCounter). +func getMetricPendingRequestsCallback() func(ctx context.Context, delta int, cn *Conn, poolName string) { + metricCallbackMu.RLock() + cb := metricPendingRequestsCallback + metricCallbackMu.RUnlock() + return cb } // Stats contains pool state information and accumulated stats. @@ -37,16 +304,20 @@ type Stats struct { Misses uint32 // number of times free connection was NOT found in the pool Timeouts uint32 // number of times a wait timeout occurred WaitCount uint32 // number of times a connection was waited + Unusable uint32 // number of times a connection was found to be unusable WaitDurationNs int64 // total time spent for waiting a connection in nanoseconds - TotalConns uint32 // number of total connections in the pool - IdleConns uint32 // number of idle connections in the pool - StaleConns uint32 // number of stale connections removed from the pool + TotalConns uint32 // number of total connections in the pool + IdleConns uint32 // number of idle connections in the pool + StaleConns uint32 // number of stale connections removed from the pool + PendingRequests uint32 // number of pending requests waiting for a connection + + PubSubStats PubSubStats } type Pooler interface { NewConn(context.Context) (*Conn, error) - CloseConn(*Conn) error + CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error Get(context.Context) (*Conn, error) Put(context.Context, *Conn) @@ -56,21 +327,55 @@ type Pooler interface { IdleLen() int Stats() *Stats + // Size returns the maximum pool size (capacity). + // This is used by the streaming credentials manager to size the re-auth worker pool. + Size() int + + AddPoolHook(hook PoolHook) + RemovePoolHook(hook PoolHook) + + // RemoveWithoutTurn removes a connection from the pool without freeing a turn. + // This should be used when removing a connection from a context that didn't acquire + // a turn via Get() (e.g., background workers, cleanup tasks). + // For normal removal after Get(), use Remove() instead. + RemoveWithoutTurn(context.Context, *Conn, error) + Close() error } type Options struct { - Dialer func(context.Context) (net.Conn, error) - - PoolFIFO bool - PoolSize int - DialTimeout time.Duration - PoolTimeout time.Duration - MinIdleConns int - MaxIdleConns int - MaxActiveConns int - ConnMaxIdleTime time.Duration - ConnMaxLifetime time.Duration + Dialer func(context.Context) (net.Conn, error) + ReadBufferSize int + WriteBufferSize int + + PoolFIFO bool + PoolSize int32 + MaxConcurrentDials int + DialTimeout time.Duration + PoolTimeout time.Duration + MinIdleConns int32 + MaxIdleConns int32 + MaxActiveConns int32 + ConnMaxIdleTime time.Duration + ConnMaxLifetime time.Duration + ConnMaxLifetimeJitter time.Duration + PushNotificationsEnabled bool + + // DialerRetries is the maximum number of retry attempts when dialing fails. + // Default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // Default: 100ms + DialerRetryTimeout time.Duration + + // DialerRetryBackoff controls the delay between dial retry attempts. + // If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms). + DialerRetryBackoff func(attempt int) time.Duration + + // Name is a unique identifier for this pool, used in metrics. + // Format: addr_uniqueID (e.g., "localhost:6379_a1b2c3d4") + Name string } type lastDialErrorWrap struct { @@ -83,75 +388,161 @@ type ConnPool struct { dialErrorsNum uint32 // atomic lastDialError atomic.Value - queue chan struct{} + dialsInProgress chan struct{} + dialsQueue *wantConnQueue + // Fast semaphore for connection limiting with eventual fairness + // Uses fast path optimization to avoid timer allocation when tokens are available + semaphore *internal.FastSemaphore connsMu sync.Mutex - conns []*Conn + conns map[uint64]*Conn idleConns []*Conn - poolSize int - idleConnsLen int + poolSize atomic.Int32 + idleConnsLen atomic.Int32 + idleCheckInProgress atomic.Bool + idleCheckNeeded atomic.Bool stats Stats waitDurationNs atomic.Int64 _closed uint32 // atomic + + // Pool hooks manager for flexible connection processing + // Using atomic.Pointer for lock-free reads in hot paths (Get/Put) + hookManager atomic.Pointer[PoolHookManager] } var _ Pooler = (*ConnPool)(nil) func NewConnPool(opt *Options) *ConnPool { p := &ConnPool{ - cfg: opt, - - queue: make(chan struct{}, opt.PoolSize), - conns: make([]*Conn, 0, opt.PoolSize), - idleConns: make([]*Conn, 0, opt.PoolSize), + cfg: opt, + semaphore: internal.NewFastSemaphore(opt.PoolSize), + conns: make(map[uint64]*Conn), + dialsInProgress: make(chan struct{}, opt.MaxConcurrentDials), + dialsQueue: newWantConnQueue(), + idleConns: make([]*Conn, 0, opt.PoolSize), } - p.connsMu.Lock() - p.checkMinIdleConns() - p.connsMu.Unlock() + // Only create MinIdleConns if explicitly requested (> 0) + // This avoids creating connections during pool initialization for tests + if opt.MinIdleConns > 0 { + p.connsMu.Lock() + p.checkMinIdleConns() + p.connsMu.Unlock() + } return p } +// initializeHooks sets up the pool hooks system. +func (p *ConnPool) initializeHooks() { + manager := NewPoolHookManager() + p.hookManager.Store(manager) +} + +// AddPoolHook adds a pool hook to the pool. +func (p *ConnPool) AddPoolHook(hook PoolHook) { + // Lock-free read of current manager + manager := p.hookManager.Load() + if manager == nil { + p.initializeHooks() + manager = p.hookManager.Load() + } + + // Create new manager with added hook + newManager := manager.Clone() + newManager.AddHook(hook) + + // Atomically swap to new manager + p.hookManager.Store(newManager) +} + +// RemovePoolHook removes a pool hook from the pool. +func (p *ConnPool) RemovePoolHook(hook PoolHook) { + manager := p.hookManager.Load() + if manager != nil { + // Create new manager with removed hook + newManager := manager.Clone() + newManager.RemoveHook(hook) + + // Atomically swap to new manager + p.hookManager.Store(newManager) + } +} + func (p *ConnPool) checkMinIdleConns() { + // If a check is already in progress, mark that we need another check and return + if !p.idleCheckInProgress.CompareAndSwap(false, true) { + p.idleCheckNeeded.Store(true) + return + } + if p.cfg.MinIdleConns == 0 { + p.idleCheckInProgress.Store(false) return } - for p.poolSize < p.cfg.PoolSize && p.idleConnsLen < p.cfg.MinIdleConns { - select { - case p.queue <- struct{}{}: - p.poolSize++ - p.idleConnsLen++ + // Keep checking until no more checks are needed + // This handles the case where multiple Remove() calls happen concurrently + for { + // Clear the "check needed" flag before we start + p.idleCheckNeeded.Store(false) + + // Only create idle connections if we haven't reached the total pool size limit + // MinIdleConns should be a subset of PoolSize, not additional connections + for p.poolSize.Load() < p.cfg.PoolSize && p.idleConnsLen.Load() < p.cfg.MinIdleConns { + // Try to acquire a semaphore token + if !p.semaphore.TryAcquire() { + // Semaphore is full, can't create more connections right now + // Break out of inner loop to check if we need to retry + break + } + + p.poolSize.Add(1) + p.idleConnsLen.Add(1) go func() { + defer func() { + if err := recover(); err != nil { + p.poolSize.Add(-1) + p.idleConnsLen.Add(-1) + + p.freeTurn() + internal.Logger.Printf(context.Background(), "addIdleConn panic: %+v", err) + } + }() + err := p.addIdleConn() if err != nil && err != ErrClosed { - p.connsMu.Lock() - p.poolSize-- - p.idleConnsLen-- - p.connsMu.Unlock() + p.poolSize.Add(-1) + p.idleConnsLen.Add(-1) } - p.freeTurn() }() - default: + } + + // If no one requested another check while we were working, we're done + if !p.idleCheckNeeded.Load() { + p.idleCheckInProgress.Store(false) return } + + // Otherwise, loop again to handle the new requests } } func (p *ConnPool) addIdleConn() error { - ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) - defer cancel() - - cn, err := p.dialConn(ctx, true) + // Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + cn, err := p.dialConn(context.Background(), true) if err != nil { return err } + // NOTE: Connection is in CREATED state and will be initialized by redis.go:initConn() + // when first acquired from the pool. Do NOT transition to IDLE here - that happens + // after initialization completes. + p.connsMu.Lock() defer p.connsMu.Unlock() @@ -161,11 +552,21 @@ func (p *ConnPool) addIdleConn() error { return ErrClosed } - p.conns = append(p.conns, cn) + p.conns[cn.GetID()] = cn p.idleConns = append(p.idleConns, cn) + + // Record connection count increment (new idle connection from min-idle prewarm) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), 1, cn, "idle", false) + } + return nil } +// NewConn creates a new connection and returns it to the user. +// This will still obey MaxActiveConns but will not include it in the pool and won't increase the pool size. +// +// NOTE: If you directly get a connection from the pool, it won't be pooled and won't support maintnotifications upgrades. func (p *ConnPool) NewConn(ctx context.Context) (*Conn, error) { return p.newConn(ctx, false) } @@ -175,36 +576,64 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) { return nil, ErrClosed } - p.connsMu.Lock() - if p.cfg.MaxActiveConns > 0 && p.poolSize >= p.cfg.MaxActiveConns { - p.connsMu.Unlock() + if p.cfg.MaxActiveConns > 0 && p.poolSize.Load() >= p.cfg.MaxActiveConns { return nil, ErrPoolExhausted } - p.connsMu.Unlock() + // Protect against nil context due to race condition in queuedNewConn + // where the context can be set to nil after timeout/cancellation + if ctx == nil { + ctx = context.Background() + } + + // Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + // We still propagate ctx so callers can cancel explicitly. cn, err := p.dialConn(ctx, pooled) if err != nil { return nil, err } - p.connsMu.Lock() - defer p.connsMu.Unlock() + // NOTE: Connection is in CREATED state and will be initialized by redis.go:initConn() + // when first used. Do NOT transition to IDLE here - that happens after initialization completes. + // The state machine flow is: CREATED → INITIALIZING (in initConn) → IDLE (after init success) - if p.cfg.MaxActiveConns > 0 && p.poolSize >= p.cfg.MaxActiveConns { + if p.cfg.MaxActiveConns > 0 && p.poolSize.Load() > p.cfg.MaxActiveConns { _ = cn.Close() return nil, ErrPoolExhausted } - p.conns = append(p.conns, cn) + p.connsMu.Lock() + defer p.connsMu.Unlock() + if p.closed() { + _ = cn.Close() + return nil, ErrClosed + } + // Check if pool was closed while we were waiting for the lock + if p.conns == nil { + p.conns = make(map[uint64]*Conn) + } + p.conns[cn.GetID()] = cn + if pooled { // If pool is full remove the cn on next Put. - if p.poolSize >= p.cfg.PoolSize { + currentPoolSize := p.poolSize.Load() + if currentPoolSize >= p.cfg.PoolSize { cn.pooled = false } else { - p.poolSize++ + p.poolSize.Add(1) } } + // All new connections start as "used" metrically. For the miss path in getConn, + // this is the final state. For putIdleConn (undelivered conn), a used→idle + // transition is emitted when it's added to idleConns. + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, "", MetricStateUsed) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, 1, cn, "used", false) + } + return cn, nil } @@ -217,18 +646,114 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) { return nil, p.getLastDialError() } - netConn, err := p.cfg.Dialer(ctx) - if err != nil { - p.setLastDialError(err) - if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.cfg.PoolSize) { - go p.tryDial() + // Record dial start time for connection creation metric + // This will be used after handshake completes in redis.go _getConn() + // Only call time.Now() if callback is registered to avoid overhead + var dialStartNs int64 + if GetMetricConnectionCreateTimeCallback() != nil { + dialStartNs = time.Now().UnixNano() + } + + // Retry dialing with backoff + // Dial timeout is applied per attempt (so retries/backoff don't eat into the next + // attempt's dial budget), while still honoring caller cancellation via ctx. + maxRetries := p.cfg.DialerRetries + if maxRetries <= 0 { + maxRetries = 5 // Default value + } + + var lastErr error + shouldLoop := true + // when the timeout is reached, we should stop retrying + // but keep the lastErr to return to the caller + // instead of a generic context deadline exceeded error + attempt := 0 + for attempt = 0; (attempt < maxRetries) && shouldLoop; attempt++ { + attemptCtx := ctx + var cancel context.CancelFunc + if p.cfg.DialTimeout > 0 { + // Apply DialTimeout per attempt, but never extend an existing earlier deadline. + if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > p.cfg.DialTimeout { + attemptCtx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout) + } } - return nil, err + + netConn, err := p.cfg.Dialer(attemptCtx) + if cancel != nil { + cancel() + } + if err != nil { + lastErr = err + // Add backoff delay for retry attempts + // (not for the first attempt, do at least one) + // Do not sleep after the last attempt. + if attempt+1 < maxRetries { + backoffDuration := p.dialRetryBackoff(attempt) + select { + case <-ctx.Done(): + shouldLoop = false + case <-time.After(backoffDuration): + // Continue with retry + } + } + continue + } + + cn := NewConnWithBufferSize(netConn, p.cfg.ReadBufferSize, p.cfg.WriteBufferSize) + cn.pooled = pooled + // Store dial start time only if we recorded it + if dialStartNs > 0 { + cn.dialStartNs.Store(dialStartNs) + } + cn.expiresAt = p.calcConnExpiresAt() + // Set pool name for metrics + cn.SetPoolName(p.cfg.Name) + + return cn, nil } - cn := NewConn(netConn) - cn.pooled = pooled - return cn, nil + internal.Logger.Printf(ctx, "redis: connection pool: failed to dial after %d attempts: %v", attempt, lastErr) + // All retries failed - handle error tracking + p.setLastDialError(lastErr) + if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.cfg.PoolSize) { + go p.tryDial() + } + return nil, lastErr +} + +func (p *ConnPool) dialRetryBackoff(attempt int) time.Duration { + if p.cfg.DialerRetryBackoff != nil { + d := p.cfg.DialerRetryBackoff(attempt) + if d < 0 { + return 0 + } + return d + } + + base := p.cfg.DialerRetryTimeout + if base <= 0 { + base = 100 * time.Millisecond + } + return base +} + +// calcConnExpiresAt calculates the expiration time for a connection. +// It applies random jitter to prevent all connections from expiring simultaneously, +// avoiding the "thundering herd" problem where all connections expire at once. +// Returns noExpiration if ConnMaxLifetime is not set. +func (p *ConnPool) calcConnExpiresAt() time.Time { + if p.cfg.ConnMaxLifetime <= 0 { + return noExpiration + } + + if p.cfg.ConnMaxLifetimeJitter <= 0 { + return time.Now().Add(p.cfg.ConnMaxLifetime) + } + + jitter := p.cfg.ConnMaxLifetimeJitter + jitterRange := jitter.Nanoseconds() * 2 + jitterNs := rand.Int63n(jitterRange) - jitter.Nanoseconds() + return time.Now().Add(p.cfg.ConnMaxLifetime + time.Duration(jitterNs)) } func (p *ConnPool) tryDial() { @@ -237,19 +762,26 @@ func (p *ConnPool) tryDial() { return } - ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + // Probe dialing even when dialErrorsNum is saturated. Apply DialTimeout per probe + // attempt so custom dialers can't hang indefinitely. + ctx := context.Background() + var cancel context.CancelFunc + if p.cfg.DialTimeout > 0 { + ctx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout) + } conn, err := p.cfg.Dialer(ctx) + if cancel != nil { + cancel() + } if err != nil { p.setLastDialError(err) time.Sleep(time.Second) - cancel() continue } atomic.StoreUint32(&p.dialErrorsNum, 0) _ = conn.Close() - cancel() return } } @@ -268,17 +800,79 @@ func (p *ConnPool) getLastDialError() error { // Get returns existed connection from the pool or creates a new one. func (p *ConnPool) Get(ctx context.Context) (*Conn, error) { + return p.getConn(ctx) +} + +// getConn returns a connection from the pool. +func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { if p.closed() { return nil, ErrClosed } - if err := p.waitTurn(ctx); err != nil { + // Track pending requests in pool stats + atomic.AddUint32(&p.stats.PendingRequests, 1) + // Record pending request increment (UpDownCounter) + // Pass pool name explicitly since we don't have a connection yet + poolName := p.cfg.Name + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, 1, nil, poolName) + } + defer func() { + if err != nil { + // Failed to get connection, decrement pending requests + atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement on failure + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, nil, poolName) + } + } + }() + + // Track wait time - only call time.Now() if callback is registered + var waitStart time.Time + waitTimeCallback := getMetricConnectionWaitTimeCallback() + if waitTimeCallback != nil { + waitStart = time.Now() + } + if err = p.waitTurn(ctx); err != nil { + // Record timeout if applicable + if err == ErrPoolTimeout { + if cb := getMetricConnectionTimeoutCallback(); cb != nil { + cb(ctx, nil, "pool") + } + // Record general error metric for pool timeout + if cb := GetMetricErrorCallback(); cb != nil { + cb(ctx, "POOL_TIMEOUT", nil, "POOL_TIMEOUT", true, 0) + } + } return nil, err } + var waitDuration time.Duration + if waitTimeCallback != nil { + waitDuration = time.Since(waitStart) + } + + // Use cached time for health checks (max 50ms staleness is acceptable) + nowNs := getCachedTimeNs() + + // Lock-free atomic read - no mutex overhead! + hookManager := p.hookManager.Load() + + for attempts := 0; attempts < getAttempts; attempts++ { - for { p.connsMu.Lock() - cn, err := p.popIdle() + cn, err = p.popIdle() + if cn != nil { + // Emit idle→used transition inside the lock so Close() sees + // consistent state (conn removed from idleConns = "used"). + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateIdle, MetricStateUsed) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "idle", false) + cb(ctx, 1, cn, "used", false) + } + } p.connsMu.Unlock() if err != nil { @@ -290,155 +884,634 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) { break } - if !p.isHealthyConn(cn) { - _ = p.CloseConn(cn) + if !p.isHealthyConn(cn, nowNs) { + // Connection was already transitioned to MetricStateUsed under the lock above. + _ = p.CloseConn(ctx, cn, CloseReasonStale, MetricStateUsed) continue } + // Process connection using the hooks system + // Combine error and rejection checks to reduce branches + if hookManager != nil { + acceptConn, hookErr := hookManager.ProcessOnGet(ctx, cn, false) + if hookErr != nil || !acceptConn { + if hookErr != nil { + internal.Logger.Printf(ctx, "redis: connection pool: failed to process idle connection by hook: %v", hookErr) + // Connection was already transitioned to MetricStateUsed under the lock above. + _ = p.CloseConn(ctx, cn, CloseReasonHookError, MetricStateUsed) + } else { + internal.Logger.Printf(ctx, "redis: connection pool: conn[%d] rejected by hook, returning to pool", cn.GetID()) + // Connection is already in MetricStateUsed (transitioned under the lock above). + // Return connection to pool without freeing the turn that this Get() call holds. + // putConnWithoutTurn will emit used→idle transition. + p.putConnWithoutTurn(ctx, cn) + cn = nil + } + continue + } + } + atomic.AddUint32(&p.stats.Hits, 1) + + // Record wait time (use cached callback from above) + if waitTimeCallback != nil { + waitTimeCallback(ctx, waitDuration, cn) + } + + // Decrement pending requests (connection acquired successfully) + atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement (UpDownCounter) + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, cn, poolName) + } + return cn, nil } atomic.AddUint32(&p.stats.Misses, 1) - newcn, err := p.newConn(ctx, true) + var newcn *Conn + newcn, err = p.queuedNewConn(ctx) if err != nil { - p.freeTurn() return nil, err } + // Process connection using the hooks system + // This includes the handshake (HELLO/AUTH) via initConn hook + if hookManager != nil { + var acceptConn bool + acceptConn, err = hookManager.ProcessOnGet(ctx, newcn, true) + // both errors and accept=false mean a hook rejected the connection + // this should not happen with a new connection, but we handle it gracefully + if err != nil || !acceptConn { + internal.Logger.Printf(ctx, "redis: connection pool: failed to process new connection conn[%d] by hook: accept=%v, err=%v", newcn.GetID(), acceptConn, err) + // newConn emitted +1 used; CloseConn will emit -1 used if we own the removal. + _ = p.CloseConn(ctx, newcn, CloseReasonHookError, MetricStateUsed) + return nil, err + } + + // Record connection creation time metric when hooks are used. + // When hookManager is set, ProcessOnGet initializes the connection (AUTH/HELLO), + // causing IsInited()=true. This means _getConn() in redis.go will take the + // early return path and never reach its create time recording. + // When hookManager is nil, _getConn() handles both initialization and create time recording. + if dialStartNs := newcn.GetDialStartNs(); dialStartNs > 0 { + if cb := GetMetricConnectionCreateTimeCallback(); cb != nil { + duration := time.Duration(time.Now().UnixNano() - dialStartNs) + cb(ctx, duration, newcn) + } + } + } + + // newConn already emitted +1 used, so no transition needed here. + + // Record wait time (use cached callback from above) + if waitTimeCallback != nil { + waitTimeCallback(ctx, waitDuration, newcn) + } + + // Decrement pending requests (connection acquired successfully) + atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement (UpDownCounter) + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, newcn, poolName) + } + return newcn, nil } +func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) { + select { + case p.dialsInProgress <- struct{}{}: + // Got permission, proceed to create connection + case <-ctx.Done(): + p.freeTurn() + return nil, ctx.Err() + } + + // Don't apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + dialCtx, cancel := context.WithCancel(context.Background()) + + w := &wantConn{ + ctx: dialCtx, + cancelCtx: cancel, + result: make(chan wantConnResult, 1), + } + var err error + defer func() { + if err != nil { + if cn := w.cancel(); cn != nil && p.putIdleConn(ctx, cn) { + p.freeTurn() + } + } + }() + + p.dialsQueue.discardDoneAtFront() + p.dialsQueue.enqueue(w) + + go func(w *wantConn) { + var freeTurnCalled bool + defer func() { + if err := recover(); err != nil { + w.tryDeliver(nil, errPanicInQueuedNewConn) + p.dialsQueue.discardDoneAtFront() + if !freeTurnCalled { + p.freeTurn() + } + internal.Logger.Printf(context.Background(), "queuedNewConn panic: %+v", err) + } + }() + + defer w.cancelCtx() + defer func() { <-p.dialsInProgress }() // Release connection creation permission + + dialCtx := w.getCtxForDial() + cn, cnErr := p.newConn(dialCtx, true) + if cnErr != nil { + w.tryDeliver(nil, cnErr) // deliver error to caller, notify connection creation failed + p.dialsQueue.discardDoneAtFront() + p.freeTurn() + freeTurnCalled = true + return + } + + delivered := w.tryDeliver(cn, cnErr) + p.dialsQueue.discardDoneAtFront() + if !delivered && p.putIdleConn(dialCtx, cn) { + p.freeTurn() + freeTurnCalled = true + } + }(w) + + select { + case <-ctx.Done(): + err = ctx.Err() + return nil, err + case result := <-w.result: + err = result.err + return result.cn, err + } +} + +// putIdleConn puts a connection back to the pool or passes it to the next waiting request. +// +// It returns true if the connection was put back to the pool, +// which means the turn needs to be freed directly by the caller, +// or false if the connection was passed to the next waiting request, +// which means the turn will be freed by the waiting goroutine after it returns. +func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) bool { + for { + w, ok := p.dialsQueue.dequeue() + if !ok { + break + } + if w.tryDeliver(cn, nil) { + return false + } + } + + p.connsMu.Lock() + defer p.connsMu.Unlock() + + if p.closed() { + // Don't close here — this connection is still in p.conns and Close() + // will handle closing it and emitting the correct metric decrements. + // We just skip adding it to idleConns. + return true + } + + p.idleConns = append(p.idleConns, cn) + p.idleConnsLen.Add(1) + + // Connection was created as "used" in newConn; transition to idle. + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } + + return true +} + func (p *ConnPool) waitTurn(ctx context.Context) error { + // Fast path: check context first select { case <-ctx.Done(): return ctx.Err() default: } - select { - case p.queue <- struct{}{}: + // Fast path: try to acquire without blocking + if p.semaphore.TryAcquire() { return nil - default: } + // Slow path: need to wait start := time.Now() - timer := timers.Get().(*time.Timer) - timer.Reset(p.cfg.PoolTimeout) + err := p.semaphore.Acquire(ctx, p.cfg.PoolTimeout, ErrPoolTimeout) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - timers.Put(timer) - return ctx.Err() - case p.queue <- struct{}{}: - p.waitDurationNs.Add(time.Since(start).Nanoseconds()) + switch err { + case nil: + // Successfully acquired after waiting + p.waitDurationNs.Add(time.Now().UnixNano() - start.UnixNano()) atomic.AddUint32(&p.stats.WaitCount, 1) - if !timer.Stop() { - <-timer.C - } - timers.Put(timer) - return nil - case <-timer.C: - timers.Put(timer) + case ErrPoolTimeout: atomic.AddUint32(&p.stats.Timeouts, 1) - return ErrPoolTimeout } + + return err } func (p *ConnPool) freeTurn() { - <-p.queue + p.semaphore.Release() } func (p *ConnPool) popIdle() (*Conn, error) { if p.closed() { return nil, ErrClosed } + defer p.checkMinIdleConns() + n := len(p.idleConns) if n == 0 { return nil, nil } var cn *Conn - if p.cfg.PoolFIFO { - cn = p.idleConns[0] - copy(p.idleConns, p.idleConns[1:]) - p.idleConns = p.idleConns[:n-1] - } else { - idx := n - 1 - cn = p.idleConns[idx] - p.idleConns = p.idleConns[:idx] + attempts := 0 + + maxAttempts := min(popAttempts, n) + for attempts < maxAttempts { + if len(p.idleConns) == 0 { + return nil, nil + } + + if p.cfg.PoolFIFO { + cn = p.idleConns[0] + copy(p.idleConns, p.idleConns[1:]) + p.idleConns = p.idleConns[:len(p.idleConns)-1] + } else { + idx := len(p.idleConns) - 1 + cn = p.idleConns[idx] + p.idleConns = p.idleConns[:idx] + } + attempts++ + + // Hot path optimization: try IDLE → IN_USE or CREATED → IN_USE transition + // Using inline TryAcquire() method for better performance (avoids pointer dereference) + if cn.TryAcquire() { + // Successfully acquired the connection + p.idleConnsLen.Add(-1) + break + } + + // Connection is in UNUSABLE, INITIALIZING, or other state - skip it + + // Connection is not in a valid state (might be UNUSABLE for handoff/re-auth, INITIALIZING, etc.) + // Put it back in the pool and try the next one + if p.cfg.PoolFIFO { + // FIFO: put at end (will be picked up last since we pop from front) + p.idleConns = append(p.idleConns, cn) + } else { + // LIFO: put at beginning (will be picked up last since we pop from end) + p.idleConns = append([]*Conn{cn}, p.idleConns...) + } + cn = nil } - p.idleConnsLen-- - p.checkMinIdleConns() + + // If we exhausted all attempts without finding a usable connection, return nil + if attempts > 1 && attempts >= maxAttempts && int32(attempts) >= p.poolSize.Load() { + internal.Logger.Printf(context.Background(), "redis: connection pool: failed to get a usable connection after %d attempts", attempts) + return nil, nil + } + return cn, nil } func (p *ConnPool) Put(ctx context.Context, cn *Conn) { - if cn.rd.Buffered() > 0 { - internal.Logger.Printf(ctx, "Conn has unread data") - p.Remove(ctx, cn, BadConnError{}) + p.putConn(ctx, cn, true) +} + +// putConnWithoutTurn is an internal method that puts a connection back to the pool +// without freeing a turn. This is used when returning a rejected connection from +// within Get(), where the turn is still held by the Get() call. +func (p *ConnPool) putConnWithoutTurn(ctx context.Context, cn *Conn) { + p.putConn(ctx, cn, false) +} + +// putConn is the internal implementation of Put that optionally frees a turn. +func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { + // Guard against nil connection + if cn == nil { + internal.Logger.Printf(ctx, "putConn called with nil connection") + if freeTurn { + p.freeTurn() + } + return + } + + // Process connection using the hooks system + shouldPool := true + shouldRemove := false + var err error + + if cn.HasBufferedData() { + // Peek at the reply type to check if it's a push notification + if replyType, err := cn.PeekReplyTypeSafe(); err != nil || replyType != proto.RespPush { + // Not a push notification or error peeking, remove connection + internal.Logger.Printf(ctx, "Conn has unread data (not push notification), removing it") + p.removeConnInternal(ctx, cn, err, freeTurn) + return + } + // It's a push notification, allow pooling (client will handle it) + } + + // Lock-free atomic read - no mutex overhead! + hookManager := p.hookManager.Load() + + if hookManager != nil { + shouldPool, shouldRemove, err = hookManager.ProcessOnPut(ctx, cn) + if err != nil { + internal.Logger.Printf(ctx, "Connection hook error: %v", err) + p.removeConnInternal(ctx, cn, err, freeTurn) + return + } + } + + // Combine all removal checks into one - reduces branches + if shouldRemove || !shouldPool { + p.removeConnInternal(ctx, cn, errHookRequestedRemoval, freeTurn) return } if !cn.pooled { - p.Remove(ctx, cn, nil) + p.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn) return } var shouldCloseConn bool + var removedFromPool bool + + if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns { + // Hot path optimization: try fast IN_USE → IDLE transition + // Using inline Release() method for better performance (avoids pointer dereference) + transitionedToIdle := cn.Release() + + // Handle unexpected state changes + if !transitionedToIdle { + // Fast path failed - hook might have changed state (e.g., to UNUSABLE for handoff) + // Keep the state set by the hook and pool the connection anyway + sm := cn.GetStateMachine() + if sm == nil { + // State machine is nil - connection is in an invalid state, remove it + internal.Logger.Printf(ctx, "conn[%d] has nil state machine, removing it", cn.GetID()) + p.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn) + return + } + currentState := sm.GetState() + switch currentState { + case StateUnusable: + // expected state, don't log it + case StateClosed: + internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, closing it", cn.GetID(), currentState) + shouldCloseConn = true + removedFromPool = p.removeConnWithLock(cn) + default: + // Pool as-is + internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, pooling as-is", cn.GetID(), currentState) + } + } - p.connsMu.Lock() + // unusable conns are expected to become usable at some point (background process is reconnecting them) + // put them at the opposite end of the queue + // Optimization: if we just transitioned to IDLE, we know it's usable - skip the check + if !transitionedToIdle && !cn.IsUsable() { + p.connsMu.Lock() + // Check if Close() already removed this connection from p.conns. + // If so, skip the append and metrics — Close() already accounted for it. + if _, inPool := p.conns[cn.GetID()]; inPool { + if p.cfg.PoolFIFO { + p.idleConns = append(p.idleConns, cn) + } else { + p.idleConns = append([]*Conn{cn}, p.idleConns...) + } + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } + p.connsMu.Unlock() + p.idleConnsLen.Add(1) + } else { + shouldCloseConn = true + p.connsMu.Unlock() + } + } else if !shouldCloseConn { + p.connsMu.Lock() + if _, inPool := p.conns[cn.GetID()]; inPool { + p.idleConns = append(p.idleConns, cn) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } + p.connsMu.Unlock() + p.idleConnsLen.Add(1) + } else { + shouldCloseConn = true + p.connsMu.Unlock() + } + } - if p.cfg.MaxIdleConns == 0 || p.idleConnsLen < p.cfg.MaxIdleConns { - p.idleConns = append(p.idleConns, cn) - p.idleConnsLen++ + if shouldCloseConn { + // Connection was removed (e.g., hook set state to StateClosed). + // Only emit if we actually removed it from the map (not already taken by Close()). + if removedFromPool { + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } + } + } } else { - p.removeConn(cn) shouldCloseConn = true - } + removedFromPool = p.removeConnWithLock(cn) - p.connsMu.Unlock() + // Only emit if we actually removed it from the map (not already taken by Close()). + if removedFromPool { + // Notify metrics: connection removed (used -> nothing) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + // Record connection count decrement (connection removed while in used state) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } + } + } - p.freeTurn() + if freeTurn { + p.freeTurn() + } if shouldCloseConn { + // Only emit connection closed if we actually owned the removal. + // If removedFromPool is false, Close() already emitted connectionClosed for this conn. + if removedFromPool { + if cb := getMetricConnectionClosedCallback(); cb != nil { + reason := "conn_pool_close" + if r := cn.closeReason.Load(); r != "" { + reason = r + } + cb(ctx, cn, reason, nil) + } + } _ = p.closeConn(cn) } + + cn.SetLastPutAtNs(getCachedTimeNs()) +} + +func (p *ConnPool) Remove(ctx context.Context, cn *Conn, reason error) { + p.removeConnInternal(ctx, cn, reason, true) +} + +// RemoveWithoutTurn removes a connection from the pool without freeing a turn. +// This should be used when removing a connection from a context that didn't acquire +// a turn via Get() (e.g., background workers, cleanup tasks). +// For normal removal after Get(), use Remove() instead. +func (p *ConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) { + p.removeConnInternal(ctx, cn, reason, false) } -func (p *ConnPool) Remove(_ context.Context, cn *Conn, reason error) { - p.removeConnWithLock(cn) - p.freeTurn() +// removeConnInternal is the internal implementation of Remove that optionally frees a turn. +func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason error, freeTurn bool) { + // Lock-free atomic read - no mutex overhead! + hookManager := p.hookManager.Load() + + if hookManager != nil { + hookManager.ProcessOnRemove(ctx, cn, reason) + } + + removed := p.removeConnWithLock(cn) + + if freeTurn { + p.freeTurn() + } + + // Only emit metric decrements if we actually removed the connection from the map. + // If removed is false, Close() already removed it and emitted the -1 delta. + if removed { + // Notify metrics: connection removed (assume from used state) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + // Record connection count decrement (connection removed, assume from used state) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } + } + + // Only emit connection closed if we actually owned the removal. + // If removed is false, Close() already emitted connectionClosed for this conn. + if removed { + if cb := getMetricConnectionClosedCallback(); cb != nil { + reasonStr := "unknown" + if reason != nil { + reasonStr = reason.Error() + } + cb(ctx, cn, reasonStr, reason) + } + } + _ = p.closeConn(cn) + + // Check if we need to create new idle connections to maintain MinIdleConns + p.checkMinIdleConns() } -func (p *ConnPool) CloseConn(cn *Conn) error { - p.removeConnWithLock(cn) +// CloseConn closes a connection and records metrics. +// Parameters: +// - ctx: context for metric callbacks (enables trace-to-metric correlation) +// - cn: the connection to close +// - reason: why the connection is being closed (use CloseReason* constants) +// - fromState: the metric state the connection was in (use MetricState* constants) +func (p *ConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + if hookManager := p.hookManager.Load(); hookManager != nil { + hookManager.ProcessOnRemove(ctx, cn, errors.New(reason)) + } + + removed := p.removeConnWithLock(cn) + + // Only emit UpDownCounter decrements if we actually removed the connection. + // If removed is false, Close() already removed it and emitted the -1 delta. + // Only emit connection closed if we actually owned the removal. + // If removed is false, Close() already emitted connectionClosed for this conn. + if removed { + p.recordConnectionMetrics(ctx, cn, reason, fromState) + } + return p.closeConn(cn) } -func (p *ConnPool) removeConnWithLock(cn *Conn) { +func (p *ConnPool) recordConnectionMetrics(ctx context.Context, cn *Conn, reason string, fromState string) { + // Record connection state change: connection is being removed from the specified state + if cb := getMetricConnectionStateChangeCallback(); cb != nil && fromState != "" { + cb(ctx, cn, fromState, "") + } + + // Record connection count decrement (UpDownCounter) for the state the connection was in + if cb := getMetricConnectionCountCallback(); cb != nil && fromState != "" { + cb(ctx, -1, cn, fromState, false) + } + + if cb := getMetricConnectionClosedCallback(); cb != nil { + cb(ctx, cn, reason, nil) + } +} + +// removeConnWithLock removes a connection from the pool under the connsMu lock. +// Returns true if the connection was actually present in p.conns and was removed, +// false if it was already gone (e.g., removed by Close()). Callers must use the +// return value to decide whether to emit metric decrements — this eliminates the +// shutdown race between Close() and concurrent removal paths. +func (p *ConnPool) removeConnWithLock(cn *Conn) bool { p.connsMu.Lock() defer p.connsMu.Unlock() - p.removeConn(cn) + return p.removeConn(cn) } -func (p *ConnPool) removeConn(cn *Conn) { - for i, c := range p.conns { - if c == cn { - p.conns = append(p.conns[:i], p.conns[i+1:]...) - if cn.pooled { - p.poolSize-- - p.checkMinIdleConns() +// removeConn removes a connection from the pool's internal data structures. +// Returns true if the connection was present and removed, false otherwise. +func (p *ConnPool) removeConn(cn *Conn) bool { + cid := cn.GetID() + if _, exists := p.conns[cid]; !exists { + return false + } + delete(p.conns, cid) + atomic.AddUint32(&p.stats.StaleConns, 1) + + // Decrement pool size counter when removing a connection + if cn.pooled { + p.poolSize.Add(-1) + // this can be idle conn + for idx, ic := range p.idleConns { + if ic == cn { + p.idleConns = append(p.idleConns[:idx], p.idleConns[idx+1:]...) + p.idleConnsLen.Add(-1) + break } - break } } - atomic.AddUint32(&p.stats.StaleConns, 1) + return true } func (p *ConnPool) closeConn(cn *Conn) error { @@ -456,18 +1529,28 @@ func (p *ConnPool) Len() int { // IdleLen returns number of idle connections. func (p *ConnPool) IdleLen() int { p.connsMu.Lock() - n := p.idleConnsLen + n := p.idleConnsLen.Load() p.connsMu.Unlock() - return n + return int(n) +} + +// Size returns the maximum pool size (capacity). +// +// This is used by the streaming credentials manager to size the re-auth worker pool, +// ensuring that re-auth operations don't exhaust the connection pool. +func (p *ConnPool) Size() int { + return int(p.cfg.PoolSize) } func (p *ConnPool) Stats() *Stats { return &Stats{ - Hits: atomic.LoadUint32(&p.stats.Hits), - Misses: atomic.LoadUint32(&p.stats.Misses), - Timeouts: atomic.LoadUint32(&p.stats.Timeouts), - WaitCount: atomic.LoadUint32(&p.stats.WaitCount), - WaitDurationNs: p.waitDurationNs.Load(), + Hits: atomic.LoadUint32(&p.stats.Hits), + Misses: atomic.LoadUint32(&p.stats.Misses), + Timeouts: atomic.LoadUint32(&p.stats.Timeouts), + WaitCount: atomic.LoadUint32(&p.stats.WaitCount), + Unusable: atomic.LoadUint32(&p.stats.Unusable), + WaitDurationNs: p.waitDurationNs.Load(), + PendingRequests: atomic.LoadUint32(&p.stats.PendingRequests), TotalConns: uint32(p.Len()), IdleConns: uint32(p.IdleLen()), @@ -480,13 +1563,33 @@ func (p *ConnPool) closed() bool { } func (p *ConnPool) Filter(fn func(*Conn) bool) error { + ctx := context.Background() + p.connsMu.Lock() defer p.connsMu.Unlock() + idleConnSet := make(map[*Conn]struct{}, len(p.idleConns)) + for _, ic := range p.idleConns { + idleConnSet[ic] = struct{}{} + } + var firstErr error for _, cn := range p.conns { if fn(cn) { - if err := p.closeConn(cn); err != nil && firstErr == nil { + var err error + if _, isIdle := idleConnSet[cn]; isIdle { + // Idle connection - remove from pool and close. + p.removeConn(cn) + p.recordConnectionMetrics(ctx, cn, CloseReasonFailover, MetricStateIdle) + err = p.closeConn(cn) + } else { + // Used connection - set closeReason and close the connection. + // The connection remains in p.conns. When putConn() is called later, + // it will close the connection instead of pooling it. + cn.closeReason.Store(CloseReasonFailover) + err = cn.Close() + } + if err != nil && firstErr == nil { firstErr = err } } @@ -500,35 +1603,107 @@ func (p *ConnPool) Close() error { } var firstErr error + nowNs := time.Now().UnixNano() p.connsMu.Lock() + + // Emit -1 for each connection. Since all idle↔used transitions happen + // under connsMu, the idleConns slice is the source of truth for state. + cb := getMetricConnectionCountCallback() + idleSet := make(map[uint64]struct{}, len(p.idleConns)) + for _, cn := range p.idleConns { + idleSet[cn.GetID()] = struct{}{} + } + ctx := context.Background() for _, cn := range p.conns { + // Check health before closing, since closeConn invalidates the + // underlying fd and would make connCheck (inside isHealthyConn) + // always fail with EBADF. + // Only check health for idle connections to avoid data races when + // peeking at the socket/reader while another goroutine is reading from it. + // Non-idle connections are either in use or in transitional states and + // shouldn't be health-checked during shutdown. + _, isIdle := idleSet[cn.GetID()] + var healthy bool + if isIdle { + healthy = p.isHealthyConn(cn, nowNs) + } else { + healthy = true + } + if cb != nil { + if isIdle { + cb(ctx, -1, cn, "idle", false) + } else { + cb(ctx, -1, cn, "used", false) + } + } + if closedCb := getMetricConnectionClosedCallback(); closedCb != nil { + closedCb(ctx, cn, "pool_shutdown", nil) + } if err := p.closeConn(cn); err != nil && firstErr == nil { - firstErr = err + // Suppress close errors for stale connections, consistent + // with how Get() handles them (see CloseReasonStale path). + if healthy { + firstErr = err + } } } p.conns = nil - p.poolSize = 0 + p.poolSize.Store(0) p.idleConns = nil - p.idleConnsLen = 0 + p.idleConnsLen.Store(0) p.connsMu.Unlock() return firstErr } -func (p *ConnPool) isHealthyConn(cn *Conn) bool { - now := time.Now() +func (p *ConnPool) isHealthyConn(cn *Conn, nowNs int64) bool { + // Performance optimization: check conditions from cheapest to most expensive, + // and from most likely to fail to least likely to fail. - if p.cfg.ConnMaxLifetime > 0 && now.Sub(cn.createdAt) >= p.cfg.ConnMaxLifetime { - return false + // Only fails if ConnMaxLifetime is set AND connection is old. + // Most pools don't set ConnMaxLifetime, so this rarely fails. + if p.cfg.ConnMaxLifetime > 0 { + if cn.expiresAt.UnixNano() < nowNs { + return false // Connection has exceeded max lifetime + } } - if p.cfg.ConnMaxIdleTime > 0 && now.Sub(cn.UsedAt()) >= p.cfg.ConnMaxIdleTime { - return false + + // Most pools set ConnMaxIdleTime, and idle connections are common. + // Checking this first allows us to fail fast without expensive syscalls. + if p.cfg.ConnMaxIdleTime > 0 { + if nowNs-cn.UsedAtNs() >= int64(p.cfg.ConnMaxIdleTime) { + return false // Connection has been idle too long + } } - if connCheck(cn.netConn) != nil { + // Only run this if the cheap checks passed. + if err := connCheck(cn.getNetConn()); err != nil { + // If there's unexpected data, it might be push notifications (RESP3) + if p.cfg.PushNotificationsEnabled && err == errUnexpectedRead { + // Peek at the reply type to check if it's a push notification + if replyType, err := cn.rd.PeekReplyType(); err == nil && replyType == proto.RespPush { + // For RESP3 connections with push notifications, we allow some buffered data + // The client will process these notifications before using the connection + internal.Logger.Printf( + context.Background(), + "push: conn[%d] has buffered data, likely push notifications - will be processed by client", + cn.GetID(), + ) + + // Update timestamp for healthy connection + cn.SetUsedAtNs(nowNs) + + // Connection is healthy, client will handle notifications + return true + } + // Not a push notification - treat as unhealthy + return false + } + // Connection failed health check return false } - cn.SetUsedAt(now) + // Only update UsedAt if connection is healthy (avoids unnecessary atomic store) + cn.SetUsedAtNs(nowNs) return true } diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go index 5a3fde191bb..68295906993 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go @@ -1,7 +1,13 @@ package pool -import "context" +import ( + "context" + "time" +) +// SingleConnPool is a pool that always returns the same connection. +// Note: This pool is not thread-safe. +// It is intended to be used by clients that need a single connection. type SingleConnPool struct { pool Pooler cn *Conn @@ -10,6 +16,12 @@ type SingleConnPool struct { var _ Pooler = (*SingleConnPool)(nil) +// NewSingleConnPool creates a new single connection pool. +// The pool will always return the same connection. +// The pool will not: +// - Close the connection +// - Reconnect the connection +// - Track the connection in any way func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool { return &SingleConnPool{ pool: pool, @@ -21,24 +33,51 @@ func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) { return p.pool.NewConn(ctx) } -func (p *SingleConnPool) CloseConn(cn *Conn) error { - return p.pool.CloseConn(cn) +func (p *SingleConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + return p.pool.CloseConn(ctx, cn, reason, fromState) } -func (p *SingleConnPool) Get(ctx context.Context) (*Conn, error) { +func (p *SingleConnPool) Get(_ context.Context) (*Conn, error) { if p.stickyErr != nil { return nil, p.stickyErr } + if p.cn == nil { + return nil, ErrClosed + } + + // NOTE: SingleConnPool is NOT thread-safe by design and is used in special scenarios: + // - During initialization (connection is in INITIALIZING state) + // - During re-authentication (connection is in UNUSABLE state) + // - For transactions (connection might be in various states) + // We use SetUsed() which forces the transition, rather than TryTransition() which + // would fail if the connection is not in IDLE/CREATED state. + p.cn.SetUsed(true) + p.cn.SetUsedAt(time.Now()) return p.cn, nil } -func (p *SingleConnPool) Put(ctx context.Context, cn *Conn) {} +func (p *SingleConnPool) Put(_ context.Context, cn *Conn) { + if p.cn == nil { + return + } + if p.cn != cn { + return + } + p.cn.SetUsed(false) +} -func (p *SingleConnPool) Remove(ctx context.Context, cn *Conn, reason error) { +func (p *SingleConnPool) Remove(_ context.Context, cn *Conn, reason error) { + cn.SetUsed(false) p.cn = nil p.stickyErr = reason } +// RemoveWithoutTurn has the same behavior as Remove for SingleConnPool +// since SingleConnPool doesn't use a turn-based queue system. +func (p *SingleConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) { + p.Remove(ctx, cn, reason) +} + func (p *SingleConnPool) Close() error { p.cn = nil p.stickyErr = ErrClosed @@ -53,6 +92,13 @@ func (p *SingleConnPool) IdleLen() int { return 0 } +// Size returns the maximum pool size, which is always 1 for SingleConnPool. +func (p *SingleConnPool) Size() int { return 1 } + func (p *SingleConnPool) Stats() *Stats { return &Stats{} } + +func (p *SingleConnPool) AddPoolHook(_ PoolHook) {} + +func (p *SingleConnPool) RemovePoolHook(_ PoolHook) {} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go index 3adb99bc820..6763299eba9 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go @@ -61,8 +61,8 @@ func (p *StickyConnPool) NewConn(ctx context.Context) (*Conn, error) { return p.pool.NewConn(ctx) } -func (p *StickyConnPool) CloseConn(cn *Conn) error { - return p.pool.CloseConn(cn) +func (p *StickyConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + return p.pool.CloseConn(ctx, cn, reason, fromState) } func (p *StickyConnPool) Get(ctx context.Context) (*Conn, error) { @@ -123,6 +123,12 @@ func (p *StickyConnPool) Remove(ctx context.Context, cn *Conn, reason error) { p.ch <- cn } +// RemoveWithoutTurn has the same behavior as Remove for StickyConnPool +// since StickyConnPool doesn't use a turn-based queue system. +func (p *StickyConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) { + p.Remove(ctx, cn, reason) +} + func (p *StickyConnPool) Close() error { if shared := atomic.AddInt32(&p.shared, -1); shared > 0 { return nil @@ -196,6 +202,13 @@ func (p *StickyConnPool) IdleLen() int { return len(p.ch) } +// Size returns the maximum pool size, which is always 1 for StickyConnPool. +func (p *StickyConnPool) Size() int { return 1 } + func (p *StickyConnPool) Stats() *Stats { return &Stats{} } + +func (p *StickyConnPool) AddPoolHook(hook PoolHook) {} + +func (p *StickyConnPool) RemovePoolHook(hook PoolHook) {} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go new file mode 100644 index 00000000000..8cfa867887e --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go @@ -0,0 +1,105 @@ +package pool + +import ( + "context" + "net" + "sync" + "sync/atomic" +) + +type PubSubStats struct { + Created uint32 + Untracked uint32 + Active uint32 +} + +// PubSubPool manages a pool of PubSub connections. +type PubSubPool struct { + opt *Options + netDialer func(ctx context.Context, network, addr string) (net.Conn, error) + + // Map to track active PubSub connections + activeConns sync.Map // map[uint64]*Conn (connID -> conn) + closed atomic.Bool + stats PubSubStats +} + +// NewPubSubPool implements a pool for PubSub connections. +// It intentionally does not implement the Pooler interface +func NewPubSubPool(opt *Options, netDialer func(ctx context.Context, network, addr string) (net.Conn, error)) *PubSubPool { + return &PubSubPool{ + opt: opt, + netDialer: netDialer, + } +} + +func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, channels []string) (*Conn, error) { + if p.closed.Load() { + return nil, ErrClosed + } + + netConn, err := p.netDialer(ctx, network, addr) + if err != nil { + return nil, err + } + cn := NewConnWithBufferSize(netConn, p.opt.ReadBufferSize, p.opt.WriteBufferSize) + cn.pubsub = true + // Set pool name for metrics + cn.SetPoolName(p.opt.Name) + atomic.AddUint32(&p.stats.Created, 1) + return cn, nil +} + +func (p *PubSubPool) TrackConn(cn *Conn) { + atomic.AddUint32(&p.stats.Active, 1) + p.activeConns.Store(cn.GetID(), cn) + // Emit +1 used for PubSub connection + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), 1, cn, "used", true) + } +} + +func (p *PubSubPool) UntrackConn(cn *Conn) { + // LoadAndDelete ensures each connection is only decremented once, + // guarding against double-decrement if Close() already untracked it. + if _, loaded := p.activeConns.LoadAndDelete(cn.GetID()); !loaded { + return + } + atomic.AddUint32(&p.stats.Active, ^uint32(0)) + atomic.AddUint32(&p.stats.Untracked, 1) + // Emit -1 used for PubSub connection + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), -1, cn, "used", true) + } +} + +func (p *PubSubPool) Close() error { + p.closed.Store(true) + cb := getMetricConnectionCountCallback() + p.activeConns.Range(func(key, value interface{}) bool { + cn := value.(*Conn) + // Use LoadAndDelete to atomically claim ownership of this entry. + // If a concurrent UntrackConn already removed it, skip to avoid double-decrement. + if _, loaded := p.activeConns.LoadAndDelete(key); !loaded { + return true + } + atomic.AddUint32(&p.stats.Active, ^uint32(0)) + atomic.AddUint32(&p.stats.Untracked, 1) + // Emit -1 used for each PubSub connection being closed + if cb != nil { + cb(context.Background(), -1, cn, "used", true) + } + _ = cn.Close() + return true + }) + return nil +} + +func (p *PubSubPool) Stats() *PubSubStats { + // load stats atomically + return &PubSubStats{ + Created: atomic.LoadUint32(&p.stats.Created), + Untracked: atomic.LoadUint32(&p.stats.Untracked), + Active: atomic.LoadUint32(&p.stats.Active), + } +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go b/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go new file mode 100644 index 00000000000..78f86813f4b --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go @@ -0,0 +1,115 @@ +package pool + +import ( + "context" + "sync" +) + +type wantConn struct { + mu sync.RWMutex // protects ctx, done and sending of the result + ctx context.Context // context for dial, cleared after delivered or canceled + cancelCtx context.CancelFunc + done bool // true after delivered or canceled + result chan wantConnResult // channel to deliver connection or error +} + +// getCtxForDial returns context for dial or nil if connection was delivered or canceled. +func (w *wantConn) getCtxForDial() context.Context { + w.mu.RLock() + defer w.mu.RUnlock() + + return w.ctx +} + +func (w *wantConn) tryDeliver(cn *Conn, err error) bool { + w.mu.Lock() + defer w.mu.Unlock() + if w.done { + return false + } + + w.done = true + w.ctx = nil + + w.result <- wantConnResult{cn: cn, err: err} + close(w.result) + + return true +} + +func (w *wantConn) cancel() *Conn { + w.mu.Lock() + var cn *Conn + if w.done { + select { + case result := <-w.result: + cn = result.cn + default: + } + } else { + close(w.result) + } + + w.done = true + w.ctx = nil + w.mu.Unlock() + + return cn +} + +func (w *wantConn) isOngoing() bool { + w.mu.RLock() + defer w.mu.RUnlock() + return !w.done +} + +type wantConnResult struct { + cn *Conn + err error +} + +type wantConnQueue struct { + mu sync.RWMutex + items []*wantConn +} + +func newWantConnQueue() *wantConnQueue { + return &wantConnQueue{ + items: make([]*wantConn, 0), + } +} + +func (q *wantConnQueue) enqueue(w *wantConn) { + q.mu.Lock() + defer q.mu.Unlock() + q.items = append(q.items, w) +} + +func (q *wantConnQueue) dequeue() (*wantConn, bool) { + q.mu.Lock() + defer q.mu.Unlock() + + if len(q.items) == 0 { + return nil, false + } + + item := q.items[0] + q.items = q.items[1:] + return item, true +} + +func (q *wantConnQueue) discardDoneAtFront() int { + q.mu.Lock() + defer q.mu.Unlock() + count := 0 + for len(q.items) > 0 { + if q.items[0].isOngoing() { + break + } + + q.items = q.items[1:] + count++ + } + + return count +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go b/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go index 8d23817fe8f..83f28e4da49 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go +++ b/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go @@ -12,6 +12,9 @@ import ( "github.com/redis/go-redis/v9/internal/util" ) +// DefaultBufferSize is the default size for read/write buffers (32 KiB). +const DefaultBufferSize = 32 * 1024 + // redis resp protocol data type. const ( RespStatus = '+' // +\r\n @@ -47,7 +50,8 @@ func (e RedisError) Error() string { return string(e) } func (RedisError) RedisError() {} func ParseErrorReply(line []byte) error { - return RedisError(line[1:]) + msg := string(line[1:]) + return parseTypedRedisError(msg) } //------------------------------------------------------------------------------ @@ -58,7 +62,13 @@ type Reader struct { func NewReader(rd io.Reader) *Reader { return &Reader{ - rd: bufio.NewReader(rd), + rd: bufio.NewReaderSize(rd, DefaultBufferSize), + } +} + +func NewReaderSize(rd io.Reader, size int) *Reader { + return &Reader{ + rd: bufio.NewReaderSize(rd, size), } } @@ -90,6 +100,161 @@ func (r *Reader) PeekReplyType() (byte, error) { return b[0], nil } +// PeekPushNotificationName returns the notification name of the next RESP3 +// push frame without consuming it. The caller is expected to have already +// verified that the next reply is a push notification (e.g. via PeekReplyType +// returning RespPush). +// +// To identify the name the method may block briefly reading more bytes from +// the underlying connection. That is safe: once the push marker '>' has been +// observed, the server is committed to sending the rest of the frame, so +// fetching the next few header bytes does not race with anything the caller +// could be waiting on. Blocking is preferred to a truncated peek, which would +// silently misidentify the notification and cause the caller's ReadReply to +// consume (and drop) the frame; see issue #3839. +func (r *Reader) PeekPushNotificationName() (string, error) { + c, err := r.rd.Peek(1) + if err != nil { + return "", err + } + if c[0] != RespPush { + return "", fmt.Errorf("redis: can't peek push notification name, next reply is not a push notification") + } + + // Start with a peek window that covers every Redis-defined notification + // header (MOVING, MIGRATING, FAILED_OVER, message, pmessage, smessage, + // subscribe, unsubscribe, ...). If a longer name is encountered, grow + // the window up to maxPushHeaderPeek before giving up. + const initialPeek = 36 + const maxPushHeaderPeek = 4096 + + peekSize := initialPeek + for { + buf, peekErr := r.rd.Peek(peekSize) + name, complete, parseErr := parsePushNotificationName(buf) + if parseErr != nil { + return "", parseErr + } + if complete { + return name, nil + } + // Parser ran out of bytes. Surface a failed underlying read before + // growing further; otherwise grow the peek window and retry. + if peekErr != nil { + return "", peekErr + } + if peekSize >= maxPushHeaderPeek { + return "", fmt.Errorf("redis: push notification header exceeds %d bytes", maxPushHeaderPeek) + } + peekSize *= 2 + if peekSize > maxPushHeaderPeek { + peekSize = maxPushHeaderPeek + } + } +} + +// parsePushNotificationName extracts the notification name from a buffered +// RESP3 push frame prefix. The three return values are: +// +// - (name, true, nil): the full name is in buf. +// - ("", false, nil): buf is a valid prefix but too short to determine the +// name; the caller should fetch more bytes and retry. +// - ("", _, err): buf is malformed. +// +// This split lets PeekPushNotificationName tell "incomplete header" apart +// from "corrupt frame" without ever returning a truncated string. +func parsePushNotificationName(buf []byte) (string, bool, error) { + // Need at least ">N\r" before any meaningful work. + if len(buf) < 3 { + return "", false, nil + } + if buf[0] != RespPush { + return "", false, fmt.Errorf("redis: can't parse push notification: %q", buf) + } + + // Skip the array length line ">N\r\n". + const arrayLenStart = 1 // first byte after the '>' marker + pos, ok, err := skipDigitsThenCRLF(buf, arrayLenStart) + if err != nil { + return "", false, fmt.Errorf("redis: can't parse push notification: %w", err) + } + if !ok { + return "", false, nil + } + // Reject ">\r\n": RESP requires at least one digit for the array length. + // Without this check the empty length looks like a valid prefix and the + // caller would block fetching more bytes for a frame that is already + // malformed. + if pos-2 == arrayLenStart { + return "", false, fmt.Errorf("redis: empty push notification array length") + } + + // First element type byte: '$' (bulk) or '+' (simple-string). + if pos >= len(buf) { + return "", false, nil + } + typeOfName := buf[pos] + if typeOfName != RespString && typeOfName != RespStatus { + return "", false, fmt.Errorf("redis: can't parse push notification name: %q", buf[pos:]) + } + pos++ + + if typeOfName == RespString { + // Read "$M\r\n" then the M-byte name. + lenStart := pos + next, ok, err := skipDigitsThenCRLF(buf, pos) + if err != nil { + return "", false, fmt.Errorf("redis: can't parse push notification name length: %w", err) + } + if !ok { + return "", false, nil + } + if next-2 == lenStart { + return "", false, fmt.Errorf("redis: empty push notification name length") + } + nameLen, err := util.Atoi(buf[lenStart : next-2]) + if err != nil { + return "", false, fmt.Errorf("redis: invalid push notification name length %q: %w", buf[lenStart:next-2], err) + } + if nameLen < 0 { + return "", false, fmt.Errorf("redis: negative push notification name length: %d", nameLen) + } + // Compare against the remaining bytes instead of computing + // next+nameLen: a hugely advertised length on malformed input could + // overflow int, wrap negative, slip past an "end > len(buf)" guard and + // panic the slice below. next <= len(buf) here, so the subtraction is + // safe. + if nameLen > len(buf)-next { + return "", false, nil + } + return util.BytesToString(buf[next : next+nameLen]), true, nil + } + + // RespStatus: scan for the terminating CRLF. + for i := pos; i < len(buf)-1; i++ { + if buf[i] == '\r' && buf[i+1] == '\n' { + return util.BytesToString(buf[pos:i]), true, nil + } + } + return "", false, nil +} + +// skipDigitsThenCRLF advances past zero-or-more ASCII digits and the +// terminating "\r\n" starting at offset start in buf. It returns the position +// after the "\r\n" and true on success; (pos, false, nil) if buf is too +// short; or an error if a non-digit non-CR byte is encountered before the CRLF. +func skipDigitsThenCRLF(buf []byte, start int) (int, bool, error) { + for pos := start; pos < len(buf)-1; pos++ { + if buf[pos] == '\r' && buf[pos+1] == '\n' { + return pos + 2, true, nil + } + if buf[pos] < '0' || buf[pos] > '9' { + return pos, false, fmt.Errorf("expected digit or CRLF, got %q", buf[pos]) + } + } + return len(buf), false, nil +} + // ReadLine Return a valid reply, it will check the protocol or redis error, // and discard the attribute type. func (r *Reader) ReadLine() ([]byte, error) { @@ -106,7 +271,7 @@ func (r *Reader) ReadLine() ([]byte, error) { var blobErr string blobErr, err = r.readStringReply(line) if err == nil { - err = RedisError(blobErr) + err = parseTypedRedisError(blobErr) } return nil, err case RespAttr: @@ -183,8 +348,8 @@ func (r *Reader) ReadReply() (interface{}, error) { } func (r *Reader) readFloat(line []byte) (float64, error) { - v := string(line[1:]) - switch string(line[1:]) { + v := util.BytesToString(line[1:]) + switch v { case "inf": return math.Inf(1), nil case "-inf": @@ -196,7 +361,7 @@ func (r *Reader) readFloat(line []byte) (float64, error) { } func (r *Reader) readBool(line []byte) (bool, error) { - switch string(line[1:]) { + switch util.BytesToString(line[1:]) { case "t": return true, nil case "f": @@ -207,7 +372,7 @@ func (r *Reader) readBool(line []byte) (bool, error) { func (r *Reader) readBigInt(line []byte) (*big.Int, error) { i := new(big.Int) - if i, ok := i.SetString(string(line[1:]), 10); ok { + if i, ok := i.SetString(util.BytesToString(line[1:]), 10); ok { return i, nil } return nil, fmt.Errorf("redis: can't parse bigInt reply: %q", line) @@ -357,7 +522,7 @@ func (r *Reader) ReadFloat() (float64, error) { case RespFloat: return r.readFloat(line) case RespStatus: - return strconv.ParseFloat(string(line[1:]), 64) + return strconv.ParseFloat(util.BytesToString(line[1:]), 64) case RespString: s, err := r.readStringReply(line) if err != nil { @@ -550,3 +715,193 @@ func IsNilReply(line []byte) bool { (line[0] == RespString || line[0] == RespArray) && line[1] == '-' && line[2] == '1' } + +// ReadRawReply reads the next RESP reply and returns it as raw bytes without parsing. +func (r *Reader) ReadRawReply() ([]byte, error) { + return r.readRawReplyBuf(nil) +} + +func (r *Reader) readRawReplyBuf(buf []byte) ([]byte, error) { + line, err := r.readLine() + if err != nil { + return buf, err + } + + buf = append(buf, line...) + buf = append(buf, '\r', '\n') + + switch line[0] { + case RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt: + return buf, nil + + case RespString, RespVerbatim, RespBlobError: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + curLen := len(buf) + buf = append(buf, make([]byte, n+2)...) + _, err = io.ReadFull(r.rd, buf[curLen:]) + return buf, err + + case RespArray, RespSet, RespPush: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + for i := 0; i < n; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + return buf, nil + + case RespMap: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + for i := 0; i < n*2; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + return buf, nil + + case RespAttr: + // Per RESP3 spec, an attribute is always followed by the actual command reply. + // We need to read the attribute's key-value pairs AND the following reply. + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + // Read the attribute key-value pairs + for i := 0; i < n*2; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + // Read the command reply that follows the attribute + return r.readRawReplyBuf(buf) + } + + return buf, fmt.Errorf("redis: can't read raw reply: %.100q", line) +} + +var crlf = []byte{'\r', '\n'} + +// ReadRawReplyWriteTo streams the next RESP reply directly to w without intermediate allocations. +// Returns the number of bytes written and any error encountered. +func (r *Reader) ReadRawReplyWriteTo(w io.Writer) (int64, error) { + return r.readRawReplyWriteTo(w) +} + +func (r *Reader) readRawReplyWriteTo(w io.Writer) (int64, error) { + line, err := r.readLine() + if err != nil { + return 0, err + } + + var written int64 + n, err := w.Write(line) + written += int64(n) + if err != nil { + return written, err + } + n, err = w.Write(crlf) + written += int64(n) + if err != nil { + return written, err + } + + switch line[0] { + case RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt: + return written, nil + + case RespString, RespVerbatim, RespBlobError: + dataLen, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + copied, err := io.CopyN(w, r.rd, int64(dataLen)+2) + written += copied + return written, err + + case RespArray, RespSet, RespPush: + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + for i := 0; i < count; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + return written, nil + + case RespMap: + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + for i := 0; i < count*2; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + return written, nil + + case RespAttr: + // Per RESP3 spec, an attribute is always followed by the actual command reply. + // We need to read the attribute's key-value pairs AND the following reply. + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + // Read the attribute key-value pairs + for i := 0; i < count*2; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + // Read the command reply that follows the attribute + n, err := r.readRawReplyWriteTo(w) + written += n + return written, err + } + + return written, fmt.Errorf("redis: can't read raw reply: %.100q", line) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go b/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go new file mode 100644 index 00000000000..a75370cf798 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go @@ -0,0 +1,539 @@ +package proto + +import ( + "errors" + "strings" +) + +// Typed Redis errors for better error handling with wrapping support. +// These errors maintain backward compatibility by keeping the same error messages. + +// LoadingError is returned when Redis is loading the dataset in memory. +type LoadingError struct { + msg string +} + +func (e *LoadingError) Error() string { + return e.msg +} + +func (e *LoadingError) RedisError() {} + +// NewLoadingError creates a new LoadingError with the given message. +func NewLoadingError(msg string) *LoadingError { + return &LoadingError{msg: msg} +} + +// ReadOnlyError is returned when trying to write to a read-only replica. +type ReadOnlyError struct { + msg string +} + +func (e *ReadOnlyError) Error() string { + return e.msg +} + +func (e *ReadOnlyError) RedisError() {} + +// NewReadOnlyError creates a new ReadOnlyError with the given message. +func NewReadOnlyError(msg string) *ReadOnlyError { + return &ReadOnlyError{msg: msg} +} + +// MovedError is returned when a key has been moved to a different node in a cluster. +type MovedError struct { + msg string + addr string +} + +func (e *MovedError) Error() string { + return e.msg +} + +func (e *MovedError) RedisError() {} + +// Addr returns the address of the node where the key has been moved. +func (e *MovedError) Addr() string { + return e.addr +} + +// NewMovedError creates a new MovedError with the given message and address. +func NewMovedError(msg string, addr string) *MovedError { + return &MovedError{msg: msg, addr: addr} +} + +// AskError is returned when a key is being migrated and the client should ask another node. +type AskError struct { + msg string + addr string +} + +func (e *AskError) Error() string { + return e.msg +} + +func (e *AskError) RedisError() {} + +// Addr returns the address of the node to ask. +func (e *AskError) Addr() string { + return e.addr +} + +// NewAskError creates a new AskError with the given message and address. +func NewAskError(msg string, addr string) *AskError { + return &AskError{msg: msg, addr: addr} +} + +// ClusterDownError is returned when the cluster is down. +type ClusterDownError struct { + msg string +} + +func (e *ClusterDownError) Error() string { + return e.msg +} + +func (e *ClusterDownError) RedisError() {} + +// NewClusterDownError creates a new ClusterDownError with the given message. +func NewClusterDownError(msg string) *ClusterDownError { + return &ClusterDownError{msg: msg} +} + +// TryAgainError is returned when a command cannot be processed and should be retried. +type TryAgainError struct { + msg string +} + +func (e *TryAgainError) Error() string { + return e.msg +} + +func (e *TryAgainError) RedisError() {} + +// NewTryAgainError creates a new TryAgainError with the given message. +func NewTryAgainError(msg string) *TryAgainError { + return &TryAgainError{msg: msg} +} + +// MasterDownError is returned when the master is down. +type MasterDownError struct { + msg string +} + +func (e *MasterDownError) Error() string { + return e.msg +} + +func (e *MasterDownError) RedisError() {} + +// NewMasterDownError creates a new MasterDownError with the given message. +func NewMasterDownError(msg string) *MasterDownError { + return &MasterDownError{msg: msg} +} + +// MaxClientsError is returned when the maximum number of clients has been reached. +type MaxClientsError struct { + msg string +} + +func (e *MaxClientsError) Error() string { + return e.msg +} + +func (e *MaxClientsError) RedisError() {} + +// NewMaxClientsError creates a new MaxClientsError with the given message. +func NewMaxClientsError(msg string) *MaxClientsError { + return &MaxClientsError{msg: msg} +} + +// AuthError is returned when authentication fails. +type AuthError struct { + msg string +} + +func (e *AuthError) Error() string { + return e.msg +} + +func (e *AuthError) RedisError() {} + +// NewAuthError creates a new AuthError with the given message. +func NewAuthError(msg string) *AuthError { + return &AuthError{msg: msg} +} + +// PermissionError is returned when a user lacks required permissions. +type PermissionError struct { + msg string +} + +func (e *PermissionError) Error() string { + return e.msg +} + +func (e *PermissionError) RedisError() {} + +// NewPermissionError creates a new PermissionError with the given message. +func NewPermissionError(msg string) *PermissionError { + return &PermissionError{msg: msg} +} + +// ExecAbortError is returned when a transaction is aborted. +type ExecAbortError struct { + msg string +} + +func (e *ExecAbortError) Error() string { + return e.msg +} + +func (e *ExecAbortError) RedisError() {} + +// NewExecAbortError creates a new ExecAbortError with the given message. +func NewExecAbortError(msg string) *ExecAbortError { + return &ExecAbortError{msg: msg} +} + +// OOMError is returned when Redis is out of memory. +type OOMError struct { + msg string +} + +func (e *OOMError) Error() string { + return e.msg +} + +func (e *OOMError) RedisError() {} + +// NewOOMError creates a new OOMError with the given message. +func NewOOMError(msg string) *OOMError { + return &OOMError{msg: msg} +} + +// NoReplicasError is returned when not enough replicas acknowledge a write. +// This error occurs when using WAIT/WAITAOF commands or CLUSTER SETSLOT with +// synchronous replication, and the required number of replicas cannot confirm +// the write within the timeout period. +type NoReplicasError struct { + msg string +} + +func (e *NoReplicasError) Error() string { + return e.msg +} + +func (e *NoReplicasError) RedisError() {} + +// NewNoReplicasError creates a new NoReplicasError with the given message. +func NewNoReplicasError(msg string) *NoReplicasError { + return &NoReplicasError{msg: msg} +} + +// parseTypedRedisError parses a Redis error message and returns a typed error if applicable. +// This function maintains backward compatibility by keeping the same error messages. +func parseTypedRedisError(msg string) error { + // Check for specific error patterns and return typed errors + switch { + case strings.HasPrefix(msg, "LOADING "): + return NewLoadingError(msg) + case strings.HasPrefix(msg, "READONLY "): + return NewReadOnlyError(msg) + case strings.HasPrefix(msg, "MOVED "): + // Extract address from "MOVED " + addr := extractAddr(msg) + return NewMovedError(msg, addr) + case strings.HasPrefix(msg, "ASK "): + // Extract address from "ASK " + addr := extractAddr(msg) + return NewAskError(msg, addr) + case strings.HasPrefix(msg, "CLUSTERDOWN "): + return NewClusterDownError(msg) + case strings.HasPrefix(msg, "TRYAGAIN "): + return NewTryAgainError(msg) + case strings.HasPrefix(msg, "MASTERDOWN "): + return NewMasterDownError(msg) + case strings.HasPrefix(msg, "NOREPLICAS "): + return NewNoReplicasError(msg) + case msg == "ERR max number of clients reached": + return NewMaxClientsError(msg) + case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"): + return NewAuthError(msg) + case strings.HasPrefix(msg, "NOPERM "): + return NewPermissionError(msg) + case strings.HasPrefix(msg, "EXECABORT "): + return NewExecAbortError(msg) + case strings.HasPrefix(msg, "OOM "): + return NewOOMError(msg) + default: + // Return generic RedisError for unknown error types + return RedisError(msg) + } +} + +// extractAddr extracts the address from MOVED/ASK error messages. +// Format: "MOVED " or "ASK " +func extractAddr(msg string) string { + ind := strings.LastIndex(msg, " ") + if ind == -1 { + return "" + } + return msg[ind+1:] +} + +// IsLoadingError checks if an error is a LoadingError, even if wrapped. +func IsLoadingError(err error) bool { + if err == nil { + return false + } + var loadingErr *LoadingError + if errors.As(err, &loadingErr) { + return true + } + // Check if wrapped error is a RedisError with LOADING prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "LOADING ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "LOADING ") +} + +// IsReadOnlyError checks if an error is a ReadOnlyError, even if wrapped. +func IsReadOnlyError(err error) bool { + if err == nil { + return false + } + var readOnlyErr *ReadOnlyError + if errors.As(err, &readOnlyErr) { + return true + } + // Check if wrapped error is a RedisError with READONLY prefix or Lua script READONLY + var redisErr RedisError + if errors.As(err, &redisErr) { + s := redisErr.Error() + if strings.HasPrefix(s, "READONLY ") { + return true + } + // Lua script wrapped READONLY errors: + // "ERR Error running script (call to f_): @user_script:N: -READONLY You can't write against a read only replica." + if strings.Contains(s, "-READONLY You can't write against a read only replica") { + return true + } + } + // Fallback to string checking for backward compatibility + s := err.Error() + if strings.HasPrefix(s, "READONLY ") { + return true + } + return strings.Contains(s, "-READONLY You can't write against a read only replica") +} + +// IsMovedError checks if an error is a MovedError, even if wrapped. +// Returns the error and a boolean indicating if it's a MovedError. +func IsMovedError(err error) (*MovedError, bool) { + if err == nil { + return nil, false + } + var movedErr *MovedError + if errors.As(err, &movedErr) { + return movedErr, true + } + // Fallback to string checking for backward compatibility + s := err.Error() + if strings.HasPrefix(s, "MOVED ") { + // Parse: MOVED 3999 127.0.0.1:6381 + parts := strings.Split(s, " ") + if len(parts) == 3 { + return &MovedError{msg: s, addr: parts[2]}, true + } + } + return nil, false +} + +// IsAskError checks if an error is an AskError, even if wrapped. +// Returns the error and a boolean indicating if it's an AskError. +func IsAskError(err error) (*AskError, bool) { + if err == nil { + return nil, false + } + var askErr *AskError + if errors.As(err, &askErr) { + return askErr, true + } + // Fallback to string checking for backward compatibility + s := err.Error() + if strings.HasPrefix(s, "ASK ") { + // Parse: ASK 3999 127.0.0.1:6381 + parts := strings.Split(s, " ") + if len(parts) == 3 { + return &AskError{msg: s, addr: parts[2]}, true + } + } + return nil, false +} + +// IsClusterDownError checks if an error is a ClusterDownError, even if wrapped. +func IsClusterDownError(err error) bool { + if err == nil { + return false + } + var clusterDownErr *ClusterDownError + if errors.As(err, &clusterDownErr) { + return true + } + // Check if wrapped error is a RedisError with CLUSTERDOWN prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "CLUSTERDOWN ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "CLUSTERDOWN ") +} + +// IsTryAgainError checks if an error is a TryAgainError, even if wrapped. +func IsTryAgainError(err error) bool { + if err == nil { + return false + } + var tryAgainErr *TryAgainError + if errors.As(err, &tryAgainErr) { + return true + } + // Check if wrapped error is a RedisError with TRYAGAIN prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "TRYAGAIN ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "TRYAGAIN ") +} + +// IsMasterDownError checks if an error is a MasterDownError, even if wrapped. +func IsMasterDownError(err error) bool { + if err == nil { + return false + } + var masterDownErr *MasterDownError + if errors.As(err, &masterDownErr) { + return true + } + // Check if wrapped error is a RedisError with MASTERDOWN prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "MASTERDOWN ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "MASTERDOWN ") +} + +// IsMaxClientsError checks if an error is a MaxClientsError, even if wrapped. +func IsMaxClientsError(err error) bool { + if err == nil { + return false + } + var maxClientsErr *MaxClientsError + if errors.As(err, &maxClientsErr) { + return true + } + // Check if wrapped error is a RedisError with max clients prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "ERR max number of clients reached") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "ERR max number of clients reached") +} + +// IsAuthError checks if an error is an AuthError, even if wrapped. +func IsAuthError(err error) bool { + if err == nil { + return false + } + var authErr *AuthError + if errors.As(err, &authErr) { + return true + } + // Check if wrapped error is a RedisError with auth error prefix + var redisErr RedisError + if errors.As(err, &redisErr) { + s := redisErr.Error() + return strings.HasPrefix(s, "NOAUTH ") || strings.HasPrefix(s, "WRONGPASS ") || strings.Contains(s, "unauthenticated") + } + // Fallback to string checking for backward compatibility + s := err.Error() + return strings.HasPrefix(s, "NOAUTH ") || strings.HasPrefix(s, "WRONGPASS ") || strings.Contains(s, "unauthenticated") +} + +// IsPermissionError checks if an error is a PermissionError, even if wrapped. +func IsPermissionError(err error) bool { + if err == nil { + return false + } + var permErr *PermissionError + if errors.As(err, &permErr) { + return true + } + // Check if wrapped error is a RedisError with NOPERM prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "NOPERM ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "NOPERM ") +} + +// IsExecAbortError checks if an error is an ExecAbortError, even if wrapped. +func IsExecAbortError(err error) bool { + if err == nil { + return false + } + var execAbortErr *ExecAbortError + if errors.As(err, &execAbortErr) { + return true + } + // Check if wrapped error is a RedisError with EXECABORT prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "EXECABORT ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "EXECABORT ") +} + +// IsOOMError checks if an error is an OOMError, even if wrapped. +func IsOOMError(err error) bool { + if err == nil { + return false + } + var oomErr *OOMError + if errors.As(err, &oomErr) { + return true + } + // Check if wrapped error is a RedisError with OOM prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "OOM ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "OOM ") +} + +// IsNoReplicasError checks if an error is a NoReplicasError, even if wrapped. +func IsNoReplicasError(err error) bool { + if err == nil { + return false + } + var noReplicasErr *NoReplicasError + if errors.As(err, &noReplicasErr) { + return true + } + // Check if wrapped error is a RedisError with NOREPLICAS prefix + var redisErr RedisError + if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "NOREPLICAS ") { + return true + } + // Fallback to string checking for backward compatibility + return strings.HasPrefix(err.Error(), "NOREPLICAS ") +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go b/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go deleted file mode 100644 index 2edccba94fe..00000000000 --- a/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go +++ /dev/null @@ -1,50 +0,0 @@ -package rand - -import ( - "math/rand" - "sync" -) - -// Int returns a non-negative pseudo-random int. -func Int() int { return pseudo.Int() } - -// Intn returns, as an int, a non-negative pseudo-random number in [0,n). -// It panics if n <= 0. -func Intn(n int) int { return pseudo.Intn(n) } - -// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). -// It panics if n <= 0. -func Int63n(n int64) int64 { return pseudo.Int63n(n) } - -// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n). -func Perm(n int) []int { return pseudo.Perm(n) } - -// Seed uses the provided seed value to initialize the default Source to a -// deterministic state. If Seed is not called, the generator behaves as if -// seeded by Seed(1). -func Seed(n int64) { pseudo.Seed(n) } - -var pseudo = rand.New(&source{src: rand.NewSource(1)}) - -type source struct { - src rand.Source - mu sync.Mutex -} - -func (s *source) Int63() int64 { - s.mu.Lock() - n := s.src.Int63() - s.mu.Unlock() - return n -} - -func (s *source) Seed(seed int64) { - s.mu.Lock() - s.src.Seed(seed) - s.mu.Unlock() -} - -// Shuffle pseudo-randomizes the order of elements. -// n is the number of elements. -// swap swaps the elements with indexes i and j. -func Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) } diff --git a/vendor/github.com/redis/go-redis/v9/internal/redis.go b/vendor/github.com/redis/go-redis/v9/internal/redis.go new file mode 100644 index 00000000000..190bbebeac0 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/redis.go @@ -0,0 +1,3 @@ +package internal + +const RedisNull = "" diff --git a/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go b/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go new file mode 100644 index 00000000000..0d6321ec11b --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go @@ -0,0 +1,1000 @@ +package routing + +import ( + "errors" + "fmt" + "math" + "sync" + + "sync/atomic" + + "github.com/redis/go-redis/v9/internal/util" + uberAtomic "go.uber.org/atomic" +) + +var ( + ErrMaxAggregation = errors.New("redis: no valid results to aggregate for max operation") + ErrMinAggregation = errors.New("redis: no valid results to aggregate for min operation") + ErrAndAggregation = errors.New("redis: no valid results to aggregate for logical AND operation") + ErrOrAggregation = errors.New("redis: no valid results to aggregate for logical OR operation") +) + +// ResponseAggregator defines the interface for aggregating responses from multiple shards. +type ResponseAggregator interface { + // Add processes a single shard response. + Add(result interface{}, err error) error + + // AddWithKey processes a single shard response for a specific key (used by keyed aggregators). + AddWithKey(key string, result interface{}, err error) error + + BatchAdd(map[string]AggregatorResErr) error + + BatchSlice([]AggregatorResErr) error + + // Result returns the final aggregated result and any error. + Result() (interface{}, error) +} + +type AggregatorResErr struct { + Result interface{} + Err error +} + +// NewResponseAggregator creates an aggregator based on the response policy. +func NewResponseAggregator(policy ResponsePolicy, cmdName string) ResponseAggregator { + switch policy { + case RespDefaultKeyless: + return &DefaultKeylessAggregator{results: make([]interface{}, 0)} + case RespDefaultHashSlot: + return &DefaultKeyedAggregator{results: make(map[string]interface{})} + case RespAllSucceeded: + return &AllSucceededAggregator{} + case RespOneSucceeded: + return &OneSucceededAggregator{} + case RespAggSum: + return &AggSumAggregator{ + // res: + } + case RespAggMin: + return &AggMinAggregator{ + res: util.NewAtomicMin(), + } + case RespAggMax: + return &AggMaxAggregator{ + res: util.NewAtomicMax(), + } + case RespAggLogicalAnd: + andAgg := &AggLogicalAndAggregator{} + andAgg.res.Store(true) + + return andAgg + case RespAggLogicalOr: + return &AggLogicalOrAggregator{} + case RespSpecial: + return NewSpecialAggregator(cmdName) + default: + return &AllSucceededAggregator{} + } +} + +func NewDefaultAggregator(isKeyed bool) ResponseAggregator { + if isKeyed { + return &DefaultKeyedAggregator{ + results: make(map[string]interface{}), + } + } + return &DefaultKeylessAggregator{} +} + +// AllSucceededAggregator returns one non-error reply if every shard succeeded, +// propagates the first error otherwise. +type AllSucceededAggregator struct { + err atomic.Value + res atomic.Value +} + +func (a *AllSucceededAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + if result != nil { + a.res.CompareAndSwap(nil, result) + } + + return nil +} + +func (a *AllSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error { + for _, res := range results { + err := a.Add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *AllSucceededAggregator) BatchSlice(results []AggregatorResErr) error { + for _, res := range results { + err := a.Add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *AllSucceededAggregator) Result() (interface{}, error) { + var err error + res, e := a.res.Load(), a.err.Load() + if e != nil { + err = e.(error) + } + + return res, err +} + +func (a *AllSucceededAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +// OneSucceededAggregator returns the first non-error reply, +// if all shards errored, returns any one of those errors. +type OneSucceededAggregator struct { + err atomic.Value + res atomic.Value +} + +func (a *OneSucceededAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + if result != nil { + a.res.CompareAndSwap(nil, result) + } + + return nil +} + +func (a *OneSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error { + for _, res := range results { + err := a.Add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err == nil { + return nil + } + } + + return nil +} + +func (a *OneSucceededAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *OneSucceededAggregator) BatchSlice(results []AggregatorResErr) error { + for _, res := range results { + err := a.Add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err == nil { + return nil + } + } + + return nil +} + +func (a *OneSucceededAggregator) Result() (interface{}, error) { + res, e := a.res.Load(), a.err.Load() + if res == nil { + return nil, e.(error) + } + + return res, nil +} + +// AggSumAggregator sums numeric replies from all shards. +type AggSumAggregator struct { + err atomic.Value + res uberAtomic.Float64 +} + +func (a *AggSumAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + } + + if result != nil { + val, err := toFloat64(result) + if err != nil { + a.err.CompareAndSwap(nil, err) + return err + } + a.res.Add(val) + } + + return nil +} + +func (a *AggSumAggregator) BatchAdd(results map[string]AggregatorResErr) error { + var sum int64 + + for _, res := range results { + if res.Err != nil { + return a.Add(res.Result, res.Err) + } + + intRes, err := toInt64(res.Result) + if err != nil { + return a.Add(nil, err) + } + + sum += intRes + } + + return a.Add(sum, nil) +} + +func (a *AggSumAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *AggSumAggregator) BatchSlice(results []AggregatorResErr) error { + var sum int64 + + for _, res := range results { + if res.Err != nil { + return a.Add(res.Result, res.Err) + } + + intRes, err := toInt64(res.Result) + if err != nil { + return a.Add(nil, err) + } + + sum += intRes + } + + return a.Add(sum, nil) +} + +func (a *AggSumAggregator) Result() (interface{}, error) { + res, err := a.res.Load(), a.err.Load() + if err != nil { + return nil, err.(error) + } + + return res, nil +} + +// AggMinAggregator returns the minimum numeric value from all shards. +type AggMinAggregator struct { + err atomic.Value + res *util.AtomicMin +} + +func (a *AggMinAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + floatVal, e := toFloat64(result) + if e != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + a.res.Value(floatVal) + + return nil +} + +func (a *AggMinAggregator) BatchAdd(results map[string]AggregatorResErr) error { + min := int64(math.MaxInt64) + + for _, res := range results { + if res.Err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + resInt, err := toInt64(res.Result) + if err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + if resInt < min { + min = resInt + } + + } + + return a.Add(min, nil) +} + +func (a *AggMinAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *AggMinAggregator) BatchSlice(results []AggregatorResErr) error { + min := float64(math.MaxFloat64) + + for _, res := range results { + if res.Err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + floatVal, err := toFloat64(res.Result) + if err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + if floatVal < min { + min = floatVal + } + + } + + return a.Add(min, nil) +} + +func (a *AggMinAggregator) Result() (interface{}, error) { + err := a.err.Load() + if err != nil { + return nil, err.(error) + } + + val, hasVal := a.res.Min() + if !hasVal { + return nil, ErrMinAggregation + } + return val, nil +} + +// AggMaxAggregator returns the maximum numeric value from all shards. +type AggMaxAggregator struct { + err atomic.Value + res *util.AtomicMax +} + +func (a *AggMaxAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + floatVal, e := toFloat64(result) + if e != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + a.res.Value(floatVal) + + return nil +} + +func (a *AggMaxAggregator) BatchAdd(results map[string]AggregatorResErr) error { + max := int64(math.MinInt64) + + for _, res := range results { + if res.Err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + resInt, err := toInt64(res.Result) + if err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + if resInt > max { + max = resInt + } + + } + + return a.Add(max, nil) +} + +func (a *AggMaxAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *AggMaxAggregator) BatchSlice(results []AggregatorResErr) error { + max := int64(math.MinInt64) + + for _, res := range results { + if res.Err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + resInt, err := toInt64(res.Result) + if err != nil { + _ = a.Add(nil, res.Err) + return nil + } + + if resInt > max { + max = resInt + } + + } + + return a.Add(max, nil) +} + +func (a *AggMaxAggregator) Result() (interface{}, error) { + err := a.err.Load() + if err != nil { + return nil, err.(error) + } + + val, hasVal := a.res.Max() + if !hasVal { + return nil, ErrMaxAggregation + } + return val, nil +} + +// AggLogicalAndAggregator performs logical AND on boolean values. +type AggLogicalAndAggregator struct { + err atomic.Value + res atomic.Bool + hasResult atomic.Bool +} + +func (a *AggLogicalAndAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + val, e := toBool(result) + if e != nil { + a.err.CompareAndSwap(nil, e) + return e + } + + // Atomic AND operation: if val is false, result is always false + if !val { + a.res.Store(false) + } + + a.hasResult.Store(true) + + return nil +} + +func (a *AggLogicalAndAggregator) BatchAdd(results map[string]AggregatorResErr) error { + result := true + + for _, res := range results { + if res.Err != nil { + return a.Add(nil, res.Err) + } + + boolRes, err := toBool(res.Result) + if err != nil { + return a.Add(nil, err) + } + + result = result && boolRes + } + + return a.Add(result, nil) +} + +func (a *AggLogicalAndAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *AggLogicalAndAggregator) BatchSlice(results []AggregatorResErr) error { + result := true + + for _, res := range results { + if res.Err != nil { + return a.Add(nil, res.Err) + } + + boolRes, err := toBool(res.Result) + if err != nil { + return a.Add(nil, err) + } + + result = result && boolRes + } + + return a.Add(result, nil) +} + +func (a *AggLogicalAndAggregator) Result() (interface{}, error) { + err := a.err.Load() + if err != nil { + return nil, err.(error) + } + + if !a.hasResult.Load() { + return nil, ErrAndAggregation + } + return a.res.Load(), nil +} + +// AggLogicalOrAggregator performs logical OR on boolean values. +type AggLogicalOrAggregator struct { + err atomic.Value + res atomic.Bool + hasResult atomic.Bool +} + +func (a *AggLogicalOrAggregator) Add(result interface{}, err error) error { + if err != nil { + a.err.CompareAndSwap(nil, err) + return nil + } + + val, e := toBool(result) + if e != nil { + a.err.CompareAndSwap(nil, e) + return e + } + + // Atomic OR operation: if val is true, result is always true + if val { + a.res.Store(true) + } + + a.hasResult.Store(true) + + return nil +} + +func (a *AggLogicalOrAggregator) BatchAdd(results map[string]AggregatorResErr) error { + result := false + + for _, res := range results { + if res.Err != nil { + return a.Add(nil, res.Err) + } + + boolRes, err := toBool(res.Result) + if err != nil { + return a.Add(nil, err) + } + + result = result || boolRes + } + + return a.Add(result, nil) +} + +func (a *AggLogicalOrAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *AggLogicalOrAggregator) BatchSlice(results []AggregatorResErr) error { + result := false + + for _, res := range results { + if res.Err != nil { + return a.Add(nil, res.Err) + } + + boolRes, err := toBool(res.Result) + if err != nil { + return a.Add(nil, err) + } + + result = result || boolRes + } + + return a.Add(result, nil) +} + +func (a *AggLogicalOrAggregator) Result() (interface{}, error) { + err := a.err.Load() + if err != nil { + return nil, err.(error) + } + + if !a.hasResult.Load() { + return nil, ErrOrAggregation + } + return a.res.Load(), nil +} + +func toInt64(val interface{}) (int64, error) { + if val == nil { + return 0, nil + } + switch v := val.(type) { + case int64: + return v, nil + case int: + return int64(v), nil + case int32: + return int64(v), nil + case float64: + if v != math.Trunc(v) { + return 0, fmt.Errorf("cannot convert float %f to int64", v) + } + return int64(v), nil + default: + return 0, fmt.Errorf("cannot convert %T to int64", val) + } +} + +func toFloat64(val interface{}) (float64, error) { + if val == nil { + return 0, nil + } + + switch v := val.(type) { + case float64: + return v, nil + case int: + return float64(v), nil + case int32: + return float64(v), nil + case int64: + return float64(v), nil + case float32: + return float64(v), nil + default: + return 0, fmt.Errorf("cannot convert %T to float64", val) + } +} + +func toBool(val interface{}) (bool, error) { + if val == nil { + return false, nil + } + switch v := val.(type) { + case bool: + return v, nil + case int64: + return v != 0, nil + case int: + return v != 0, nil + default: + return false, fmt.Errorf("cannot convert %T to bool", val) + } +} + +// DefaultKeylessAggregator collects all results in an array, order doesn't matter. +type DefaultKeylessAggregator struct { + mu sync.Mutex + results []interface{} + firstErr error +} + +func (a *DefaultKeylessAggregator) add(result interface{}, err error) error { + if err != nil && a.firstErr == nil { + a.firstErr = err + return nil + } + if err == nil { + a.results = append(a.results, result) + } + return nil +} + +func (a *DefaultKeylessAggregator) Add(result interface{}, err error) error { + a.mu.Lock() + defer a.mu.Unlock() + + return a.add(result, err) +} + +func (a *DefaultKeylessAggregator) BatchAdd(results map[string]AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *DefaultKeylessAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *DefaultKeylessAggregator) BatchSlice(results []AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *DefaultKeylessAggregator) Result() (interface{}, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.firstErr != nil { + return nil, a.firstErr + } + return a.results, nil +} + +// DefaultKeyedAggregator reassembles replies in the exact key order of the original request. +type DefaultKeyedAggregator struct { + mu sync.Mutex + results map[string]interface{} + keyOrder []string + firstErr error +} + +func NewDefaultKeyedAggregator(keyOrder []string) *DefaultKeyedAggregator { + return &DefaultKeyedAggregator{ + results: make(map[string]interface{}), + keyOrder: keyOrder, + } +} + +func (a *DefaultKeyedAggregator) add(result interface{}, err error) error { + if err != nil && a.firstErr == nil { + a.firstErr = err + return nil + } + // For non-keyed Add, just collect the result without ordering + if err == nil { + a.results["__default__"] = result + } + return nil +} + +func (a *DefaultKeyedAggregator) Add(result interface{}, err error) error { + a.mu.Lock() + defer a.mu.Unlock() + + return a.add(result, err) +} + +func (a *DefaultKeyedAggregator) BatchAdd(results map[string]AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *DefaultKeyedAggregator) addWithKey(key string, result interface{}, err error) error { + if err != nil && a.firstErr == nil { + a.firstErr = err + return nil + } + if err == nil { + a.results[key] = result + } + return nil +} + +func (a *DefaultKeyedAggregator) AddWithKey(key string, result interface{}, err error) error { + a.mu.Lock() + defer a.mu.Unlock() + + return a.addWithKey(key, result, err) +} + +func (a *DefaultKeyedAggregator) BatchAddWithKeyOrder(results map[string]AggregatorResErr, keyOrder []string) error { + a.mu.Lock() + defer a.mu.Unlock() + + a.keyOrder = keyOrder + for key, res := range results { + err := a.addWithKey(key, res.Result, res.Err) + if err != nil { + return nil + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *DefaultKeyedAggregator) SetKeyOrder(keyOrder []string) { + a.mu.Lock() + defer a.mu.Unlock() + a.keyOrder = keyOrder +} + +func (a *DefaultKeyedAggregator) BatchSlice(results []AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *DefaultKeyedAggregator) Result() (interface{}, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.firstErr != nil { + return nil, a.firstErr + } + + // If no explicit key order is set, return results in any order + if len(a.keyOrder) == 0 { + orderedResults := make([]interface{}, 0, len(a.results)) + for _, result := range a.results { + orderedResults = append(orderedResults, result) + } + return orderedResults, nil + } + + // Return results in the exact key order + orderedResults := make([]interface{}, len(a.keyOrder)) + for i, key := range a.keyOrder { + if result, exists := a.results[key]; exists { + orderedResults[i] = result + } + } + return orderedResults, nil +} + +// SpecialAggregator provides a registry for command-specific aggregation logic. +type SpecialAggregator struct { + mu sync.Mutex + aggregatorFunc func([]interface{}, []error) (interface{}, error) + results []interface{} + errors []error +} + +func (a *SpecialAggregator) add(result interface{}, err error) error { + a.results = append(a.results, result) + a.errors = append(a.errors, err) + return nil +} + +func (a *SpecialAggregator) Add(result interface{}, err error) error { + a.mu.Lock() + defer a.mu.Unlock() + + return a.add(result, err) +} + +func (a *SpecialAggregator) BatchAdd(results map[string]AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *SpecialAggregator) AddWithKey(key string, result interface{}, err error) error { + return a.Add(result, err) +} + +func (a *SpecialAggregator) BatchSlice(results []AggregatorResErr) error { + a.mu.Lock() + defer a.mu.Unlock() + + for _, res := range results { + err := a.add(res.Result, res.Err) + if err != nil { + return err + } + + if res.Err != nil { + return nil + } + } + + return nil +} + +func (a *SpecialAggregator) Result() (interface{}, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.aggregatorFunc != nil { + return a.aggregatorFunc(a.results, a.errors) + } + // Default behavior: return first non-error result or first error + for i, err := range a.errors { + if err == nil { + return a.results[i], nil + } + } + if len(a.errors) > 0 { + return nil, a.errors[0] + } + return nil, nil +} + +// SpecialAggregatorRegistry holds custom aggregation functions for specific commands. +var SpecialAggregatorRegistry = make(map[string]func([]interface{}, []error) (interface{}, error)) + +// RegisterSpecialAggregator registers a custom aggregation function for a command. +func RegisterSpecialAggregator(cmdName string, fn func([]interface{}, []error) (interface{}, error)) { + SpecialAggregatorRegistry[cmdName] = fn +} + +// NewSpecialAggregator creates a special aggregator with command-specific logic if available. +func NewSpecialAggregator(cmdName string) *SpecialAggregator { + agg := &SpecialAggregator{} + if fn, exists := SpecialAggregatorRegistry[cmdName]; exists { + agg.aggregatorFunc = fn + } + return agg +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go b/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go new file mode 100644 index 00000000000..7f784b50618 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go @@ -0,0 +1,144 @@ +package routing + +import ( + "fmt" + "strings" +) + +type RequestPolicy uint8 + +const ( + ReqDefault RequestPolicy = iota + + ReqAllNodes + + ReqAllShards + + ReqMultiShard + + ReqSpecial +) + +const ( + ReadOnlyCMD string = "readonly" +) + +func (p RequestPolicy) String() string { + switch p { + case ReqDefault: + return "default" + case ReqAllNodes: + return "all_nodes" + case ReqAllShards: + return "all_shards" + case ReqMultiShard: + return "multi_shard" + case ReqSpecial: + return "special" + default: + return fmt.Sprintf("unknown_request_policy(%d)", p) + } +} + +func ParseRequestPolicy(raw string) (RequestPolicy, error) { + switch strings.ToLower(raw) { + case "", "default", "none": + return ReqDefault, nil + case "all_nodes": + return ReqAllNodes, nil + case "all_shards": + return ReqAllShards, nil + case "multi_shard": + return ReqMultiShard, nil + case "special": + return ReqSpecial, nil + default: + return ReqDefault, fmt.Errorf("routing: unknown request_policy %q", raw) + } +} + +type ResponsePolicy uint8 + +const ( + RespDefaultKeyless ResponsePolicy = iota + RespDefaultHashSlot + RespAllSucceeded + RespOneSucceeded + RespAggSum + RespAggMin + RespAggMax + RespAggLogicalAnd + RespAggLogicalOr + RespSpecial +) + +func (p ResponsePolicy) String() string { + switch p { + case RespDefaultKeyless: + return "default(keyless)" + case RespDefaultHashSlot: + return "default(hashslot)" + case RespAllSucceeded: + return "all_succeeded" + case RespOneSucceeded: + return "one_succeeded" + case RespAggSum: + return "agg_sum" + case RespAggMin: + return "agg_min" + case RespAggMax: + return "agg_max" + case RespAggLogicalAnd: + return "agg_logical_and" + case RespAggLogicalOr: + return "agg_logical_or" + case RespSpecial: + return "special" + default: + return "all_succeeded" + } +} + +func ParseResponsePolicy(raw string) (ResponsePolicy, error) { + switch strings.ToLower(raw) { + case "default(keyless)": + return RespDefaultKeyless, nil + case "default(hashslot)": + return RespDefaultHashSlot, nil + case "all_succeeded": + return RespAllSucceeded, nil + case "one_succeeded": + return RespOneSucceeded, nil + case "agg_sum": + return RespAggSum, nil + case "agg_min": + return RespAggMin, nil + case "agg_max": + return RespAggMax, nil + case "agg_logical_and": + return RespAggLogicalAnd, nil + case "agg_logical_or": + return RespAggLogicalOr, nil + case "special": + return RespSpecial, nil + default: + return RespDefaultKeyless, fmt.Errorf("routing: unknown response_policy %q", raw) + } +} + +type CommandPolicy struct { + Request RequestPolicy + Response ResponsePolicy + // Tips that are not request_policy or response_policy + // e.g nondeterministic_output, nondeterministic_output_order. + Tips map[string]string +} + +func (p *CommandPolicy) CanBeUsedInPipeline() bool { + return p.Request != ReqAllNodes && p.Request != ReqAllShards && p.Request != ReqMultiShard +} + +func (p *CommandPolicy) IsReadOnly() bool { + _, readOnly := p.Tips[ReadOnlyCMD] + return readOnly +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go b/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go new file mode 100644 index 00000000000..8e6228dd205 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go @@ -0,0 +1,57 @@ +package routing + +import ( + "math/rand" + "sync/atomic" +) + +// ShardPicker chooses “one arbitrary shard” when the request_policy is +// ReqDefault and the command has no keys. +type ShardPicker interface { + Next(total int) int // returns an index in [0,total) +} + +// StaticShardPicker always returns the same shard index. +type StaticShardPicker struct { + index int +} + +func NewStaticShardPicker(index int) *StaticShardPicker { + return &StaticShardPicker{index: index} +} + +func (p *StaticShardPicker) Next(total int) int { + if total == 0 || p.index >= total { + return 0 + } + return p.index +} + +/*─────────────────────────────── + Round-robin (default) +────────────────────────────────*/ + +type RoundRobinPicker struct { + cnt atomic.Uint32 +} + +func (p *RoundRobinPicker) Next(total int) int { + if total == 0 { + return 0 + } + i := p.cnt.Add(1) + return int(i-1) % total +} + +/*─────────────────────────────── + Random +────────────────────────────────*/ + +type RandomPicker struct{} + +func (RandomPicker) Next(total int) int { + if total == 0 { + return 0 + } + return rand.Intn(total) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/semaphore.go b/vendor/github.com/redis/go-redis/v9/internal/semaphore.go new file mode 100644 index 00000000000..a7f40466ccb --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/semaphore.go @@ -0,0 +1,193 @@ +package internal + +import ( + "context" + "sync" + "time" +) + +var semTimers = sync.Pool{ + New: func() interface{} { + t := time.NewTimer(time.Hour) + t.Stop() + return t + }, +} + +// FastSemaphore is a channel-based semaphore optimized for performance. +// It uses a fast path that avoids timer allocation when tokens are available. +// The channel is pre-filled with tokens: Acquire = receive, Release = send. +// Closing the semaphore unblocks all waiting goroutines. +// +// Performance: ~30 ns/op with zero allocations on fast path. +// Fairness: Eventual fairness (no starvation) but not strict FIFO. +type FastSemaphore struct { + tokens chan struct{} + max int32 +} + +// NewFastSemaphore creates a new fast semaphore with the given capacity. +func NewFastSemaphore(capacity int32) *FastSemaphore { + ch := make(chan struct{}, capacity) + // Pre-fill with tokens + for i := int32(0); i < capacity; i++ { + ch <- struct{}{} + } + return &FastSemaphore{ + tokens: ch, + max: capacity, + } +} + +// TryAcquire attempts to acquire a token without blocking. +// Returns true if successful, false if no tokens available. +func (s *FastSemaphore) TryAcquire() bool { + select { + case <-s.tokens: + return true + default: + return false + } +} + +// Acquire acquires a token, blocking if necessary until one is available. +// Returns an error if the context is cancelled or the timeout expires. +// Uses a fast path to avoid timer allocation when tokens are immediately available. +func (s *FastSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error { + // Check context first + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Try fast path first (no timer needed) + select { + case <-s.tokens: + return nil + default: + } + + // Slow path: need to wait with timeout + timer := semTimers.Get().(*time.Timer) + defer semTimers.Put(timer) + timer.Reset(timeout) + + select { + case <-s.tokens: + if !timer.Stop() { + <-timer.C + } + return nil + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ctx.Err() + case <-timer.C: + return timeoutErr + } +} + +// AcquireBlocking acquires a token, blocking indefinitely until one is available. +func (s *FastSemaphore) AcquireBlocking() { + <-s.tokens +} + +// Release releases a token back to the semaphore. +func (s *FastSemaphore) Release() { + s.tokens <- struct{}{} +} + +// Close closes the semaphore, unblocking all waiting goroutines. +// After close, all Acquire calls will receive a closed channel signal. +func (s *FastSemaphore) Close() { + close(s.tokens) +} + +// Len returns the current number of acquired tokens. +func (s *FastSemaphore) Len() int32 { + return s.max - int32(len(s.tokens)) +} + +// FIFOSemaphore is a channel-based semaphore with strict FIFO ordering. +// Unlike FastSemaphore, this guarantees that threads are served in the exact order they call Acquire(). +// The channel is pre-filled with tokens: Acquire = receive, Release = send. +// Closing the semaphore unblocks all waiting goroutines. +// +// Performance: ~115 ns/op with zero allocations (slower than FastSemaphore due to timer allocation). +// Fairness: Strict FIFO ordering guaranteed by Go runtime. +type FIFOSemaphore struct { + tokens chan struct{} + max int32 +} + +// NewFIFOSemaphore creates a new FIFO semaphore with the given capacity. +func NewFIFOSemaphore(capacity int32) *FIFOSemaphore { + ch := make(chan struct{}, capacity) + // Pre-fill with tokens + for i := int32(0); i < capacity; i++ { + ch <- struct{}{} + } + return &FIFOSemaphore{ + tokens: ch, + max: capacity, + } +} + +// TryAcquire attempts to acquire a token without blocking. +// Returns true if successful, false if no tokens available. +func (s *FIFOSemaphore) TryAcquire() bool { + select { + case <-s.tokens: + return true + default: + return false + } +} + +// Acquire acquires a token, blocking if necessary until one is available. +// Returns an error if the context is cancelled or the timeout expires. +// Always uses timer to guarantee FIFO ordering (no fast path). +func (s *FIFOSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error { + // No fast path - always use timer to guarantee FIFO + timer := semTimers.Get().(*time.Timer) + defer semTimers.Put(timer) + timer.Reset(timeout) + + select { + case <-s.tokens: + if !timer.Stop() { + <-timer.C + } + return nil + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ctx.Err() + case <-timer.C: + return timeoutErr + } +} + +// AcquireBlocking acquires a token, blocking indefinitely until one is available. +func (s *FIFOSemaphore) AcquireBlocking() { + <-s.tokens +} + +// Release releases a token back to the semaphore. +func (s *FIFOSemaphore) Release() { + s.tokens <- struct{}{} +} + +// Close closes the semaphore, unblocking all waiting goroutines. +// After close, all Acquire calls will receive a closed channel signal. +func (s *FIFOSemaphore) Close() { + close(s.tokens) +} + +// Len returns the current number of acquired tokens. +func (s *FIFOSemaphore) Len() int32 { + return s.max - int32(len(s.tokens)) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/util.go b/vendor/github.com/redis/go-redis/v9/internal/util.go index f77775ff405..00516075bdc 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/util.go +++ b/vendor/github.com/redis/go-redis/v9/internal/util.go @@ -2,6 +2,7 @@ package internal import ( "context" + "math" "net" "strconv" "strings" @@ -10,6 +11,29 @@ import ( "github.com/redis/go-redis/v9/internal/util" ) +// String representations of special float values. +// Values are lowercase for consistency with Redis RESP2 protocol responses. +const ( + NaN = "nan" // Not a Number + Inf = "inf" // Positive infinity + NInf = "-inf" // Negative infinity +) + +// FormatFloat formats a float64 to string, normalizing special values +// (NaN, Inf) to lowercase for consistency with Redis RESP2 protocol. +func FormatFloat(f float64) string { + switch { + case math.IsNaN(f): + return NaN + case math.IsInf(f, 1): + return Inf + case math.IsInf(f, -1): + return NInf + default: + return strconv.FormatFloat(f, 'f', -1, 64) + } +} + func Sleep(ctx context.Context, dur time.Duration) error { t := time.NewTimer(dur) defer t.Stop() diff --git a/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go b/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go new file mode 100644 index 00000000000..6c621ba850c --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go @@ -0,0 +1,97 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License + +Modified by htemelski-redis +Removed the treshold, adapted it to work with float64 +*/ + +package util + +import ( + "math" + + "go.uber.org/atomic" +) + +// AtomicMax is a thread-safe max container +// - hasValue indicator true if a value was equal to or greater than threshold +// - optional threshold for minimum accepted max value +// - if threshold is not used, initialization-free +// - — +// - wait-free CompareAndSwap mechanic +type AtomicMax struct { + + // value is current max + value atomic.Float64 + // whether [AtomicMax.Value] has been invoked + // with value equal or greater to threshold + hasValue atomic.Bool +} + +// NewAtomicMax returns a thread-safe max container +// - if threshold is not used, AtomicMax is initialization-free +func NewAtomicMax() (atomicMax *AtomicMax) { + m := AtomicMax{} + m.value.Store((-math.MaxFloat64)) + return &m +} + +// Value updates the container with a possible max value +// - isNewMax is true if: +// - — value is equal to or greater than any threshold and +// - — invocation recorded the first 0 or +// - — a new max +// - upon return, Max and Max1 are guaranteed to reflect the invocation +// - the return order of concurrent Value invocations is not guaranteed +// - Thread-safe +func (m *AtomicMax) Value(value float64) (isNewMax bool) { + // -math.MaxFloat64 as max case + var hasValue0 = m.hasValue.Load() + if value == (-math.MaxFloat64) { + if !hasValue0 { + isNewMax = m.hasValue.CompareAndSwap(false, true) + } + return // -math.MaxFloat64 as max: isNewMax true for first 0 writer + } + + // check against present value + var current = m.value.Load() + if isNewMax = value > current; !isNewMax { + return // not a new max return: isNewMax false + } + + // store the new max + for { + + // try to write value to *max + if isNewMax = m.value.CompareAndSwap(current, value); isNewMax { + if !hasValue0 { + // may be rarely written multiple times + // still faster than CompareAndSwap + m.hasValue.Store(true) + } + return // new max written return: isNewMax true + } + if current = m.value.Load(); current >= value { + return // no longer a need to write return: isNewMax false + } + } +} + +// Max returns current max and value-present flag +// - hasValue true indicates that value reflects a Value invocation +// - hasValue false: value is zero-value +// - Thread-safe +func (m *AtomicMax) Max() (value float64, hasValue bool) { + if hasValue = m.hasValue.Load(); !hasValue { + return + } + value = m.value.Load() + return +} + +// Max1 returns current maximum whether zero-value or set by Value +// - threshold is ignored +// - Thread-safe +func (m *AtomicMax) Max1() (value float64) { return m.value.Load() } diff --git a/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go b/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go new file mode 100644 index 00000000000..e33d29cc21c --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go @@ -0,0 +1,96 @@ +package util + +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License + +Modified by htemelski-redis +Adapted from the modified atomic_max, but with inverted logic +*/ + +import ( + "math" + + "go.uber.org/atomic" +) + +// AtomicMin is a thread-safe Min container +// - hasValue indicator true if a value was equal to or greater than threshold +// - optional threshold for minimum accepted Min value +// - — +// - wait-free CompareAndSwap mechanic +type AtomicMin struct { + + // value is current Min + value atomic.Float64 + // whether [AtomicMin.Value] has been invoked + // with value equal or greater to threshold + hasValue atomic.Bool +} + +// NewAtomicMin returns a thread-safe Min container +// - if threshold is not used, AtomicMin is initialization-free +func NewAtomicMin() (atomicMin *AtomicMin) { + m := AtomicMin{} + m.value.Store(math.MaxFloat64) + return &m +} + +// Value updates the container with a possible Min value +// - isNewMin is true if: +// - — value is equal to or greater than any threshold and +// - — invocation recorded the first 0 or +// - — a new Min +// - upon return, Min and Min1 are guaranteed to reflect the invocation +// - the return order of concurrent Value invocations is not guaranteed +// - Thread-safe +func (m *AtomicMin) Value(value float64) (isNewMin bool) { + // math.MaxFloat64 as Min case + var hasValue0 = m.hasValue.Load() + if value == math.MaxFloat64 { + if !hasValue0 { + isNewMin = m.hasValue.CompareAndSwap(false, true) + } + return // math.MaxFloat64 as Min: isNewMin true for first 0 writer + } + + // check against present value + var current = m.value.Load() + if isNewMin = value < current; !isNewMin { + return // not a new Min return: isNewMin false + } + + // store the new Min + for { + + // try to write value to *Min + if isNewMin = m.value.CompareAndSwap(current, value); isNewMin { + if !hasValue0 { + // may be rarely written multiple times + // still faster than CompareAndSwap + m.hasValue.Store(true) + } + return // new Min written return: isNewMin true + } + if current = m.value.Load(); current <= value { + return // no longer a need to write return: isNewMin false + } + } +} + +// Min returns current min and value-present flag +// - hasValue true indicates that value reflects a Value invocation +// - hasValue false: value is zero-value +// - Thread-safe +func (m *AtomicMin) Min() (value float64, hasValue bool) { + if hasValue = m.hasValue.Load(); !hasValue { + return + } + value = m.value.Load() + return +} + +// Min1 returns current Minimum whether zero-value or set by Value +// - threshold is ignored +// - Thread-safe +func (m *AtomicMin) Min1() (value float64) { return m.value.Load() } diff --git a/vendor/github.com/redis/go-redis/v9/internal/util/convert.go b/vendor/github.com/redis/go-redis/v9/internal/util/convert.go index d326d50d355..b743a4f0eb3 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/util/convert.go +++ b/vendor/github.com/redis/go-redis/v9/internal/util/convert.go @@ -28,3 +28,14 @@ func MustParseFloat(s string) float64 { } return f } + +// SafeIntToInt32 safely converts an int to int32, returning an error if overflow would occur. +func SafeIntToInt32(value int, fieldName string) (int32, error) { + if value > math.MaxInt32 { + return 0, fmt.Errorf("redis: %s value %d exceeds maximum allowed value %d", fieldName, value, math.MaxInt32) + } + if value < math.MinInt32 { + return 0, fmt.Errorf("redis: %s value %d is below minimum allowed value %d", fieldName, value, math.MinInt32) + } + return int32(value), nil +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go b/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go index cbcd2cc0902..f4c3c3f33cd 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go +++ b/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go @@ -8,15 +8,10 @@ import ( // BytesToString converts byte slice to string. func BytesToString(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + return unsafe.String(unsafe.SliceData(b), len(b)) } // StringToBytes converts string to byte slice. func StringToBytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer( - &struct { - string - Cap int - }{s, len(s)}, - )) + return unsafe.Slice(unsafe.StringData(s), len(s)) } diff --git a/vendor/github.com/redis/go-redis/v9/json.go b/vendor/github.com/redis/go-redis/v9/json.go index b3cadf4b795..2bcad0b7974 100644 --- a/vendor/github.com/redis/go-redis/v9/json.go +++ b/vendor/github.com/redis/go-redis/v9/json.go @@ -35,6 +35,7 @@ type JSONCmdable interface { JSONObjLen(ctx context.Context, key, path string) *IntPointerSliceCmd JSONSet(ctx context.Context, key, path string, value interface{}) *StatusCmd JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd + JSONSetWithArgs(ctx context.Context, key, path string, value interface{}, options *JSONSetArgsOptions) *StatusCmd JSONStrAppend(ctx context.Context, key, path, value string) *IntPointerSliceCmd JSONStrLen(ctx context.Context, key, path string) *IntPointerSliceCmd JSONToggle(ctx context.Context, key, path string) *IntPointerSliceCmd @@ -57,6 +58,25 @@ type JSONArrTrimArgs struct { Stop *int } +// FPHAType is the floating-point type used for storing FP homogeneous arrays +// in JSON.SET (Redis 8.8+). +type FPHAType string + +const ( + FPHATypeBF16 FPHAType = "BF16" + FPHATypeFP16 FPHAType = "FP16" + FPHATypeFP32 FPHAType = "FP32" + FPHATypeFP64 FPHAType = "FP64" +) + +// JSONSetArgsOptions are the optional arguments for JSONSetWithArgs. +// Mode is "NX" or "XX" (case-insensitive). FPHA, when set, forces Redis to +// store all FP homogeneous arrays using the specified floating-point type. +type JSONSetArgsOptions struct { + Mode string + FPHA FPHAType +} + type JSONCmd struct { baseCmd val string @@ -68,8 +88,9 @@ var _ Cmder = (*JSONCmd)(nil) func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd { return &JSONCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeJSON, }, } } @@ -82,6 +103,7 @@ func (cmd *JSONCmd) SetVal(val string) { cmd.val = val } +// Val returns the result of the JSON.GET command as a string. func (cmd *JSONCmd) Val() string { if len(cmd.val) == 0 && cmd.expanded != nil { val, err := json.Marshal(cmd.expanded) @@ -100,6 +122,7 @@ func (cmd *JSONCmd) Result() (string, error) { return cmd.Val(), cmd.Err() } +// Expanded returns the result of the JSON.GET command as unmarshalled JSON. func (cmd *JSONCmd) Expanded() (interface{}, error) { if len(cmd.val) != 0 && cmd.expanded == nil { err := json.Unmarshal([]byte(cmd.val), &cmd.expanded) @@ -113,11 +136,17 @@ func (cmd *JSONCmd) Expanded() (interface{}, error) { func (cmd *JSONCmd) readReply(rd *proto.Reader) error { // nil response from JSON.(M)GET (cmd.baseCmd.err will be "redis: nil") + // This happens when the key doesn't exist if cmd.baseCmd.Err() == Nil { cmd.val = "" return Nil } + // Handle other base command errors + if cmd.baseCmd.Err() != nil { + return cmd.baseCmd.Err() + } + if readType, err := rd.PeekReplyType(); err != nil { return err } else if readType == proto.RespArray { @@ -127,6 +156,13 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return err } + // Empty array means no results found for JSON path, but key exists + // This should return "[]", not an error + if size == 0 { + cmd.val = "[]" + return nil + } + expanded := make([]interface{}, size) for i := 0; i < size; i++ { @@ -141,6 +177,7 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return err } else if str == "" || err == Nil { cmd.val = "" + return Nil } else { cmd.val = str } @@ -149,6 +186,14 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *JSONCmd) Clone() Cmder { + return &JSONCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + expanded: cmd.expanded, // interface{} can be shared as it should be immutable after parsing + } +} + // ------------------------------------------- type JSONSliceCmd struct { @@ -159,8 +204,9 @@ type JSONSliceCmd struct { func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd { return &JSONSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeJSONSlice, }, } } @@ -217,6 +263,18 @@ func (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *JSONSliceCmd) Clone() Cmder { + var val []interface{} + if cmd.val != nil { + val = make([]interface{}, len(cmd.val)) + copy(val, cmd.val) + } + return &JSONSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + /******************************************************************************* * * IntPointerSliceCmd @@ -233,8 +291,9 @@ type IntPointerSliceCmd struct { func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd { return &IntPointerSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeIntPointerSlice, }, } } @@ -274,6 +333,18 @@ func (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error { return nil } +func (cmd *IntPointerSliceCmd) Clone() Cmder { + var val []*int64 + if cmd.val != nil { + val = make([]*int64, len(cmd.val)) + copy(val, cmd.val) + } + return &IntPointerSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ // JSONArrAppend adds the provided JSON values to the end of the array at the given path. @@ -533,6 +604,15 @@ func (c cmdable) JSONSet(ctx context.Context, key, path string, value interface{ // the argument is a string or []byte when we assume that it can be passed directly as JSON. // For more information, see https://redis.io/commands/json.set func (c cmdable) JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd { + return c.JSONSetWithArgs(ctx, key, path, value, &JSONSetArgsOptions{Mode: mode}) +} + +// JSONSetWithArgs sets the JSON value at the given path in the given key with optional arguments +// for setting mode (NX/XX) and the FPHA (Floating-Point Homogeneous Array) type used for storing +// FP arrays. The value must be something that can be marshaled to JSON (using encoding/JSON) unless +// the argument is a string or []byte when we assume that it can be passed directly as JSON. +// For more information, see https://redis.io/commands/json.set +func (c cmdable) JSONSetWithArgs(ctx context.Context, key, path string, value interface{}, options *JSONSetArgsOptions) *StatusCmd { var bytes []byte var err error switch v := value.(type) { @@ -544,13 +624,17 @@ func (c cmdable) JSONSetMode(ctx context.Context, key, path string, value interf bytes, err = json.Marshal(v) } args := []interface{}{"JSON.SET", key, path, util.BytesToString(bytes)} - if mode != "" { - switch strings.ToUpper(mode) { - case "XX", "NX": - args = append(args, strings.ToUpper(mode)) - - default: - panic("redis: JSON.SET mode must be NX or XX") + if options != nil { + if options.Mode != "" { + switch strings.ToUpper(options.Mode) { + case "XX", "NX": + args = append(args, strings.ToUpper(options.Mode)) + default: + panic("redis: JSON.SET mode must be NX or XX") + } + } + if options.FPHA != "" { + args = append(args, "FPHA", string(options.FPHA)) } } cmd := NewStatusCmd(ctx, args...) diff --git a/vendor/github.com/redis/go-redis/v9/list_commands.go b/vendor/github.com/redis/go-redis/v9/list_commands.go index 24a0de08100..9d9e16c65f1 100644 --- a/vendor/github.com/redis/go-redis/v9/list_commands.go +++ b/vendor/github.com/redis/go-redis/v9/list_commands.go @@ -77,6 +77,10 @@ func (c cmdable) BRPop(ctx context.Context, timeout time.Duration, keys ...strin return cmd } +// BRPopLPush pops an element from a list, pushes it to another list and returns it. +// Blocks until an element is available or timeout is reached. +// +// Deprecated: Use BLMove with RIGHT and LEFT arguments instead as of Redis 6.2.0. func (c cmdable) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd { cmd := NewStringCmd( ctx, @@ -247,6 +251,10 @@ func (c cmdable) RPopCount(ctx context.Context, key string, count int) *StringSl return cmd } +// RPopLPush atomically returns and removes the last element of the source list, +// and pushes the element as the first element of the destination list. +// +// Deprecated: Use LMove with RIGHT and LEFT arguments instead as of Redis 6.2.0. func (c cmdable) RPopLPush(ctx context.Context, source, destination string) *StringCmd { cmd := NewStringCmd(ctx, "rpoplpush", source, destination) _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md b/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md new file mode 100644 index 00000000000..03bbd39180d --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md @@ -0,0 +1,235 @@ +# Maintenance Notifications - FEATURES + +## Overview + +The Maintenance Notifications feature enables seamless Redis connection handoffs during cluster maintenance operations without dropping active connections. This feature leverages Redis RESP3 push notifications to provide zero-downtime maintenance for Redis Enterprise and compatible Redis deployments. + +## Important + +Using Maintenance Notifications may affect the read and write timeouts by relaxing them during maintenance operations. +This is necessary to prevent false failures due to increased latency during handoffs. The relaxed timeouts are automatically applied and removed as needed. + +## Key Features + +### Seamless Connection Handoffs +- **Zero-Downtime Maintenance**: Automatically handles connection transitions during cluster operations +- **Active Operation Preservation**: Transfers in-flight operations to new connections without interruption +- **Graceful Degradation**: Falls back to standard reconnection if handoff fails + +### Push Notification Support +Supports all Redis Enterprise maintenance notification types: +- **MOVING** - Slot moving to a new node +- **MIGRATING** - Slot in migration state +- **MIGRATED** - Migration completed +- **FAILING_OVER** - Node failing over +- **FAILED_OVER** - Failover completed + +### Circuit Breaker Pattern +- **Endpoint-Specific Failure Tracking**: Prevents repeated connection attempts to failing endpoints +- **Automatic Recovery Testing**: Half-open state allows gradual recovery validation +- **Configurable Thresholds**: Customize failure thresholds and reset timeouts + +### Flexible Configuration +- **Auto-Detection Mode**: Automatically detects server support for maintenance notifications +- **Multiple Endpoint Types**: Support for internal/external IP/FQDN endpoint resolution +- **Auto-Scaling Workers**: Automatically sizes worker pool based on connection pool size +- **Timeout Management**: Separate timeouts for relaxed (during maintenance) and normal operations + +### Extensible Hook System +- **Pre/Post Processing Hooks**: Monitor and customize notification handling +- **Built-in Hooks**: Logging and metrics collection hooks included +- **Custom Hook Support**: Implement custom business logic around maintenance events + +### Comprehensive Monitoring +- **Metrics Collection**: Track notification counts, processing times, and error rates +- **Circuit Breaker Stats**: Monitor endpoint health and circuit breaker states +- **Operation Tracking**: Track active handoff operations and their lifecycle + +## Architecture Highlights + +### Event-Driven Handoff System +- **Asynchronous Processing**: Non-blocking handoff operations using worker pool pattern +- **Queue-Based Architecture**: Configurable queue size with auto-scaling support +- **Retry Mechanism**: Configurable retry attempts with exponential backoff + +### Connection Pool Integration +- **Pool Hook Interface**: Seamless integration with go-redis connection pool +- **Connection State Management**: Atomic flags for connection usability tracking +- **Graceful Shutdown**: Ensures all in-flight handoffs complete before shutdown + +### Thread-Safe Design +- **Lock-Free Operations**: Atomic operations for high-performance state tracking +- **Concurrent-Safe Maps**: sync.Map for tracking active operations +- **Minimal Lock Contention**: Read-write locks only where necessary + +## Configuration Options + +### Operation Modes +- **`ModeDisabled`**: Maintenance notifications completely disabled +- **`ModeEnabled`**: Forcefully enabled (fails if server doesn't support) +- **`ModeAuto`**: Auto-detect server support (recommended default) + +### Endpoint Types +- **`EndpointTypeAuto`**: Auto-detect based on current connection +- **`EndpointTypeInternalIP`**: Use internal IP addresses +- **`EndpointTypeInternalFQDN`**: Use internal fully qualified domain names +- **`EndpointTypeExternalIP`**: Use external IP addresses +- **`EndpointTypeExternalFQDN`**: Use external fully qualified domain names +- **`EndpointTypeNone`**: No endpoint (reconnect with current configuration) + +### Timeout Configuration +- **`RelaxedTimeout`**: Extended timeout during maintenance operations (default: 10s) +- **`HandoffTimeout`**: Maximum time for handoff completion (default: 15s) +- **`PostHandoffRelaxedDuration`**: Relaxed period after handoff (default: 2×RelaxedTimeout) + +### Worker Pool Configuration +- **`MaxWorkers`**: Maximum concurrent handoff workers (auto-calculated if 0) +- **`HandoffQueueSize`**: Handoff queue capacity (auto-calculated if 0) +- **`MaxHandoffRetries`**: Maximum retry attempts for failed handoffs (default: 3) + +### Circuit Breaker Configuration +- **`CircuitBreakerFailureThreshold`**: Failures before opening circuit (default: 5) +- **`CircuitBreakerResetTimeout`**: Time before testing recovery (default: 60s) +- **`CircuitBreakerMaxRequests`**: Max requests in half-open state (default: 3) + +## Auto-Scaling Formulas + +### Worker Pool Sizing +When `MaxWorkers = 0` (auto-calculate): +``` +MaxWorkers = min(PoolSize/2, max(10, PoolSize/3)) +``` + +### Queue Sizing +When `HandoffQueueSize = 0` (auto-calculate): +``` +QueueSize = max(20 × MaxWorkers, PoolSize) +Capped by: min(MaxActiveConns + 1, 5 × PoolSize) +``` + +### Examples +- **Pool Size 100**: 33 workers, 660 queue (capped at 500) +- **Pool Size 100 + MaxActiveConns 150**: 33 workers, 151 queue +- **Pool Size 50**: 16 workers, 320 queue (capped at 250) + +## Performance Characteristics + +### Throughput +- **Non-Blocking Handoffs**: Client operations continue during handoffs +- **Concurrent Processing**: Multiple handoffs processed in parallel +- **Minimal Overhead**: Lock-free atomic operations for state tracking + +### Latency +- **Relaxed Timeouts**: Extended timeouts during maintenance prevent false failures +- **Fast Path**: Connections not undergoing handoff have zero overhead +- **Graceful Degradation**: Failed handoffs fall back to standard reconnection + +### Resource Usage +- **Memory Efficient**: Bounded queue sizes prevent memory exhaustion +- **Worker Pool**: Fixed worker count prevents goroutine explosion +- **Connection Reuse**: Handoff reuses existing connection objects + +## Testing + +### Unit Tests +- Comprehensive unit test coverage for all components +- Mock-based testing for isolation +- Concurrent operation testing + +### Integration Tests +- Pool integration tests with real connection handoffs +- Circuit breaker behavior validation +- Hook system integration testing + +### E2E Tests +- Real Redis Enterprise cluster testing +- Multiple scenario coverage (timeouts, endpoint types, stress tests) +- Fault injection testing +- TLS configuration testing + +## Compatibility + +### Requirements +- **Redis Protocol**: RESP3 required for push notifications +- **Redis Version**: Redis Enterprise or compatible Redis with maintenance notifications +- **Go Version**: Go 1.18+ (uses generics and atomic types) + +### Client Support +#### Currently Supported +- **Standalone Client** (`redis.NewClient`) - Full support for MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications +- **Cluster Client** (`redis.NewClusterClient`) - Support for SMIGRATING and SMIGRATED notifications for hitless slot migrations + +#### Will Not Support +- **Failover Client** (no planned support) +- **Ring Client** (no planned support) + +## Migration Guide + +### Enabling Maintenance Notifications (Standalone Client) + +**Before:** +```go +client := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Protocol: 2, // RESP2 +}) +``` + +**After:** +```go +client := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Protocol: 3, // RESP3 required + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeAuto, + }, +}) +``` + +### Enabling Hitless Upgrades (Cluster Client) + +For Redis Cluster with hitless slot migration support: + +```go +client := redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: []string{"localhost:7000", "localhost:7001", "localhost:7002"}, + Protocol: 3, // RESP3 required for push notifications + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeAuto, + RelaxedTimeout: 10 * time.Second, // Extended timeout during slot migrations + }, +}) +``` + +The cluster client automatically handles: +- **SMIGRATING**: Relaxes timeouts when slots are being migrated +- **SMIGRATED**: Triggers lazy cluster state reload when migration completes +- **SeqID Deduplication**: Same notification from multiple nodes triggers only one reload + +### Adding Monitoring + +```go +// Get the manager from the client +manager := client.GetMaintNotificationsManager() +if manager != nil { + // Add logging hook + loggingHook := maintnotifications.NewLoggingHook(2) // Info level + manager.AddNotificationHook(loggingHook) + + // Add metrics hook + metricsHook := maintnotifications.NewMetricsHook() + manager.AddNotificationHook(metricsHook) +} +``` + +## Known Limitations + +1. **RESP3 Required**: Push notifications require RESP3 protocol +2. **Server Support**: Requires Redis Enterprise or compatible Redis with maintenance notifications +3. **Single Connection Commands**: Some commands (MULTI/EXEC, WATCH) may need special handling +4. **No Failover/Ring Client Support**: Failover and Ring clients are not supported and there are no plans to add support + +## Future Enhancements + +- Enhanced metrics and observability +- TTL-based cleanup for SeqID deduplication map \ No newline at end of file diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md b/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md new file mode 100644 index 00000000000..2f354ef6a54 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md @@ -0,0 +1,73 @@ +# Maintenance Notifications + +Seamless Redis connection handoffs during cluster maintenance operations without dropping connections. + +## Cluster Support + +**Cluster notifications are now supported for ClusterClient!** + +- **SMIGRATING**: `["SMIGRATING", SeqID, slot/range, ...]` - Relaxes timeouts when slots are being migrated +- **SMIGRATED**: `["SMIGRATED", SeqID, src host:port, dst host:port, slot/range, ...]` - Reloads cluster state when slot migration completes + +**Note:** Other maintenance notifications (MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER) are supported only in standalone Redis clients. Cluster clients support SMIGRATING and SMIGRATED for cluster-specific slot migration handling. + +## Quick Start + +```go +client := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Protocol: 3, // RESP3 required + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeEnabled, + }, +}) +``` + +## Modes + +- **`ModeDisabled`** - Maintenance notifications disabled +- **`ModeEnabled`** - Forcefully enabled (fails if server doesn't support) +- **`ModeAuto`** - Auto-detect server support (default) + +## Configuration + +```go +&maintnotifications.Config{ + Mode: maintnotifications.ModeAuto, + EndpointType: maintnotifications.EndpointTypeAuto, + RelaxedTimeout: 10 * time.Second, + HandoffTimeout: 15 * time.Second, + MaxHandoffRetries: 3, + MaxWorkers: 0, // Auto-calculated + HandoffQueueSize: 0, // Auto-calculated + PostHandoffRelaxedDuration: 0, // 2 * RelaxedTimeout +} +``` + +### Endpoint Types + +- **`EndpointTypeAuto`** - Auto-detect based on connection (default) +- **`EndpointTypeInternalIP`** - Internal IP address +- **`EndpointTypeInternalFQDN`** - Internal FQDN +- **`EndpointTypeExternalIP`** - External IP address +- **`EndpointTypeExternalFQDN`** - External FQDN +- **`EndpointTypeNone`** - No endpoint (reconnect with current config) + +### Auto-Scaling + +**Workers**: `min(PoolSize/2, max(10, PoolSize/3))` when auto-calculated +**Queue**: `max(20×Workers, PoolSize)` capped by `MaxActiveConns+1` or `5×PoolSize` + +**Examples:** +- Pool 100: 33 workers, 660 queue (capped at 500) +- Pool 100 + MaxActiveConns 150: 33 workers, 151 queue + +## How It Works + +1. Redis sends push notifications about cluster maintenance operations +2. Client creates new connections to updated endpoints +3. Active operations transfer to new connections +4. Old connections close gracefully + + +## For more information, see [FEATURES](FEATURES.md) diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/circuit_breaker.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/circuit_breaker.go new file mode 100644 index 00000000000..cb76b6447fb --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/circuit_breaker.go @@ -0,0 +1,353 @@ +package maintnotifications + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" +) + +// CircuitBreakerState represents the state of a circuit breaker +type CircuitBreakerState int32 + +const ( + // CircuitBreakerClosed - normal operation, requests allowed + CircuitBreakerClosed CircuitBreakerState = iota + // CircuitBreakerOpen - failing fast, requests rejected + CircuitBreakerOpen + // CircuitBreakerHalfOpen - testing if service recovered + CircuitBreakerHalfOpen +) + +func (s CircuitBreakerState) String() string { + switch s { + case CircuitBreakerClosed: + return "closed" + case CircuitBreakerOpen: + return "open" + case CircuitBreakerHalfOpen: + return "half-open" + default: + return "unknown" + } +} + +// CircuitBreaker implements the circuit breaker pattern for endpoint-specific failure handling +type CircuitBreaker struct { + // Configuration + failureThreshold int // Number of failures before opening + resetTimeout time.Duration // How long to stay open before testing + maxRequests int // Max requests allowed in half-open state + + // State tracking (atomic for lock-free access) + state atomic.Int32 // CircuitBreakerState + failures atomic.Int64 // Current failure count + successes atomic.Int64 // Success count in half-open state + requests atomic.Int64 // Request count in half-open state + lastFailureTime atomic.Int64 // Unix timestamp of last failure + lastSuccessTime atomic.Int64 // Unix timestamp of last success + + // Endpoint identification + endpoint string + config *Config +} + +// newCircuitBreaker creates a new circuit breaker for an endpoint +func newCircuitBreaker(endpoint string, config *Config) *CircuitBreaker { + // Use configuration values with sensible defaults + failureThreshold := 5 + resetTimeout := 60 * time.Second + maxRequests := 3 + + if config != nil { + failureThreshold = config.CircuitBreakerFailureThreshold + resetTimeout = config.CircuitBreakerResetTimeout + maxRequests = config.CircuitBreakerMaxRequests + } + + return &CircuitBreaker{ + failureThreshold: failureThreshold, + resetTimeout: resetTimeout, + maxRequests: maxRequests, + endpoint: endpoint, + config: config, + state: atomic.Int32{}, // Defaults to CircuitBreakerClosed (0) + } +} + +// IsOpen returns true if the circuit breaker is open (rejecting requests) +func (cb *CircuitBreaker) IsOpen() bool { + state := CircuitBreakerState(cb.state.Load()) + return state == CircuitBreakerOpen +} + +// shouldAttemptReset checks if enough time has passed to attempt reset +func (cb *CircuitBreaker) shouldAttemptReset() bool { + lastFailure := time.Unix(cb.lastFailureTime.Load(), 0) + return time.Since(lastFailure) >= cb.resetTimeout +} + +// Execute runs the given function with circuit breaker protection +func (cb *CircuitBreaker) Execute(fn func() error) error { + // Single atomic state load for consistency + state := CircuitBreakerState(cb.state.Load()) + + switch state { + case CircuitBreakerOpen: + if cb.shouldAttemptReset() { + // Attempt transition to half-open + if cb.state.CompareAndSwap(int32(CircuitBreakerOpen), int32(CircuitBreakerHalfOpen)) { + cb.requests.Store(0) + cb.successes.Store(0) + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.CircuitBreakerTransitioningToHalfOpen(cb.endpoint)) + } + // Fall through to half-open logic + } else { + return ErrCircuitBreakerOpen + } + } else { + return ErrCircuitBreakerOpen + } + fallthrough + case CircuitBreakerHalfOpen: + requests := cb.requests.Add(1) + if requests > int64(cb.maxRequests) { + cb.requests.Add(-1) // Revert the increment + return ErrCircuitBreakerOpen + } + } + + // Execute the function with consistent state + err := fn() + + if err != nil { + cb.recordFailure() + return err + } + + cb.recordSuccess() + return nil +} + +// recordFailure records a failure and potentially opens the circuit +func (cb *CircuitBreaker) recordFailure() { + cb.lastFailureTime.Store(time.Now().Unix()) + failures := cb.failures.Add(1) + + state := CircuitBreakerState(cb.state.Load()) + + switch state { + case CircuitBreakerClosed: + if failures >= int64(cb.failureThreshold) { + if cb.state.CompareAndSwap(int32(CircuitBreakerClosed), int32(CircuitBreakerOpen)) { + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(context.Background(), logs.CircuitBreakerOpened(cb.endpoint, failures)) + } + } + } + case CircuitBreakerHalfOpen: + // Any failure in half-open state immediately opens the circuit + if cb.state.CompareAndSwap(int32(CircuitBreakerHalfOpen), int32(CircuitBreakerOpen)) { + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(context.Background(), logs.CircuitBreakerReopened(cb.endpoint)) + } + } + } +} + +// recordSuccess records a success and potentially closes the circuit +func (cb *CircuitBreaker) recordSuccess() { + cb.lastSuccessTime.Store(time.Now().Unix()) + + state := CircuitBreakerState(cb.state.Load()) + + switch state { + case CircuitBreakerClosed: + // Reset failure count on success in closed state + cb.failures.Store(0) + case CircuitBreakerHalfOpen: + successes := cb.successes.Add(1) + + // If we've had enough successful requests, close the circuit + if successes >= int64(cb.maxRequests) { + if cb.state.CompareAndSwap(int32(CircuitBreakerHalfOpen), int32(CircuitBreakerClosed)) { + cb.failures.Store(0) + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.CircuitBreakerClosed(cb.endpoint, successes)) + } + } + } + } +} + +// GetState returns the current state of the circuit breaker +func (cb *CircuitBreaker) GetState() CircuitBreakerState { + return CircuitBreakerState(cb.state.Load()) +} + +// GetStats returns current statistics for monitoring +func (cb *CircuitBreaker) GetStats() CircuitBreakerStats { + return CircuitBreakerStats{ + Endpoint: cb.endpoint, + State: cb.GetState(), + Failures: cb.failures.Load(), + Successes: cb.successes.Load(), + Requests: cb.requests.Load(), + LastFailureTime: time.Unix(cb.lastFailureTime.Load(), 0), + LastSuccessTime: time.Unix(cb.lastSuccessTime.Load(), 0), + } +} + +// CircuitBreakerStats provides statistics about a circuit breaker +type CircuitBreakerStats struct { + Endpoint string + State CircuitBreakerState + Failures int64 + Successes int64 + Requests int64 + LastFailureTime time.Time + LastSuccessTime time.Time +} + +// CircuitBreakerEntry wraps a circuit breaker with access tracking +type CircuitBreakerEntry struct { + breaker *CircuitBreaker + lastAccess atomic.Int64 // Unix timestamp + created time.Time +} + +// CircuitBreakerManager manages circuit breakers for multiple endpoints +type CircuitBreakerManager struct { + breakers sync.Map // map[string]*CircuitBreakerEntry + config *Config + cleanupStop chan struct{} + cleanupMu sync.Mutex + lastCleanup atomic.Int64 // Unix timestamp +} + +// newCircuitBreakerManager creates a new circuit breaker manager +func newCircuitBreakerManager(config *Config) *CircuitBreakerManager { + cbm := &CircuitBreakerManager{ + config: config, + cleanupStop: make(chan struct{}), + } + cbm.lastCleanup.Store(time.Now().Unix()) + + // Start background cleanup goroutine + go cbm.cleanupLoop() + + return cbm +} + +// GetCircuitBreaker returns the circuit breaker for an endpoint, creating it if necessary +func (cbm *CircuitBreakerManager) GetCircuitBreaker(endpoint string) *CircuitBreaker { + now := time.Now().Unix() + + if entry, ok := cbm.breakers.Load(endpoint); ok { + cbEntry := entry.(*CircuitBreakerEntry) + cbEntry.lastAccess.Store(now) + return cbEntry.breaker + } + + // Create new circuit breaker with metadata + newBreaker := newCircuitBreaker(endpoint, cbm.config) + newEntry := &CircuitBreakerEntry{ + breaker: newBreaker, + created: time.Now(), + } + newEntry.lastAccess.Store(now) + + actual, _ := cbm.breakers.LoadOrStore(endpoint, newEntry) + return actual.(*CircuitBreakerEntry).breaker +} + +// GetAllStats returns statistics for all circuit breakers +func (cbm *CircuitBreakerManager) GetAllStats() []CircuitBreakerStats { + var stats []CircuitBreakerStats + cbm.breakers.Range(func(key, value interface{}) bool { + entry := value.(*CircuitBreakerEntry) + stats = append(stats, entry.breaker.GetStats()) + return true + }) + return stats +} + +// cleanupLoop runs background cleanup of unused circuit breakers +func (cbm *CircuitBreakerManager) cleanupLoop() { + ticker := time.NewTicker(5 * time.Minute) // Cleanup every 5 minutes + defer ticker.Stop() + + for { + select { + case <-ticker.C: + cbm.cleanup() + case <-cbm.cleanupStop: + return + } + } +} + +// cleanup removes circuit breakers that haven't been accessed recently +func (cbm *CircuitBreakerManager) cleanup() { + // Prevent concurrent cleanups + if !cbm.cleanupMu.TryLock() { + return + } + defer cbm.cleanupMu.Unlock() + + now := time.Now() + cutoff := now.Add(-30 * time.Minute).Unix() // 30 minute TTL + + var toDelete []string + count := 0 + + cbm.breakers.Range(func(key, value interface{}) bool { + endpoint := key.(string) + entry := value.(*CircuitBreakerEntry) + + count++ + + // Remove if not accessed recently + if entry.lastAccess.Load() < cutoff { + toDelete = append(toDelete, endpoint) + } + + return true + }) + + // Delete expired entries + for _, endpoint := range toDelete { + cbm.breakers.Delete(endpoint) + } + + // Log cleanup results + if len(toDelete) > 0 && internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.CircuitBreakerCleanup(len(toDelete), count)) + } + + cbm.lastCleanup.Store(now.Unix()) +} + +// Shutdown stops the cleanup goroutine +func (cbm *CircuitBreakerManager) Shutdown() { + close(cbm.cleanupStop) +} + +// Reset resets all circuit breakers (useful for testing) +func (cbm *CircuitBreakerManager) Reset() { + cbm.breakers.Range(func(key, value interface{}) bool { + entry := value.(*CircuitBreakerEntry) + breaker := entry.breaker + breaker.state.Store(int32(CircuitBreakerClosed)) + breaker.failures.Store(0) + breaker.successes.Store(0) + breaker.requests.Store(0) + breaker.lastFailureTime.Store(0) + breaker.lastSuccessTime.Store(0) + return true + }) +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go new file mode 100644 index 00000000000..70d5acdcae8 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go @@ -0,0 +1,502 @@ +package maintnotifications + +import ( + "context" + "net" + "runtime" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" +) + +// Mode represents the maintenance notifications mode +type Mode string + +// Constants for maintenance push notifications modes +const ( + ModeDisabled Mode = "disabled" // Client doesn't send CLIENT MAINT_NOTIFICATIONS ON command + ModeEnabled Mode = "enabled" // Client forcefully sends command, interrupts connection on error + ModeAuto Mode = "auto" // Client tries to send command, disables feature on error +) + +// IsValid returns true if the maintenance notifications mode is valid +func (m Mode) IsValid() bool { + switch m { + case ModeDisabled, ModeEnabled, ModeAuto: + return true + default: + return false + } +} + +// String returns the string representation of the mode +func (m Mode) String() string { + return string(m) +} + +// EndpointType represents the type of endpoint to request in MOVING notifications +type EndpointType string + +// Constants for endpoint types +const ( + EndpointTypeAuto EndpointType = "auto" // Auto-detect based on connection + EndpointTypeInternalIP EndpointType = "internal-ip" // Internal IP address + EndpointTypeInternalFQDN EndpointType = "internal-fqdn" // Internal FQDN + EndpointTypeExternalIP EndpointType = "external-ip" // External IP address + EndpointTypeExternalFQDN EndpointType = "external-fqdn" // External FQDN + EndpointTypeNone EndpointType = "none" // No endpoint (reconnect with current config) +) + +// IsValid returns true if the endpoint type is valid +func (e EndpointType) IsValid() bool { + switch e { + case EndpointTypeAuto, EndpointTypeInternalIP, EndpointTypeInternalFQDN, + EndpointTypeExternalIP, EndpointTypeExternalFQDN, EndpointTypeNone: + return true + default: + return false + } +} + +// String returns the string representation of the endpoint type +func (e EndpointType) String() string { + return string(e) +} + +// Config provides configuration options for maintenance notifications +type Config struct { + // Mode controls how client maintenance notifications are handled. + // Valid values: ModeDisabled, ModeEnabled, ModeAuto + // Default: ModeAuto + Mode Mode + + // EndpointType specifies the type of endpoint to request in MOVING notifications. + // Valid values: EndpointTypeAuto, EndpointTypeInternalIP, EndpointTypeInternalFQDN, + // EndpointTypeExternalIP, EndpointTypeExternalFQDN, EndpointTypeNone + // Default: EndpointTypeAuto + EndpointType EndpointType + + // RelaxedTimeout is the concrete timeout value to use during + // MIGRATING/FAILING_OVER states to accommodate increased latency. + // This applies to both read and write timeouts. + // Default: 10 seconds + RelaxedTimeout time.Duration + + // HandoffTimeout is the maximum time to wait for connection handoff to complete. + // If handoff takes longer than this, the old connection will be forcibly closed. + // Default: 15 seconds (matches server-side eviction timeout) + HandoffTimeout time.Duration + + // MaxWorkers is the maximum number of worker goroutines for processing handoff requests. + // Workers are created on-demand and automatically cleaned up when idle. + // If zero, defaults to min(10, PoolSize/2) to handle bursts effectively. + // If explicitly set, enforces minimum of PoolSize/2 + // + // Default: min(PoolSize/2, max(10, PoolSize/3)), Minimum when set: PoolSize/2 + MaxWorkers int + + // HandoffQueueSize is the size of the buffered channel used to queue handoff requests. + // If the queue is full, new handoff requests will be rejected. + // Scales with both worker count and pool size for better burst handling. + // + // Default: max(20×MaxWorkers, PoolSize), capped by MaxActiveConns+1 (if set) or 5×PoolSize + // When set: minimum 200, capped by MaxActiveConns+1 (if set) or 5×PoolSize + HandoffQueueSize int + + // PostHandoffRelaxedDuration is how long to keep relaxed timeouts on the new connection + // after a handoff completes. This provides additional resilience during cluster transitions. + // Default: 2 * RelaxedTimeout + PostHandoffRelaxedDuration time.Duration + + // Circuit breaker configuration for endpoint failure handling + // CircuitBreakerFailureThreshold is the number of failures before opening the circuit. + // Default: 5 + CircuitBreakerFailureThreshold int + + // CircuitBreakerResetTimeout is how long to wait before testing if the endpoint recovered. + // Default: 60 seconds + CircuitBreakerResetTimeout time.Duration + + // CircuitBreakerMaxRequests is the maximum number of requests allowed in half-open state. + // Default: 3 + CircuitBreakerMaxRequests int + + // MaxHandoffRetries is the maximum number of times to retry a failed handoff. + // After this many retries, the connection will be removed from the pool. + // Default: 3 + MaxHandoffRetries int +} + +func (c *Config) IsEnabled() bool { + return c != nil && c.Mode != ModeDisabled +} + +// DefaultConfig returns a Config with sensible defaults. +func DefaultConfig() *Config { + return &Config{ + Mode: ModeAuto, // Enable by default for Redis Cloud + EndpointType: EndpointTypeAuto, // Auto-detect based on connection + RelaxedTimeout: 10 * time.Second, + HandoffTimeout: 15 * time.Second, + MaxWorkers: 0, // Auto-calculated based on pool size + HandoffQueueSize: 0, // Auto-calculated based on max workers + PostHandoffRelaxedDuration: 0, // Auto-calculated based on relaxed timeout + + // Circuit breaker configuration + CircuitBreakerFailureThreshold: 5, + CircuitBreakerResetTimeout: 60 * time.Second, + CircuitBreakerMaxRequests: 3, + + // Connection Handoff Configuration + MaxHandoffRetries: 3, + } +} + +// Validate checks if the configuration is valid. +func (c *Config) Validate() error { + if c.RelaxedTimeout <= 0 { + return ErrInvalidRelaxedTimeout + } + if c.HandoffTimeout <= 0 { + return ErrInvalidHandoffTimeout + } + // Validate worker configuration + // Allow 0 for auto-calculation, but negative values are invalid + if c.MaxWorkers < 0 { + return ErrInvalidHandoffWorkers + } + // HandoffQueueSize validation - allow 0 for auto-calculation + if c.HandoffQueueSize < 0 { + return ErrInvalidHandoffQueueSize + } + if c.PostHandoffRelaxedDuration < 0 { + return ErrInvalidPostHandoffRelaxedDuration + } + + // Circuit breaker validation + if c.CircuitBreakerFailureThreshold < 1 { + return ErrInvalidCircuitBreakerFailureThreshold + } + if c.CircuitBreakerResetTimeout < 0 { + return ErrInvalidCircuitBreakerResetTimeout + } + if c.CircuitBreakerMaxRequests < 1 { + return ErrInvalidCircuitBreakerMaxRequests + } + + // Validate Mode (maintenance notifications mode) + if !c.Mode.IsValid() { + return ErrInvalidMaintNotifications + } + + // Validate EndpointType + if !c.EndpointType.IsValid() { + return ErrInvalidEndpointType + } + + // Validate configuration fields + if c.MaxHandoffRetries < 1 || c.MaxHandoffRetries > 10 { + return ErrInvalidHandoffRetries + } + + return nil +} + +// ApplyDefaults applies default values to any zero-value fields in the configuration. +// This ensures that partially configured structs get sensible defaults for missing fields. +func (c *Config) ApplyDefaults() *Config { + return c.ApplyDefaultsWithPoolSize(0) +} + +// ApplyDefaultsWithPoolSize applies default values to any zero-value fields in the configuration, +// using the provided pool size to calculate worker defaults. +// This ensures that partially configured structs get sensible defaults for missing fields. +func (c *Config) ApplyDefaultsWithPoolSize(poolSize int) *Config { + return c.ApplyDefaultsWithPoolConfig(poolSize, 0) +} + +// ApplyDefaultsWithPoolConfig applies default values to any zero-value fields in the configuration, +// using the provided pool size and max active connections to calculate worker and queue defaults. +// This ensures that partially configured structs get sensible defaults for missing fields. +func (c *Config) ApplyDefaultsWithPoolConfig(poolSize int, maxActiveConns int) *Config { + if c == nil { + return DefaultConfig().ApplyDefaultsWithPoolSize(poolSize) + } + + defaults := DefaultConfig() + result := &Config{} + + // Apply defaults for enum fields (empty/zero means not set) + result.Mode = defaults.Mode + if c.Mode != "" { + result.Mode = c.Mode + } + + result.EndpointType = defaults.EndpointType + if c.EndpointType != "" { + result.EndpointType = c.EndpointType + } + + // Apply defaults for duration fields (zero means not set) + result.RelaxedTimeout = defaults.RelaxedTimeout + if c.RelaxedTimeout > 0 { + result.RelaxedTimeout = c.RelaxedTimeout + } + + result.HandoffTimeout = defaults.HandoffTimeout + if c.HandoffTimeout > 0 { + result.HandoffTimeout = c.HandoffTimeout + } + + // Copy worker configuration + result.MaxWorkers = c.MaxWorkers + + // Apply worker defaults based on pool size + result.applyWorkerDefaults(poolSize) + + // Apply queue size defaults with new scaling approach + // Default: max(20x workers, PoolSize), capped by maxActiveConns or 5x pool size + workerBasedSize := result.MaxWorkers * 20 + poolBasedSize := poolSize + result.HandoffQueueSize = max(workerBasedSize, poolBasedSize) + if c.HandoffQueueSize > 0 { + // When explicitly set: enforce minimum of 200 + result.HandoffQueueSize = max(200, c.HandoffQueueSize) + } + + // Cap queue size: use maxActiveConns+1 if set, otherwise 5x pool size + var queueCap int + if maxActiveConns > 0 { + queueCap = maxActiveConns + 1 + // Ensure queue cap is at least 2 for very small maxActiveConns + if queueCap < 2 { + queueCap = 2 + } + } else { + queueCap = poolSize * 5 + } + result.HandoffQueueSize = min(result.HandoffQueueSize, queueCap) + + // Ensure minimum queue size of 2 (fallback for very small pools) + if result.HandoffQueueSize < 2 { + result.HandoffQueueSize = 2 + } + + result.PostHandoffRelaxedDuration = result.RelaxedTimeout * 2 + if c.PostHandoffRelaxedDuration > 0 { + result.PostHandoffRelaxedDuration = c.PostHandoffRelaxedDuration + } + + // Apply defaults for configuration fields + result.MaxHandoffRetries = defaults.MaxHandoffRetries + if c.MaxHandoffRetries > 0 { + result.MaxHandoffRetries = c.MaxHandoffRetries + } + + // Circuit breaker configuration + result.CircuitBreakerFailureThreshold = defaults.CircuitBreakerFailureThreshold + if c.CircuitBreakerFailureThreshold > 0 { + result.CircuitBreakerFailureThreshold = c.CircuitBreakerFailureThreshold + } + + result.CircuitBreakerResetTimeout = defaults.CircuitBreakerResetTimeout + if c.CircuitBreakerResetTimeout > 0 { + result.CircuitBreakerResetTimeout = c.CircuitBreakerResetTimeout + } + + result.CircuitBreakerMaxRequests = defaults.CircuitBreakerMaxRequests + if c.CircuitBreakerMaxRequests > 0 { + result.CircuitBreakerMaxRequests = c.CircuitBreakerMaxRequests + } + + if internal.LogLevel.DebugOrAbove() { + internal.Logger.Printf(context.Background(), logs.DebugLoggingEnabled()) + internal.Logger.Printf(context.Background(), logs.ConfigDebug(result)) + } + return result +} + +// Clone creates a deep copy of the configuration. +func (c *Config) Clone() *Config { + if c == nil { + return DefaultConfig() + } + + return &Config{ + Mode: c.Mode, + EndpointType: c.EndpointType, + RelaxedTimeout: c.RelaxedTimeout, + HandoffTimeout: c.HandoffTimeout, + MaxWorkers: c.MaxWorkers, + HandoffQueueSize: c.HandoffQueueSize, + PostHandoffRelaxedDuration: c.PostHandoffRelaxedDuration, + + // Circuit breaker configuration + CircuitBreakerFailureThreshold: c.CircuitBreakerFailureThreshold, + CircuitBreakerResetTimeout: c.CircuitBreakerResetTimeout, + CircuitBreakerMaxRequests: c.CircuitBreakerMaxRequests, + + // Configuration fields + MaxHandoffRetries: c.MaxHandoffRetries, + } +} + +// applyWorkerDefaults calculates and applies worker defaults based on pool size +func (c *Config) applyWorkerDefaults(poolSize int) { + // Calculate defaults based on pool size + if poolSize <= 0 { + poolSize = 10 * runtime.GOMAXPROCS(0) + } + + // When not set: min(poolSize/2, max(10, poolSize/3)) - balanced scaling approach + originalMaxWorkers := c.MaxWorkers + c.MaxWorkers = min(poolSize/2, max(10, poolSize/3)) + if originalMaxWorkers != 0 { + // When explicitly set: max(poolSize/2, set_value) - ensure at least poolSize/2 workers + c.MaxWorkers = max(poolSize/2, originalMaxWorkers) + } + + // Ensure minimum of 1 worker (fallback for very small pools) + if c.MaxWorkers < 1 { + c.MaxWorkers = 1 + } +} + +// endpointDetectResolveTimeout bounds the DNS lookup performed by +// DetectEndpointType so a slow or broken resolver cannot block client +// construction for the full system resolver timeout (often 5-30s). +const endpointDetectResolveTimeout = 2 * time.Second + +// cgnatNet is RFC6598 shared address space (100.64.0.0/10), used by many +// cloud/carrier NATs and not covered by net.IP.IsPrivate. +var cgnatNet = &net.IPNet{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)} + +// isPrivateIP reports whether ip belongs to a range that should be treated +// as "internal" for the purpose of endpoint type detection. It extends +// net.IP.IsPrivate (RFC1918 + RFC4193) with loopback, link-local and +// RFC6598 shared address space (CGNAT). +func isPrivateIP(ip net.IP) bool { + if ip == nil { + return false + } + if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return true + } + if v4 := ip.To4(); v4 != nil && cgnatNet.Contains(v4) { + return true + } + return false +} + +// DetectEndpointType automatically detects the appropriate endpoint type +// based on the connection address and TLS configuration. +// +// TLS behaviour: +// - If TLS is enabled: requests FQDN for proper certificate validation +// (SNI / hostname verification). +// - If TLS is disabled: always requests IP for better performance, even +// when the configured address is a hostname. In that case the hostname +// is resolved to determine whether it belongs to an internal or +// external network range. +// +// Internal vs External detection: +// - For IPs: uses private IP range detection +// - For hostnames: resolves the hostname to an IP address and uses the IP range detection +func DetectEndpointType(addr string, tlsEnabled bool) EndpointType { + // Extract host from "host:port" format + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr // Assume no port + } + + // An empty host (e.g., ":6379") conventionally means the loopback + // interface and is treated as internal. With TLS off we return an IP + // endpoint; with TLS on the caller still needs an FQDN for SNI. + if host == "" { + if tlsEnabled { + return EndpointTypeInternalFQDN + } + return EndpointTypeInternalIP + } + + // Check if the host is an IP address or hostname + ip := net.ParseIP(host) + isIPAddress := ip != nil + var endpointType EndpointType + + if isIPAddress { + // Address is an IP - determine if it's private or public + isPrivate := isPrivateIP(ip) + + if tlsEnabled { + // TLS with IP addresses - still prefer FQDN for certificate validation + if isPrivate { + endpointType = EndpointTypeInternalFQDN + } else { + endpointType = EndpointTypeExternalFQDN + } + } else { + // No TLS - can use IP addresses directly + if isPrivate { + endpointType = EndpointTypeInternalIP + } else { + endpointType = EndpointTypeExternalIP + } + } + } else { + // Address is a hostname - resolve it under a bounded timeout so a + // slow/broken DNS server cannot stall client construction. + ctx, cancel := context.WithTimeout(context.Background(), endpointDetectResolveTimeout) + defer cancel() + + isInternal, err := isInternalHostname(ctx, host) + // Will fallback to external classification if we can't determine + // whether the hostname is internal. + if err != nil && internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(ctx, "Failed to determine if hostname %q is internal: %v", host, err) + } + + if tlsEnabled { + // With TLS the server name must be preserved for certificate + // validation, so request an FQDN endpoint. + if isInternal { + endpointType = EndpointTypeInternalFQDN + } else { + endpointType = EndpointTypeExternalFQDN + } + } else { + // Without TLS we always prefer IP endpoints for performance, + // even if the configured address is a hostname. + if isInternal { + endpointType = EndpointTypeInternalIP + } else { + endpointType = EndpointTypeExternalIP + } + } + } + + return endpointType +} + +// isInternalHostname resolves the hostname (both IPv4 and IPv6) under the +// given context and reports whether every resolved address is in a +// private/internal range. If any address is public the hostname is treated +// as external. A resolution error returns (false, err). An empty result set +// returns (false, nil); callers are expected to fall back to an external +// classification when the hostname cannot be determined to be internal. +func isInternalHostname(ctx context.Context, hostname string) (bool, error) { + ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) + if err != nil { + return false, err + } + if len(ips) == 0 { + return false, nil + } + for _, ia := range ips { + if !isPrivateIP(ia.IP) { + return false, nil + } + } + return true, nil +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/errors.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/errors.go new file mode 100644 index 00000000000..049656bddc2 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/errors.go @@ -0,0 +1,76 @@ +package maintnotifications + +import ( + "errors" + + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" +) + +// Configuration errors +var ( + ErrInvalidRelaxedTimeout = errors.New(logs.InvalidRelaxedTimeoutError()) + ErrInvalidHandoffTimeout = errors.New(logs.InvalidHandoffTimeoutError()) + ErrInvalidHandoffWorkers = errors.New(logs.InvalidHandoffWorkersError()) + ErrInvalidHandoffQueueSize = errors.New(logs.InvalidHandoffQueueSizeError()) + ErrInvalidPostHandoffRelaxedDuration = errors.New(logs.InvalidPostHandoffRelaxedDurationError()) + ErrInvalidEndpointType = errors.New(logs.InvalidEndpointTypeError()) + ErrInvalidMaintNotifications = errors.New(logs.InvalidMaintNotificationsError()) + ErrMaxHandoffRetriesReached = errors.New(logs.MaxHandoffRetriesReachedError()) + + // Configuration validation errors + + // ErrInvalidHandoffRetries is returned when the number of handoff retries is invalid + ErrInvalidHandoffRetries = errors.New(logs.InvalidHandoffRetriesError()) +) + +// Integration errors +var ( + // ErrInvalidClient is returned when the client does not support push notifications + ErrInvalidClient = errors.New(logs.InvalidClientError()) +) + +// Handoff errors +var ( + // ErrHandoffQueueFull is returned when the handoff queue is full + ErrHandoffQueueFull = errors.New(logs.HandoffQueueFullError()) +) + +// Notification errors +var ( + // ErrInvalidNotification is returned when a notification is in an invalid format + ErrInvalidNotification = errors.New(logs.InvalidNotificationError()) +) + +// connection handoff errors +var ( + // ErrConnectionMarkedForHandoff is returned when a connection is marked for handoff + // and should not be used until the handoff is complete + ErrConnectionMarkedForHandoff = errors.New(logs.ConnectionMarkedForHandoffErrorMessage) + // ErrConnectionMarkedForHandoffWithState is returned when a connection is marked for handoff + // and should not be used until the handoff is complete + ErrConnectionMarkedForHandoffWithState = errors.New(logs.ConnectionMarkedForHandoffErrorMessage + " with state") + // ErrConnectionInvalidHandoffState is returned when a connection is in an invalid state for handoff + ErrConnectionInvalidHandoffState = errors.New(logs.ConnectionInvalidHandoffStateErrorMessage) +) + +// shutdown errors +var ( + // ErrShutdown is returned when the maintnotifications manager is shutdown + ErrShutdown = errors.New(logs.ShutdownError()) +) + +// circuit breaker errors +var ( + // ErrCircuitBreakerOpen is returned when the circuit breaker is open + ErrCircuitBreakerOpen = errors.New(logs.CircuitBreakerOpenErrorMessage) +) + +// circuit breaker configuration errors +var ( + // ErrInvalidCircuitBreakerFailureThreshold is returned when the circuit breaker failure threshold is invalid + ErrInvalidCircuitBreakerFailureThreshold = errors.New(logs.InvalidCircuitBreakerFailureThresholdError()) + // ErrInvalidCircuitBreakerResetTimeout is returned when the circuit breaker reset timeout is invalid + ErrInvalidCircuitBreakerResetTimeout = errors.New(logs.InvalidCircuitBreakerResetTimeoutError()) + // ErrInvalidCircuitBreakerMaxRequests is returned when the circuit breaker max requests is invalid + ErrInvalidCircuitBreakerMaxRequests = errors.New(logs.InvalidCircuitBreakerMaxRequestsError()) +) diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/example_hooks.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/example_hooks.go new file mode 100644 index 00000000000..3a34655715a --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/example_hooks.go @@ -0,0 +1,101 @@ +package maintnotifications + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/push" +) + +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + startTimeKey contextKey = "maint_notif_start_time" +) + +// MetricsHook collects metrics about notification processing. +type MetricsHook struct { + NotificationCounts map[string]int64 + ProcessingTimes map[string]time.Duration + ErrorCounts map[string]int64 + HandoffCounts int64 // Total handoffs initiated + HandoffSuccesses int64 // Successful handoffs + HandoffFailures int64 // Failed handoffs +} + +// NewMetricsHook creates a new metrics collection hook. +func NewMetricsHook() *MetricsHook { + return &MetricsHook{ + NotificationCounts: make(map[string]int64), + ProcessingTimes: make(map[string]time.Duration), + ErrorCounts: make(map[string]int64), + } +} + +// PreHook records the start time for processing metrics. +func (mh *MetricsHook) PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) { + mh.NotificationCounts[notificationType]++ + + // Log connection information if available + if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { + internal.Logger.Printf(ctx, logs.MetricsHookProcessingNotification(notificationType, conn.GetID())) + } + + // Store start time in context for duration calculation + startTime := time.Now() + _ = context.WithValue(ctx, startTimeKey, startTime) // Context not used further + + return notification, true +} + +// PostHook records processing completion and any errors. +func (mh *MetricsHook) PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) { + // Calculate processing duration + if startTime, ok := ctx.Value(startTimeKey).(time.Time); ok { + duration := time.Since(startTime) + mh.ProcessingTimes[notificationType] = duration + } + + // Record errors + if result != nil { + mh.ErrorCounts[notificationType]++ + + // Log error details with connection information + if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { + internal.Logger.Printf(ctx, logs.MetricsHookRecordedError(notificationType, conn.GetID(), result)) + } + } +} + +// GetMetrics returns a summary of collected metrics. +func (mh *MetricsHook) GetMetrics() map[string]interface{} { + return map[string]interface{}{ + "notification_counts": mh.NotificationCounts, + "processing_times": mh.ProcessingTimes, + "error_counts": mh.ErrorCounts, + } +} + +// ExampleCircuitBreakerMonitor demonstrates how to monitor circuit breaker status +func ExampleCircuitBreakerMonitor(poolHook *PoolHook) { + // Get circuit breaker statistics + stats := poolHook.GetCircuitBreakerStats() + + for _, stat := range stats { + fmt.Printf("Circuit Breaker for %s:\n", stat.Endpoint) + fmt.Printf(" State: %s\n", stat.State) + fmt.Printf(" Failures: %d\n", stat.Failures) + fmt.Printf(" Last Failure: %v\n", stat.LastFailureTime) + fmt.Printf(" Last Success: %v\n", stat.LastSuccessTime) + + // Alert if circuit breaker is open + if stat.State.String() == "open" { + fmt.Printf(" ⚠️ ALERT: Circuit breaker is OPEN for %s\n", stat.Endpoint) + } + } +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go new file mode 100644 index 00000000000..d66542ffc4f --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go @@ -0,0 +1,525 @@ +package maintnotifications + +import ( + "context" + "errors" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" +) + +// PoolNameMain is the name used for the main connection pool in metrics. +const PoolNameMain = "main" + +// handoffWorkerManager manages background workers and queue for connection handoffs +type handoffWorkerManager struct { + // Event-driven handoff support + handoffQueue chan HandoffRequest // Queue for handoff requests + shutdown chan struct{} // Shutdown signal + shutdownOnce sync.Once // Ensure clean shutdown + workerWg sync.WaitGroup // Track worker goroutines + + // On-demand worker management + maxWorkers int + activeWorkers atomic.Int32 + workerTimeout time.Duration // How long workers wait for work before exiting + workersScaling atomic.Bool + + // Simple state tracking + pending sync.Map // map[uint64]int64 (connID -> seqID) + + // Configuration for the maintenance notifications + config *Config + + // Pool hook reference for handoff processing + poolHook *PoolHook + + // Circuit breaker manager for endpoint failure handling + circuitBreakerManager *CircuitBreakerManager +} + +// newHandoffWorkerManager creates a new handoff worker manager +func newHandoffWorkerManager(config *Config, poolHook *PoolHook) *handoffWorkerManager { + return &handoffWorkerManager{ + handoffQueue: make(chan HandoffRequest, config.HandoffQueueSize), + shutdown: make(chan struct{}), + maxWorkers: config.MaxWorkers, + activeWorkers: atomic.Int32{}, // Start with no workers - create on demand + workerTimeout: 15 * time.Second, // Workers exit after 15s of inactivity + config: config, + poolHook: poolHook, + circuitBreakerManager: newCircuitBreakerManager(config), + } +} + +// getCurrentWorkers returns the current number of active workers (for testing) +func (hwm *handoffWorkerManager) getCurrentWorkers() int { + return int(hwm.activeWorkers.Load()) +} + +// getPendingMap returns the pending map for testing purposes +func (hwm *handoffWorkerManager) getPendingMap() *sync.Map { + return &hwm.pending +} + +// getMaxWorkers returns the max workers for testing purposes +func (hwm *handoffWorkerManager) getMaxWorkers() int { + return hwm.maxWorkers +} + +// getHandoffQueue returns the handoff queue for testing purposes +func (hwm *handoffWorkerManager) getHandoffQueue() chan HandoffRequest { + return hwm.handoffQueue +} + +// getCircuitBreakerStats returns circuit breaker statistics for monitoring +func (hwm *handoffWorkerManager) getCircuitBreakerStats() []CircuitBreakerStats { + return hwm.circuitBreakerManager.GetAllStats() +} + +// resetCircuitBreakers resets all circuit breakers (useful for testing) +func (hwm *handoffWorkerManager) resetCircuitBreakers() { + hwm.circuitBreakerManager.Reset() +} + +// isHandoffPending returns true if the given connection has a pending handoff +func (hwm *handoffWorkerManager) isHandoffPending(conn *pool.Conn) bool { + _, pending := hwm.pending.Load(conn.GetID()) + return pending +} + +// ensureWorkerAvailable ensures at least one worker is available to process requests +// Creates a new worker if needed and under the max limit +func (hwm *handoffWorkerManager) ensureWorkerAvailable() { + select { + case <-hwm.shutdown: + return + default: + if hwm.workersScaling.CompareAndSwap(false, true) { + defer hwm.workersScaling.Store(false) + // Check if we need a new worker + currentWorkers := hwm.activeWorkers.Load() + workersWas := currentWorkers + for currentWorkers < int32(hwm.maxWorkers) { + hwm.workerWg.Add(1) + go hwm.onDemandWorker() + currentWorkers++ + } + // workersWas is always <= currentWorkers + // currentWorkers will be maxWorkers, but if we have a worker that was closed + // while we were creating new workers, just add the difference between + // the currentWorkers and the number of workers we observed initially (i.e. the number of workers we created) + hwm.activeWorkers.Add(currentWorkers - workersWas) + } + } +} + +// onDemandWorker processes handoff requests and exits when idle +func (hwm *handoffWorkerManager) onDemandWorker() { + defer func() { + // Handle panics to ensure proper cleanup + if r := recover(); r != nil { + internal.Logger.Printf(context.Background(), logs.WorkerPanicRecovered(r)) + } + + // Decrement active worker count when exiting + hwm.activeWorkers.Add(-1) + hwm.workerWg.Done() + }() + + // Create reusable timer to prevent timer leaks + timer := time.NewTimer(hwm.workerTimeout) + defer timer.Stop() + + for { + // Reset timer for next iteration + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(hwm.workerTimeout) + + select { + case <-hwm.shutdown: + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.WorkerExitingDueToShutdown()) + } + return + case <-timer.C: + // Worker has been idle for too long, exit to save resources + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.WorkerExitingDueToInactivityTimeout(hwm.workerTimeout)) + } + return + case request := <-hwm.handoffQueue: + // Check for shutdown before processing + select { + case <-hwm.shutdown: + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.WorkerExitingDueToShutdownWhileProcessing()) + } + // Clean up the request before exiting + hwm.pending.Delete(request.ConnID) + return + default: + // Process the request + hwm.processHandoffRequest(request) + } + } + } +} + +// processHandoffRequest processes a single handoff request +func (hwm *handoffWorkerManager) processHandoffRequest(request HandoffRequest) { + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.HandoffStarted(request.Conn.GetID(), request.Endpoint)) + } + + // Create a context with handoff timeout from config + handoffTimeout := 15 * time.Second // Default timeout + if hwm.config != nil && hwm.config.HandoffTimeout > 0 { + handoffTimeout = hwm.config.HandoffTimeout + } + ctx, cancel := context.WithTimeout(context.Background(), handoffTimeout) + defer cancel() + + // Create a context that also respects the shutdown signal + shutdownCtx, shutdownCancel := context.WithCancel(ctx) + defer shutdownCancel() + + // Monitor shutdown signal in a separate goroutine + go func() { + select { + case <-hwm.shutdown: + shutdownCancel() + case <-shutdownCtx.Done(): + } + }() + + // Perform the handoff with cancellable context + shouldRetry, err := hwm.performConnectionHandoff(shutdownCtx, request.Conn) + minRetryBackoff := 500 * time.Millisecond + if err != nil { + if shouldRetry { + now := time.Now() + deadline, ok := shutdownCtx.Deadline() + thirdOfTimeout := handoffTimeout / 3 + if !ok || deadline.Before(now) { + // wait half the timeout before retrying if no deadline or deadline has passed + deadline = now.Add(thirdOfTimeout) + } + afterTime := deadline.Sub(now) + if afterTime < minRetryBackoff { + afterTime = minRetryBackoff + } + + if internal.LogLevel.InfoOrAbove() { + // Get current retry count for better logging + currentRetries := request.Conn.HandoffRetries() + maxRetries := 3 // Default fallback + if hwm.config != nil { + maxRetries = hwm.config.MaxHandoffRetries + } + internal.Logger.Printf(context.Background(), logs.HandoffFailed(request.ConnID, request.Endpoint, currentRetries, maxRetries, err)) + } + // Schedule retry - keep connection in pending map until retry is queued + time.AfterFunc(afterTime, func() { + if err := hwm.queueHandoff(request.Conn); err != nil { + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(context.Background(), logs.CannotQueueHandoffForRetry(err)) + } + // Failed to queue retry - remove from pending and close connection + hwm.pending.Delete(request.Conn.GetID()) + hwm.closeConnFromRequest(context.Background(), request, err) + } else { + // Successfully queued retry - remove from pending (will be re-added by queueHandoff) + hwm.pending.Delete(request.Conn.GetID()) + } + }) + return + } else { + // Won't retry - remove from pending and close connection + hwm.pending.Delete(request.Conn.GetID()) + go hwm.closeConnFromRequest(ctx, request, err) + } + + // Clear handoff state if not returned for retry + seqID := request.Conn.GetMovingSeqID() + connID := request.Conn.GetID() + if hwm.poolHook.operationsManager != nil { + hwm.poolHook.operationsManager.UntrackOperationWithConnID(seqID, connID) + } + } else { + // Success - remove from pending map + hwm.pending.Delete(request.Conn.GetID()) + } +} + +// queueHandoff queues a handoff request for processing +// if err is returned, connection will be removed from pool +func (hwm *handoffWorkerManager) queueHandoff(conn *pool.Conn) error { + // Get handoff info atomically to prevent race conditions + shouldHandoff, endpoint, seqID := conn.GetHandoffInfo() + + // on retries the connection will not be marked for handoff, but it will have retries > 0 + // if shouldHandoff is false and retries is 0, then we are not retrying and not do a handoff + if !shouldHandoff && conn.HandoffRetries() == 0 { + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.ConnectionNotMarkedForHandoff(conn.GetID())) + } + return errors.New(logs.ConnectionNotMarkedForHandoffError(conn.GetID())) + } + + // Create handoff request with atomically retrieved data + request := HandoffRequest{ + Conn: conn, + ConnID: conn.GetID(), + Endpoint: endpoint, + SeqID: seqID, + Pool: hwm.poolHook.pool, // Include pool for connection removal on failure + } + + select { + // priority to shutdown + case <-hwm.shutdown: + return ErrShutdown + default: + select { + case <-hwm.shutdown: + return ErrShutdown + case hwm.handoffQueue <- request: + // Store in pending map + hwm.pending.Store(request.ConnID, request.SeqID) + // Ensure we have a worker to process this request + hwm.ensureWorkerAvailable() + return nil + default: + select { + case <-hwm.shutdown: + return ErrShutdown + case hwm.handoffQueue <- request: + // Store in pending map + hwm.pending.Store(request.ConnID, request.SeqID) + // Ensure we have a worker to process this request + hwm.ensureWorkerAvailable() + return nil + case <-time.After(100 * time.Millisecond): // give workers a chance to process + // Queue is full - log and attempt scaling + queueLen := len(hwm.handoffQueue) + queueCap := cap(hwm.handoffQueue) + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(context.Background(), logs.HandoffQueueFull(queueLen, queueCap)) + } + } + } + } + + // Ensure we have workers available to handle the load + hwm.ensureWorkerAvailable() + return ErrHandoffQueueFull +} + +// shutdownWorkers gracefully shuts down the worker manager, waiting for workers to complete +func (hwm *handoffWorkerManager) shutdownWorkers(ctx context.Context) error { + hwm.shutdownOnce.Do(func() { + close(hwm.shutdown) + // workers will exit when they finish their current request + + // Shutdown circuit breaker manager cleanup goroutine + if hwm.circuitBreakerManager != nil { + hwm.circuitBreakerManager.Shutdown() + } + }) + + // Wait for workers to complete + done := make(chan struct{}) + go func() { + hwm.workerWg.Wait() + close(done) + }() + + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// performConnectionHandoff performs the actual connection handoff +// When error is returned, the connection handoff should be retried if err is not ErrMaxHandoffRetriesReached +func (hwm *handoffWorkerManager) performConnectionHandoff(ctx context.Context, conn *pool.Conn) (shouldRetry bool, err error) { + // Clear handoff state after successful handoff + connID := conn.GetID() + + newEndpoint := conn.GetHandoffEndpoint() + if newEndpoint == "" { + return false, ErrConnectionInvalidHandoffState + } + + // Use circuit breaker to protect against failing endpoints + circuitBreaker := hwm.circuitBreakerManager.GetCircuitBreaker(newEndpoint) + + // Check if circuit breaker is open before attempting handoff + if circuitBreaker.IsOpen() { + internal.Logger.Printf(ctx, logs.CircuitBreakerOpen(connID, newEndpoint)) + return false, ErrCircuitBreakerOpen // Don't retry when circuit breaker is open + } + + // Perform the handoff + shouldRetry, err = hwm.performHandoffInternal(ctx, conn, newEndpoint, connID) + + // Update circuit breaker based on result + if err != nil { + // Only track dial/network errors in circuit breaker, not initialization errors + if shouldRetry { + circuitBreaker.recordFailure() + } + return shouldRetry, err + } + + // Success - record in circuit breaker + circuitBreaker.recordSuccess() + return false, nil +} + +// performHandoffInternal performs the actual handoff logic (extracted for circuit breaker integration) +func (hwm *handoffWorkerManager) performHandoffInternal( + ctx context.Context, + conn *pool.Conn, + newEndpoint string, + connID uint64, +) (shouldRetry bool, err error) { + retries := conn.IncrementAndGetHandoffRetries(1) + internal.Logger.Printf(ctx, logs.HandoffRetryAttempt(connID, retries, newEndpoint, conn.RemoteAddr().String())) + maxRetries := 3 // Default fallback + if hwm.config != nil { + maxRetries = hwm.config.MaxHandoffRetries + } + + if retries > maxRetries { + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(ctx, logs.ReachedMaxHandoffRetries(connID, newEndpoint, maxRetries)) + } + // won't retry on ErrMaxHandoffRetriesReached + return false, ErrMaxHandoffRetriesReached + } + + // Create endpoint-specific dialer + endpointDialer := hwm.createEndpointDialer(newEndpoint) + + // Create new connection to the new endpoint + newNetConn, err := endpointDialer(ctx) + if err != nil { + internal.Logger.Printf(ctx, logs.FailedToDialNewEndpoint(connID, newEndpoint, err)) + // will retry + // Maybe a network error - retry after a delay + return true, err + } + + // Get the old connection + oldConn := conn.GetNetConn() + + // Apply relaxed timeout to the new connection for the configured post-handoff duration + // This gives the new connection more time to handle operations during cluster transition + // Setting this here (before initing the connection) ensures that the connection is going + // to use the relaxed timeout for the first operation (auth/ACL select) + if hwm.config != nil && hwm.config.PostHandoffRelaxedDuration > 0 { + relaxedTimeout := hwm.config.RelaxedTimeout + // Set relaxed timeout with deadline - no background goroutine needed + deadline := time.Now().Add(hwm.config.PostHandoffRelaxedDuration) + conn.SetRelaxedTimeoutWithDeadline(relaxedTimeout, relaxedTimeout, deadline) + + // Record relaxed timeout metric (post-handoff) + if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil { + relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "HANDOFF") + } + + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(context.Background(), logs.ApplyingRelaxedTimeoutDueToPostHandoff(connID, relaxedTimeout, deadline.Format("15:04:05.000"))) + } + } + + // Replace the connection and execute initialization + err = conn.SetNetConnAndInitConn(ctx, newNetConn) + if err != nil { + // won't retry + // Initialization failed - remove the connection + return false, err + } + defer func() { + if oldConn != nil { + oldConn.Close() + } + }() + + // Clear handoff state will: + // - set the connection as usable again + // - clear the handoff state (shouldHandoff, endpoint, seqID) + // - reset the handoff retries to 0 + // Note: Theoretically there may be a short window where the connection is in the pool + // and IDLE (initConn completed) but still has handoff state set. + conn.ClearHandoffState() + internal.Logger.Printf(ctx, logs.HandoffSucceeded(connID, newEndpoint)) + + // successfully completed the handoff, no retry needed and no error + // Notify metrics: connection handoff succeeded + if handoffCallback := pool.GetMetricConnectionHandoffCallback(); handoffCallback != nil { + handoffCallback(ctx, conn, PoolNameMain) + } + + return false, nil +} + +// createEndpointDialer creates a dialer function that connects to a specific endpoint +func (hwm *handoffWorkerManager) createEndpointDialer(endpoint string) func(context.Context) (net.Conn, error) { + return func(ctx context.Context) (net.Conn, error) { + // Parse endpoint to extract host and port + host, port, err := net.SplitHostPort(endpoint) + if err != nil { + // If no port specified, assume default Redis port + host = endpoint + if port == "" { + port = "6379" + } + } + + // Use the base dialer to connect to the new endpoint + return hwm.poolHook.baseDialer(ctx, hwm.poolHook.network, net.JoinHostPort(host, port)) + } +} + +// closeConnFromRequest closes the connection and logs the reason +func (hwm *handoffWorkerManager) closeConnFromRequest(ctx context.Context, request HandoffRequest, err error) { + pooler := request.Pool + conn := request.Conn + + // Clear handoff state before closing + conn.ClearHandoffState() + + if pooler != nil { + // Use RemoveWithoutTurn instead of Remove to avoid freeing a turn that we don't have. + // The handoff worker doesn't call Get(), so it doesn't have a turn to free. + // Remove() is meant to be called after Get() and frees a turn. + // RemoveWithoutTurn() removes and closes the connection without affecting the queue. + pooler.RemoveWithoutTurn(ctx, conn, err) + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(ctx, logs.RemovingConnectionFromPool(conn.GetID(), err)) + } + } else { + errClose := conn.Close() // Close the connection if no pool provided + if errClose != nil { + internal.Logger.Printf(ctx, "redis: failed to close connection: %v", errClose) + } + if internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(ctx, logs.NoPoolProvidedCannotRemove(conn.GetID(), err)) + } + } +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/hooks.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/hooks.go new file mode 100644 index 00000000000..ee3c3819c2a --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/hooks.go @@ -0,0 +1,60 @@ +package maintnotifications + +import ( + "context" + "slices" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/push" +) + +// LoggingHook is an example hook implementation that logs all notifications. +type LoggingHook struct { + LogLevel int // 0=Error, 1=Warn, 2=Info, 3=Debug +} + +// PreHook logs the notification before processing and allows modification. +func (lh *LoggingHook) PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) { + if lh.LogLevel >= 2 { // Info level + // Log the notification type and content + connID := uint64(0) + if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { + connID = conn.GetID() + } + seqID := int64(0) + if slices.Contains(maintenanceNotificationTypes, notificationType) { + // seqID is the second element in the notification array + if len(notification) > 1 { + if parsedSeqID, ok := notification[1].(int64); !ok { + seqID = 0 + } else { + seqID = parsedSeqID + } + } + + } + internal.Logger.Printf(ctx, logs.ProcessingNotification(connID, seqID, notificationType, notification)) + } + return notification, true // Continue processing with unmodified notification +} + +// PostHook logs the result after processing. +func (lh *LoggingHook) PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) { + connID := uint64(0) + if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { + connID = conn.GetID() + } + if result != nil && lh.LogLevel >= 1 { // Warning level + internal.Logger.Printf(ctx, logs.ProcessingNotificationFailed(connID, notificationType, result, notification)) + } else if lh.LogLevel >= 3 { // Debug level + internal.Logger.Printf(ctx, logs.ProcessingNotificationSucceeded(connID, notificationType)) + } +} + +// NewLoggingHook creates a new logging hook with the specified log level. +// Log levels: 0=Error, 1=Warn, 2=Info, 3=Debug +func NewLoggingHook(logLevel int) *LoggingHook { + return &LoggingHook{LogLevel: logLevel} +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go new file mode 100644 index 00000000000..3f9478e1b12 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go @@ -0,0 +1,362 @@ +package maintnotifications + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/interfaces" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/push" +) + +// Push notification type constants for maintenance +const ( + NotificationMoving = "MOVING" // Per-connection handoff notification + NotificationMigrating = "MIGRATING" // Per-connection migration start notification - relaxes timeouts + NotificationMigrated = "MIGRATED" // Per-connection migration complete notification - clears relaxed timeouts + NotificationFailingOver = "FAILING_OVER" // Per-connection failover start notification - relaxes timeouts + NotificationFailedOver = "FAILED_OVER" // Per-connection failover complete notification - clears relaxed timeouts + NotificationSMigrating = "SMIGRATING" // Cluster slot migrating notification - relaxes timeouts + NotificationSMigrated = "SMIGRATED" // Cluster slot migrated notification - unrelaxes timeouts and triggers cluster state reload +) + +// maintenanceNotificationTypes contains all notification types that maintenance handles +var maintenanceNotificationTypes = []string{ + NotificationMoving, + NotificationMigrating, + NotificationMigrated, + NotificationFailingOver, + NotificationFailedOver, + NotificationSMigrating, + NotificationSMigrated, +} + +// NotificationHook is called before and after notification processing +// PreHook can modify the notification and return false to skip processing +// PostHook is called after successful processing +type NotificationHook interface { + PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) + PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) +} + +// MovingOperationKey provides a unique key for tracking MOVING operations +// that combines sequence ID with connection identifier to handle duplicate +// sequence IDs across multiple connections to the same node. +type MovingOperationKey struct { + SeqID int64 // Sequence ID from MOVING notification + ConnID uint64 // Unique connection identifier +} + +// String returns a string representation of the key for debugging +func (k MovingOperationKey) String() string { + return fmt.Sprintf("seq:%d-conn:%d", k.SeqID, k.ConnID) +} + +// Manager provides a simplified upgrade functionality with hooks and atomic state. +type Manager struct { + client interfaces.ClientInterface + config *Config + options interfaces.OptionsInterface + pool pool.Pooler + + // MOVING operation tracking - using sync.Map for better concurrent performance + activeMovingOps sync.Map // map[MovingOperationKey]*MovingOperation + + // SMIGRATED notification deduplication - tracks processed SeqIDs + // Multiple connections may receive the same SMIGRATED notification + processedSMigratedSeqIDs sync.Map // map[int64]bool + + // Atomic state tracking - no locks needed for state queries + activeOperationCount atomic.Int64 // Number of active operations + closed atomic.Bool // Manager closed state + + // Notification hooks for extensibility + hooks []NotificationHook + hooksMu sync.RWMutex // Protects hooks slice + poolHooksRef *PoolHook + + // Cluster state reload callback for SMIGRATED notifications + clusterStateReloadCallback ClusterStateReloadCallback +} + +// MovingOperation tracks an active MOVING operation. +type MovingOperation struct { + SeqID int64 + NewEndpoint string + StartTime time.Time + Deadline time.Time +} + +// ClusterStateReloadCallback is a callback function that triggers cluster state reload. +// This is used by node clients to notify their parent ClusterClient about SMIGRATED notifications. +// The hostPort parameter indicates the destination node (e.g., "127.0.0.1:6379"). +// The slotRanges parameter contains the migrated slots (e.g., ["1234", "5000-6000"]). +// Currently, implementations typically reload the entire cluster state, but in the future +// this could be optimized to reload only the specific slots. +type ClusterStateReloadCallback func(ctx context.Context, hostPort string, slotRanges []string) + +// NewManager creates a new simplified manager. +func NewManager(client interfaces.ClientInterface, pool pool.Pooler, config *Config) (*Manager, error) { + if client == nil { + return nil, ErrInvalidClient + } + + hm := &Manager{ + client: client, + pool: pool, + options: client.GetOptions(), + config: config.Clone(), + hooks: make([]NotificationHook, 0), + } + + // Set up push notification handling + if err := hm.setupPushNotifications(); err != nil { + return nil, err + } + + return hm, nil +} + +// GetPoolHook creates a pool hook with a custom dialer. +func (hm *Manager) InitPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) { + poolHook := hm.createPoolHook(baseDialer) + hm.pool.AddPoolHook(poolHook) +} + +// setupPushNotifications sets up push notification handling by registering with the client's processor. +func (hm *Manager) setupPushNotifications() error { + processor := hm.client.GetPushProcessor() + if processor == nil { + return ErrInvalidClient // Client doesn't support push notifications + } + + // Create our notification handler + handler := &NotificationHandler{manager: hm, operationsManager: hm} + + // Register handlers for all upgrade notifications with the client's processor + for _, notificationType := range maintenanceNotificationTypes { + if err := processor.RegisterHandler(notificationType, handler, true); err != nil { + return errors.New(logs.FailedToRegisterHandler(notificationType, err)) + } + } + + return nil +} + +// TrackMovingOperationWithConnID starts a new MOVING operation with a specific connection ID. +func (hm *Manager) TrackMovingOperationWithConnID(ctx context.Context, newEndpoint string, deadline time.Time, seqID int64, connID uint64) error { + // Create composite key + key := MovingOperationKey{ + SeqID: seqID, + ConnID: connID, + } + + // Create MOVING operation record + movingOp := &MovingOperation{ + SeqID: seqID, + NewEndpoint: newEndpoint, + StartTime: time.Now(), + Deadline: deadline, + } + + // Use LoadOrStore for atomic check-and-set operation + if _, loaded := hm.activeMovingOps.LoadOrStore(key, movingOp); loaded { + // Duplicate MOVING notification, ignore + if internal.LogLevel.DebugOrAbove() { // Debug level + internal.Logger.Printf(context.Background(), logs.DuplicateMovingOperation(connID, newEndpoint, seqID)) + } + return nil + } + if internal.LogLevel.DebugOrAbove() { // Debug level + internal.Logger.Printf(context.Background(), logs.TrackingMovingOperation(connID, newEndpoint, seqID)) + } + + // Increment active operation count atomically + hm.activeOperationCount.Add(1) + + return nil +} + +// UntrackOperationWithConnID completes a MOVING operation with a specific connection ID. +func (hm *Manager) UntrackOperationWithConnID(seqID int64, connID uint64) { + // Create composite key + key := MovingOperationKey{ + SeqID: seqID, + ConnID: connID, + } + + // Remove from active operations atomically + if _, loaded := hm.activeMovingOps.LoadAndDelete(key); loaded { + if internal.LogLevel.DebugOrAbove() { // Debug level + internal.Logger.Printf(context.Background(), logs.UntrackingMovingOperation(connID, seqID)) + } + // Decrement active operation count only if operation existed + hm.activeOperationCount.Add(-1) + } else { + if internal.LogLevel.DebugOrAbove() { // Debug level + internal.Logger.Printf(context.Background(), logs.OperationNotTracked(connID, seqID)) + } + } +} + +// GetActiveMovingOperations returns active operations with composite keys. +// WARNING: This method creates a new map and copies all operations on every call. +// Use sparingly, especially in hot paths or high-frequency logging. +func (hm *Manager) GetActiveMovingOperations() map[MovingOperationKey]*MovingOperation { + result := make(map[MovingOperationKey]*MovingOperation) + + // Iterate over sync.Map to build result + hm.activeMovingOps.Range(func(key, value interface{}) bool { + k := key.(MovingOperationKey) + op := value.(*MovingOperation) + + // Create a copy to avoid sharing references + result[k] = &MovingOperation{ + SeqID: op.SeqID, + NewEndpoint: op.NewEndpoint, + StartTime: op.StartTime, + Deadline: op.Deadline, + } + return true // Continue iteration + }) + + return result +} + +// IsHandoffInProgress returns true if any handoff is in progress. +// Uses atomic counter for lock-free operation. +func (hm *Manager) IsHandoffInProgress() bool { + return hm.activeOperationCount.Load() > 0 +} + +// GetActiveOperationCount returns the number of active operations. +// Uses atomic counter for lock-free operation. +func (hm *Manager) GetActiveOperationCount() int64 { + return hm.activeOperationCount.Load() +} + +// MarkSMigratedSeqIDProcessed attempts to mark a SMIGRATED SeqID as processed. +// Returns true if this is the first time processing this SeqID (should process), +// false if it was already processed (should skip). +// This prevents duplicate processing when multiple connections receive the same notification. +func (hm *Manager) MarkSMigratedSeqIDProcessed(seqID int64) bool { + _, alreadyProcessed := hm.processedSMigratedSeqIDs.LoadOrStore(seqID, true) + return !alreadyProcessed // Return true if NOT already processed +} + +// Close closes the manager. +func (hm *Manager) Close() error { + // Use atomic operation for thread-safe close check + if !hm.closed.CompareAndSwap(false, true) { + return nil // Already closed + } + + // Shutdown the pool hook if it exists + if hm.poolHooksRef != nil { + // Use a timeout to prevent hanging indefinitely + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := hm.poolHooksRef.Shutdown(shutdownCtx) + if err != nil { + // was not able to close pool hook, keep closed state false + hm.closed.Store(false) + return err + } + // Remove the pool hook from the pool + if hm.pool != nil { + hm.pool.RemovePoolHook(hm.poolHooksRef) + } + } + + // Clear all active operations + hm.activeMovingOps.Range(func(key, value interface{}) bool { + hm.activeMovingOps.Delete(key) + return true + }) + + // Reset counter + hm.activeOperationCount.Store(0) + + return nil +} + +// GetState returns current state using atomic counter for lock-free operation. +func (hm *Manager) GetState() State { + if hm.activeOperationCount.Load() > 0 { + return StateMoving + } + return StateIdle +} + +// processPreHooks calls all pre-hooks and returns the modified notification and whether to continue processing. +func (hm *Manager) processPreHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) { + hm.hooksMu.RLock() + defer hm.hooksMu.RUnlock() + + currentNotification := notification + + for _, hook := range hm.hooks { + modifiedNotification, shouldContinue := hook.PreHook(ctx, notificationCtx, notificationType, currentNotification) + if !shouldContinue { + return modifiedNotification, false + } + currentNotification = modifiedNotification + } + + return currentNotification, true +} + +// processPostHooks calls all post-hooks with the processing result. +func (hm *Manager) processPostHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) { + hm.hooksMu.RLock() + defer hm.hooksMu.RUnlock() + + for _, hook := range hm.hooks { + hook.PostHook(ctx, notificationCtx, notificationType, notification, result) + } +} + +// createPoolHook creates a pool hook with this manager already set. +func (hm *Manager) createPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) *PoolHook { + if hm.poolHooksRef != nil { + return hm.poolHooksRef + } + // Get pool size from client options for better worker defaults + poolSize := 0 + if hm.options != nil { + poolSize = hm.options.GetPoolSize() + } + + hm.poolHooksRef = NewPoolHookWithPoolSize(baseDialer, hm.options.GetNetwork(), hm.config, hm, poolSize) + hm.poolHooksRef.SetPool(hm.pool) + + return hm.poolHooksRef +} + +func (hm *Manager) AddNotificationHook(notificationHook NotificationHook) { + hm.hooksMu.Lock() + defer hm.hooksMu.Unlock() + hm.hooks = append(hm.hooks, notificationHook) +} + +// SetClusterStateReloadCallback sets the callback function that will be called when a SMIGRATED notification is received. +// This allows node clients to notify their parent ClusterClient to reload cluster state. +func (hm *Manager) SetClusterStateReloadCallback(callback ClusterStateReloadCallback) { + hm.clusterStateReloadCallback = callback +} + +// TriggerClusterStateReload calls the cluster state reload callback if it's set. +// This is called when a SMIGRATED notification is received. +func (hm *Manager) TriggerClusterStateReload(ctx context.Context, hostPort string, slotRanges []string) { + if hm.clusterStateReloadCallback != nil { + hm.clusterStateReloadCallback(ctx, hostPort, slotRanges) + } +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/pool_hook.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/pool_hook.go new file mode 100644 index 00000000000..9ea0558bf8e --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/pool_hook.go @@ -0,0 +1,182 @@ +package maintnotifications + +import ( + "context" + "net" + "sync" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" +) + +// OperationsManagerInterface defines the interface for completing handoff operations +type OperationsManagerInterface interface { + TrackMovingOperationWithConnID(ctx context.Context, newEndpoint string, deadline time.Time, seqID int64, connID uint64) error + UntrackOperationWithConnID(seqID int64, connID uint64) +} + +// HandoffRequest represents a request to handoff a connection to a new endpoint +type HandoffRequest struct { + Conn *pool.Conn + ConnID uint64 // Unique connection identifier + Endpoint string + SeqID int64 + Pool pool.Pooler // Pool to remove connection from on failure +} + +// PoolHook implements pool.PoolHook for Redis-specific connection handling +// with maintenance notifications support. +type PoolHook struct { + // Base dialer for creating connections to new endpoints during handoffs + // args are network and address + baseDialer func(context.Context, string, string) (net.Conn, error) + + // Network type (e.g., "tcp", "unix") + network string + + // Worker manager for background handoff processing + workerManager *handoffWorkerManager + + // Configuration for the maintenance notifications + config *Config + + // Operations manager interface for operation completion tracking + operationsManager OperationsManagerInterface + + // Pool interface for removing connections on handoff failure + pool pool.Pooler +} + +// NewPoolHook creates a new pool hook +func NewPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error), network string, config *Config, operationsManager OperationsManagerInterface) *PoolHook { + return NewPoolHookWithPoolSize(baseDialer, network, config, operationsManager, 0) +} + +// NewPoolHookWithPoolSize creates a new pool hook with pool size for better worker defaults +func NewPoolHookWithPoolSize(baseDialer func(context.Context, string, string) (net.Conn, error), network string, config *Config, operationsManager OperationsManagerInterface, poolSize int) *PoolHook { + // Apply defaults if config is nil or has zero values + if config == nil { + config = config.ApplyDefaultsWithPoolSize(poolSize) + } + + ph := &PoolHook{ + // baseDialer is used to create connections to new endpoints during handoffs + baseDialer: baseDialer, + network: network, + config: config, + operationsManager: operationsManager, + } + + // Create worker manager + ph.workerManager = newHandoffWorkerManager(config, ph) + + return ph +} + +// SetPool sets the pool interface for removing connections on handoff failure +func (ph *PoolHook) SetPool(pooler pool.Pooler) { + ph.pool = pooler +} + +// GetCurrentWorkers returns the current number of active workers (for testing) +func (ph *PoolHook) GetCurrentWorkers() int { + return ph.workerManager.getCurrentWorkers() +} + +// IsHandoffPending returns true if the given connection has a pending handoff +func (ph *PoolHook) IsHandoffPending(conn *pool.Conn) bool { + return ph.workerManager.isHandoffPending(conn) +} + +// GetPendingMap returns the pending map for testing purposes +func (ph *PoolHook) GetPendingMap() *sync.Map { + return ph.workerManager.getPendingMap() +} + +// GetMaxWorkers returns the max workers for testing purposes +func (ph *PoolHook) GetMaxWorkers() int { + return ph.workerManager.getMaxWorkers() +} + +// GetHandoffQueue returns the handoff queue for testing purposes +func (ph *PoolHook) GetHandoffQueue() chan HandoffRequest { + return ph.workerManager.getHandoffQueue() +} + +// GetCircuitBreakerStats returns circuit breaker statistics for monitoring +func (ph *PoolHook) GetCircuitBreakerStats() []CircuitBreakerStats { + return ph.workerManager.getCircuitBreakerStats() +} + +// ResetCircuitBreakers resets all circuit breakers (useful for testing) +func (ph *PoolHook) ResetCircuitBreakers() { + ph.workerManager.resetCircuitBreakers() +} + +// OnGet is called when a connection is retrieved from the pool +func (ph *PoolHook) OnGet(_ context.Context, conn *pool.Conn, _ bool) (accept bool, err error) { + // Check if connection is marked for handoff + // This prevents using connections that have received MOVING notifications + if conn.ShouldHandoff() { + return false, ErrConnectionMarkedForHandoffWithState + } + + // Check if connection is usable (not in UNUSABLE or CLOSED state) + // This ensures we don't return connections that are currently being handed off or re-authenticated. + if !conn.IsUsable() { + return false, ErrConnectionMarkedForHandoff + } + + return true, nil +} + +// OnPut is called when a connection is returned to the pool +func (ph *PoolHook) OnPut(ctx context.Context, conn *pool.Conn) (shouldPool bool, shouldRemove bool, err error) { + // first check if we should handoff for faster rejection + if !conn.ShouldHandoff() { + // Default behavior (no handoff): pool the connection + return true, false, nil + } + + // check pending handoff to not queue the same connection twice + if ph.workerManager.isHandoffPending(conn) { + // Default behavior (pending handoff): pool the connection + return true, false, nil + } + + if err := ph.workerManager.queueHandoff(conn); err != nil { + // Failed to queue handoff, remove the connection + internal.Logger.Printf(ctx, logs.FailedToQueueHandoff(conn.GetID(), err)) + // Don't pool, remove connection, no error to caller + return false, true, nil + } + + // Check if handoff was already processed by a worker before we can mark it as queued + if !conn.ShouldHandoff() { + // Handoff was already processed - this is normal and the connection should be pooled + return true, false, nil + } + + if err := conn.MarkQueuedForHandoff(); err != nil { + // If marking fails, check if handoff was processed in the meantime + if !conn.ShouldHandoff() { + // Handoff was processed - this is normal, pool the connection + return true, false, nil + } + // Other error - remove the connection + return false, true, nil + } + internal.Logger.Printf(ctx, logs.MarkedForHandoff(conn.GetID())) + return true, false, nil +} + +func (ph *PoolHook) OnRemove(_ context.Context, _ *pool.Conn, _ error) { + // Not used +} + +// Shutdown gracefully shuts down the processor, waiting for workers to complete +func (ph *PoolHook) Shutdown(ctx context.Context) error { + return ph.workerManager.shutdownWorkers(ctx) +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go new file mode 100644 index 00000000000..7108265b2ca --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go @@ -0,0 +1,524 @@ +package maintnotifications + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/maintnotifications/logs" + "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/push" +) + +// NotificationHandler handles push notifications for the simplified manager. +type NotificationHandler struct { + manager *Manager + operationsManager OperationsManagerInterface +} + +// HandlePushNotification processes push notifications with hook support. +func (snh *NotificationHandler) HandlePushNotification(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) == 0 { + internal.Logger.Printf(ctx, logs.InvalidNotificationFormat(notification)) + return ErrInvalidNotification + } + + notificationType, ok := notification[0].(string) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidNotificationTypeFormat(notification[0])) + return ErrInvalidNotification + } + + // Process pre-hooks - they can modify the notification or skip processing + modifiedNotification, shouldContinue := snh.manager.processPreHooks(ctx, handlerCtx, notificationType, notification) + if !shouldContinue { + return nil // Hooks decided to skip processing + } + + var err error + switch notificationType { + case NotificationMoving: + err = snh.handleMoving(ctx, handlerCtx, modifiedNotification) + case NotificationMigrating: + err = snh.handleMigrating(ctx, handlerCtx, modifiedNotification) + case NotificationMigrated: + err = snh.handleMigrated(ctx, handlerCtx, modifiedNotification) + case NotificationFailingOver: + err = snh.handleFailingOver(ctx, handlerCtx, modifiedNotification) + case NotificationFailedOver: + err = snh.handleFailedOver(ctx, handlerCtx, modifiedNotification) + case NotificationSMigrating: + err = snh.handleSMigrating(ctx, handlerCtx, modifiedNotification) + case NotificationSMigrated: + err = snh.handleSMigrated(ctx, handlerCtx, modifiedNotification) + default: + // Ignore other notification types (e.g., pub/sub messages) + err = nil + } + + // Record maintenance notification metric + if maintenanceCallback := pool.GetMetricMaintenanceNotificationCallback(); maintenanceCallback != nil { + if conn, ok := handlerCtx.Conn.(*pool.Conn); ok { + maintenanceCallback(ctx, conn, notificationType) + } + } + + // Process post-hooks with the result + snh.manager.processPostHooks(ctx, handlerCtx, notificationType, modifiedNotification, err) + + return err +} + +// handleMoving processes MOVING notifications. +// MOVING indicates that a connection should be handed off to a new endpoint. +// This is a per-connection notification that triggers connection handoff. +// Expected format: ["MOVING", seqNum, timeS, endpoint] +func (snh *NotificationHandler) handleMoving(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 3 { + internal.Logger.Printf(ctx, logs.InvalidNotification("MOVING", notification)) + return ErrInvalidNotification + } + seqID, ok := notification[1].(int64) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidSeqIDInMovingNotification(notification[1])) + return ErrInvalidNotification + } + + // Extract timeS + timeS, ok := notification[2].(int64) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidTimeSInMovingNotification(notification[2])) + return ErrInvalidNotification + } + + newEndpoint := "" + if len(notification) > 3 { + // Extract new endpoint + newEndpoint, ok = notification[3].(string) + if !ok { + stringified := fmt.Sprintf("%v", notification[3]) + // this could be which is valid + if notification[3] == nil || stringified == internal.RedisNull { + newEndpoint = "" + } else { + internal.Logger.Printf(ctx, logs.InvalidNewEndpointInMovingNotification(notification[3])) + return ErrInvalidNotification + } + } + } + + // Get the connection that received this notification + conn := handlerCtx.Conn + if conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("MOVING")) + return ErrInvalidNotification + } + + // Type assert to get the underlying pool connection + var poolConn *pool.Conn + if pc, ok := conn.(*pool.Conn); ok { + poolConn = pc + } else { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("MOVING", conn, handlerCtx)) + return ErrInvalidNotification + } + + // If the connection is closed or not pooled, we can ignore the notification + // this connection won't be remembered by the pool and will be garbage collected + // Keep pubsub connections around since they are not pooled but are long-lived + // and should be allowed to handoff (the pubsub instance will reconnect and change + // the underlying *pool.Conn) + if (poolConn.IsClosed() || !poolConn.IsPooled()) && !poolConn.IsPubSub() { + return nil + } + + deadline := time.Now().Add(time.Duration(timeS) * time.Second) + // If newEndpoint is empty, we should schedule a handoff to the current endpoint in timeS/2 seconds + if newEndpoint == "" || newEndpoint == internal.RedisNull { + if internal.LogLevel.DebugOrAbove() { + internal.Logger.Printf(ctx, logs.SchedulingHandoffToCurrentEndpoint(poolConn.GetID(), float64(timeS)/2)) + } + // same as current endpoint + newEndpoint = snh.manager.options.GetAddr() + // delay the handoff for timeS/2 seconds to the same endpoint + // do this in a goroutine to avoid blocking the notification handler + // NOTE: This timer is started while parsing the notification, so the connection is not marked for handoff + // and there should be no possibility of a race condition or double handoff. + time.AfterFunc(time.Duration(timeS/2)*time.Second, func() { + if poolConn == nil || poolConn.IsClosed() { + return + } + if err := snh.markConnForHandoff(poolConn, newEndpoint, seqID, deadline); err != nil { + // Log error but don't fail the goroutine - use background context since original may be cancelled + internal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err)) + return + } + + // Queue the handoff immediately if the connection is idle in the pool. + // If the connection is in use (StateInUse), it will be queued when returned to the pool via OnPut. + // This handles the case where the connection is idle and might never be retrieved again. + if poolConn.GetStateMachine().GetState() == pool.StateIdle { + if snh.manager.poolHooksRef != nil && snh.manager.poolHooksRef.workerManager != nil { + if err := snh.manager.poolHooksRef.workerManager.queueHandoff(poolConn); err != nil { + internal.Logger.Printf(context.Background(), logs.FailedToQueueHandoff(poolConn.GetID(), err)) + } else { + // Mark the connection as queued for handoff to prevent it from being retrieved + // This transitions the connection to StateUnusable + if err := poolConn.MarkQueuedForHandoff(); err != nil { + internal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err)) + } else { + internal.Logger.Printf(context.Background(), logs.MarkedForHandoff(poolConn.GetID())) + } + } + } + } + // If connection is StateInUse, the handoff will be queued when it's returned to the pool + }) + return nil + } + + return snh.markConnForHandoff(poolConn, newEndpoint, seqID, deadline) +} + +func (snh *NotificationHandler) markConnForHandoff(conn *pool.Conn, newEndpoint string, seqID int64, deadline time.Time) error { + if err := conn.MarkForHandoff(newEndpoint, seqID); err != nil { + internal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(conn.GetID(), err)) + // Connection is already marked for handoff, which is acceptable + // This can happen if multiple MOVING notifications are received for the same connection + return nil + } + // Optionally track in m + if snh.operationsManager != nil { + connID := conn.GetID() + // Track the operation (ignore errors since this is optional) + _ = snh.operationsManager.TrackMovingOperationWithConnID(context.Background(), newEndpoint, deadline, seqID, connID) + } else { + return errors.New(logs.ManagerNotInitialized()) + } + return nil +} + +// handleMigrating processes MIGRATING notifications. +// MIGRATING indicates that a connection migration is starting. +// This is a per-connection notification that applies relaxed timeouts. +// Expected format: ["MIGRATING", ...] +func (snh *NotificationHandler) handleMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 2 { + internal.Logger.Printf(ctx, logs.InvalidNotification("MIGRATING", notification)) + return ErrInvalidNotification + } + + if handlerCtx.Conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("MIGRATING")) + return ErrInvalidNotification + } + + conn, ok := handlerCtx.Conn.(*pool.Conn) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("MIGRATING", handlerCtx.Conn, handlerCtx)) + return ErrInvalidNotification + } + + // Apply relaxed timeout to this specific connection + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), "MIGRATING", snh.manager.config.RelaxedTimeout)) + } + conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout) + + // Record relaxed timeout metric + if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil { + relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "MIGRATING") + } + + return nil +} + +// handleMigrated processes MIGRATED notifications. +// MIGRATED indicates that a connection migration has completed. +// This is a per-connection notification that clears relaxed timeouts. +// Expected format: ["MIGRATED", ...] +func (snh *NotificationHandler) handleMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 2 { + internal.Logger.Printf(ctx, logs.InvalidNotification("MIGRATED", notification)) + return ErrInvalidNotification + } + + if handlerCtx.Conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("MIGRATED")) + return ErrInvalidNotification + } + + conn, ok := handlerCtx.Conn.(*pool.Conn) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("MIGRATED", handlerCtx.Conn, handlerCtx)) + return ErrInvalidNotification + } + + // Clear relaxed timeout for this specific connection + if internal.LogLevel.InfoOrAbove() { + connID := conn.GetID() + internal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID)) + } + conn.ClearRelaxedTimeout() + return nil +} + +// handleFailingOver processes FAILING_OVER notifications. +// FAILING_OVER indicates that a failover is starting. +// This is a per-connection notification that applies relaxed timeouts. +// Expected format: ["FAILING_OVER", ...] +func (snh *NotificationHandler) handleFailingOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 2 { + internal.Logger.Printf(ctx, logs.InvalidNotification("FAILING_OVER", notification)) + return ErrInvalidNotification + } + + if handlerCtx.Conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("FAILING_OVER")) + return ErrInvalidNotification + } + + conn, ok := handlerCtx.Conn.(*pool.Conn) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("FAILING_OVER", handlerCtx.Conn, handlerCtx)) + return ErrInvalidNotification + } + + // Apply relaxed timeout to this specific connection + if internal.LogLevel.InfoOrAbove() { + connID := conn.GetID() + internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(connID, "FAILING_OVER", snh.manager.config.RelaxedTimeout)) + } + conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout) + + // Record relaxed timeout metric + if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil { + relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "FAILING_OVER") + } + + return nil +} + +// handleFailedOver processes FAILED_OVER notifications. +// FAILED_OVER indicates that a failover has completed. +// This is a per-connection notification that clears relaxed timeouts. +// Expected format: ["FAILED_OVER", ...] +func (snh *NotificationHandler) handleFailedOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 2 { + internal.Logger.Printf(ctx, logs.InvalidNotification("FAILED_OVER", notification)) + return ErrInvalidNotification + } + + if handlerCtx.Conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("FAILED_OVER")) + return ErrInvalidNotification + } + + conn, ok := handlerCtx.Conn.(*pool.Conn) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("FAILED_OVER", handlerCtx.Conn, handlerCtx)) + return ErrInvalidNotification + } + + // Clear relaxed timeout for this specific connection + if internal.LogLevel.InfoOrAbove() { + connID := conn.GetID() + internal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID)) + } + conn.ClearRelaxedTimeout() + return nil +} + +// handleSMigrating processes SMIGRATING notifications. +// SMIGRATING indicates that a cluster slot is in the process of migrating to a different node. +// This is a per-connection notification that applies relaxed timeouts during slot migration. +// Expected format: ["SMIGRATING", SeqID, slot/range1-range2, ...] +func (snh *NotificationHandler) handleSMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + if len(notification) < 3 { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATING", notification)) + return ErrInvalidNotification + } + + // Validate SeqID (position 1) + if _, ok := notification[1].(int64); !ok { + internal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratingNotification(notification[1])) + return ErrInvalidNotification + } + + if handlerCtx.Conn == nil { + internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("SMIGRATING")) + return ErrInvalidNotification + } + + conn, ok := handlerCtx.Conn.(*pool.Conn) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("SMIGRATING", handlerCtx.Conn, handlerCtx)) + return ErrInvalidNotification + } + + // Apply relaxed timeout to this specific connection + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), "SMIGRATING", snh.manager.config.RelaxedTimeout)) + } + conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout) + return nil +} + +// handleSMigrated processes SMIGRATED notifications. +// SMIGRATED indicates that a cluster slot has finished migrating to a different node. +// This is a cluster-level notification that triggers cluster state reload. +// +// Expected RESP3 format: +// +// >3 +// +SMIGRATED +// :SeqID +// * <- array of triplet arrays +// *3 <- each triplet is a 3-element array +// + <- node from which slots are migrating FROM +// + <- node to which slots are migrating TO +// + <- comma-separated slots and/or ranges (e.g., "123,789-1000") +// +// A source and target endpoint may appear in multiple triplets. +// The notification is only processed if the connection's NodeAddress matches one of the source endpoints. +// +// Note: Multiple connections may receive the same notification, so we deduplicate by SeqID before triggering reload. +// but we still process the notification on each connection to clear the relaxed timeout. +// In the case when the connection is from MOVED/ASK, the connection's original endpoint is not set, +// so we will not be able to match the source endpoint. In such case, we will trigger the reload callback with the first target endpoint. +func (snh *NotificationHandler) handleSMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error { + // Expected: ["SMIGRATED", SeqID, [[source, target, slots], ...]] + // Minimum 3 elements: SMIGRATED, SeqID, and the array of triplets + if len(notification) < 3 { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED", notification)) + return ErrInvalidNotification + } + + // Extract SeqID (position 1) + seqID, ok := notification[1].(int64) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratedNotification(notification[1])) + return ErrInvalidNotification + } + + // Extract the array of triplets (position 2) + triplets, ok := notification[2].([]interface{}) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (triplets array)", notification[2])) + return ErrInvalidNotification + } + + if len(triplets) == 0 { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (empty triplets)", notification)) + return ErrInvalidNotification + } + + // Get the connection's endpoints to check if this notification is relevant + // We check against both nodeAddress (from CLUSTER SLOTS) and addr (after resolution) + // since we cannot be certain which format the notification source will use + var connectionNodeAddress string + var connectionAddr string + if snh.manager.options != nil { + connectionNodeAddress = snh.manager.options.GetNodeAddress() + connectionAddr = snh.manager.options.GetAddr() + } + + // Helper function to check if source matches either of our endpoints + // notification source can be either the node address or the addr after resolution + sourceMatchesConnection := func(source string) bool { + if source == connectionNodeAddress { + return true + } + if source == connectionAddr { + return true + } + return false + } + + // Parse triplets and check if any source matches our connection's endpoints + var matchingTriplets []struct { + source string + target string + slots string + } + var allSlotRanges []string + + for _, tripletInterface := range triplets { + // Each triplet should be a 3-element array: [source, target, slots] + triplet, ok := tripletInterface.([]interface{}) + if !ok || len(triplet) != 3 { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (triplet format)", tripletInterface)) + continue + } + + // Extract source endpoint + source, ok := triplet[0].(string) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (source)", triplet[0])) + continue + } + + // Extract target endpoint + target, ok := triplet[1].(string) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (target)", triplet[1])) + continue + } + + // Extract slots + slots, ok := triplet[2].(string) + if !ok { + internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (slots)", triplet[2])) + continue + } + + // Check if this triplet's source matches our connection's endpoints + if sourceMatchesConnection(source) { + matchingTriplets = append(matchingTriplets, struct { + source string + target string + slots string + }{source, target, slots}) + slotRanges := strings.Split(slots, ",") + allSlotRanges = append(allSlotRanges, slotRanges...) + } + } + + var connID uint64 + // Reset relaxed timeout for this specific connection + if handlerCtx.Conn != nil { + conn, ok := handlerCtx.Conn.(*pool.Conn) + if ok { + if internal.LogLevel.InfoOrAbove() { + connID = conn.GetID() + internal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID)) + } + conn.ClearRelaxedTimeout() + } + } + + // If no matching triplets, this notification is not relevant to this connection + if len(matchingTriplets) == 0 { + return nil + } + + // Deduplicate by SeqID - multiple connections may receive the same notification + // Only trigger cluster state reload once per seqID + if snh.manager.MarkSMigratedSeqIDProcessed(seqID) { + // Use the first matching triplet + target := matchingTriplets[0].target + slotsForLog := allSlotRanges + + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(ctx, logs.TriggeringClusterStateReload(seqID, target, slotsForLog)) + } + + // Trigger cluster state reload via callback + snh.manager.TriggerClusterStateReload(ctx, target, slotsForLog) + } + + return nil +} diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/state.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/state.go new file mode 100644 index 00000000000..8180bcd97d9 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/state.go @@ -0,0 +1,24 @@ +package maintnotifications + +// State represents the current state of a maintenance operation +type State int + +const ( + // StateIdle indicates no upgrade is in progress + StateIdle State = iota + + // StateHandoff indicates a connection handoff is in progress + StateMoving +) + +// String returns a string representation of the state. +func (s State) String() string { + switch s { + case StateIdle: + return "idle" + case StateMoving: + return "moving" + default: + return "unknown" + } +} diff --git a/vendor/github.com/redis/go-redis/v9/options.go b/vendor/github.com/redis/go-redis/v9/options.go index b87a234a411..ba45a0cb818 100644 --- a/vendor/github.com/redis/go-redis/v9/options.go +++ b/vendor/github.com/redis/go-redis/v9/options.go @@ -5,18 +5,34 @@ import ( "crypto/tls" "errors" "fmt" + "maps" "net" "net/url" "runtime" - "sort" + "slices" "strconv" "strings" + "sync/atomic" "time" "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/internal/proto" + "github.com/redis/go-redis/v9/internal/util" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/redis/go-redis/v9/push" ) +// poolIDCounter is a global auto-increment counter for generating unique pool IDs. +var poolIDCounter atomic.Uint64 + +// generateUniqueID generates a short unique identifier for pool names using auto-increment. +// This makes it easier to identify and track pools in order of creation. +func generateUniqueID() string { + id := poolIDCounter.Add(1) + return strconv.FormatUint(id, 10) +} + // Limiter is the interface of a rate limiter or a circuit breaker. type Limiter interface { // Allow returns nil if operation is allowed or an error otherwise. @@ -30,7 +46,6 @@ type Limiter interface { // Options keeps the settings to set up redis connection. type Options struct { - // Network type, either tcp or unix. // // default: is tcp. @@ -39,6 +54,17 @@ type Options struct { // Addr is the address formated as host:port Addr string + // NodeAddress is the address of the Redis node as reported by the server. + // For cluster clients, this is the exact endpoint string returned by CLUSTER SLOTS + // before any resolution or transformation (e.g., loopback replacement). + // For standalone clients, this defaults to Addr. + // + // This is used to match the source endpoint in maintenance notifications + // (e.g. SMIGRATED). + // + // Use Client.NodeAddress() to access this value. + NodeAddress string + // ClientName will execute the `CLIENT SETNAME ClientName` command for each conn. ClientName string @@ -108,6 +134,23 @@ type Options struct { // default: 5 seconds DialTimeout time.Duration + // DialerRetries is the maximum number of retry attempts when dialing fails. + // + // default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // + // default: 100 milliseconds + DialerRetryTimeout time.Duration + + // DialerRetryBackoff controls the delay between dial retry attempts. + // + // attempt is 0-based: attempt=0 is the delay after the 1st failed dial (before the 2nd attempt). + // + // If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms). + DialerRetryBackoff func(attempt int) time.Duration + // ReadTimeout for socket reads. If reached, commands will fail // with a timeout instead of blocking. Supported values: // @@ -130,6 +173,20 @@ type Options struct { // See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts ContextTimeoutEnabled bool + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + WriteBufferSize int + // PoolFIFO type of connection pool. // // - true for FIFO pool @@ -137,6 +194,7 @@ type Options struct { // // Note that FIFO has slightly higher overhead compared to LIFO, // but it helps closing idle connections faster reducing the pool size. + // default: false PoolFIFO bool // PoolSize is the base number of socket connections. @@ -147,6 +205,10 @@ type Options struct { // default: 10 * runtime.GOMAXPROCS(0) PoolSize int + // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines. + // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize. + MaxConcurrentDials int + // PoolTimeout is the amount of time client waits for connection if all connections // are busy before returning an error. // @@ -168,6 +230,8 @@ type Options struct { // MaxActiveConns is the maximum number of connections allocated by the pool at a given time. // When zero, there is no limit on the number of connections in the pool. // If the pool is full, the next call to Get() will block until a connection is released. + // + // default: 0 MaxActiveConns int // ConnMaxIdleTime is the maximum amount of time a connection may be idle. @@ -188,6 +252,19 @@ type Options struct { // default: 0 ConnMaxLifetime time.Duration + // ConnMaxLifetimeJitter is the absolute jitter duration applied to ConnMaxLifetime + // to prevent all connections from expiring simultaneously. + // + // The jitter is applied as a random offset in the range [-jitter, +jitter]. + // For example, if ConnMaxLifetime is 1 hour and ConnMaxLifetimeJitter is 6 minutes, + // connections will expire between 54 minutes and 66 minutes. + // + // If <= 0, no jitter is applied. + // If > ConnMaxLifetime, it will be capped at ConnMaxLifetime. + // + // default: 0 + ConnMaxLifetimeJitter time.Duration + // TLSConfig to use. When set, TLS will be negotiated. TLSConfig *tls.Config @@ -213,9 +290,29 @@ type Options struct { // IdentitySuffix - add suffix to client name. IdentitySuffix string - // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. - // When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool + + // Push notifications are always enabled for RESP3 connections (Protocol: 3) + // and are not available for RESP2 connections. No configuration option is needed. + + // PushNotificationProcessor is the processor for handling push notifications. + // If nil, a default processor will be created for RESP3 connections. + PushNotificationProcessor push.NotificationProcessor + + // FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing. + // When a node is marked as failing, it will be avoided for this duration. + // Default is 15 seconds. + FailingTimeoutSeconds int + + // MaintNotificationsConfig provides custom configuration for maintnotifications. + // When MaintNotificationsConfig.Mode is not "disabled", the client will handle + // cluster upgrade notifications gracefully and manage connection/pool state + // transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications. + // If nil, maintnotifications are in "auto" mode and will be enabled if the server supports it. + MaintNotificationsConfig *maintnotifications.Config } func (opt *Options) init() { @@ -229,18 +326,41 @@ func (opt *Options) init() { opt.Network = "tcp" } } + // For standalone clients, default NodeAddress to Addr if not set. + // This ensures maintenance notifications (SMIGRATED, etc.) can match + // the connection's endpoint even for non-cluster clients. + if opt.NodeAddress == "" { + opt.NodeAddress = opt.Addr + } if opt.Protocol < 2 { opt.Protocol = 3 } if opt.DialTimeout == 0 { opt.DialTimeout = 5 * time.Second } + if opt.DialerRetries == 0 { + opt.DialerRetries = 5 + } + if opt.DialerRetryTimeout == 0 { + opt.DialerRetryTimeout = 100 * time.Millisecond + } if opt.Dialer == nil { opt.Dialer = NewDialer(opt) } if opt.PoolSize == 0 { opt.PoolSize = 10 * runtime.GOMAXPROCS(0) } + if opt.MaxConcurrentDials <= 0 { + opt.MaxConcurrentDials = opt.PoolSize + } else if opt.MaxConcurrentDials > opt.PoolSize { + opt.MaxConcurrentDials = opt.PoolSize + } + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } switch opt.ReadTimeout { case -2: opt.ReadTimeout = -1 @@ -268,6 +388,8 @@ func (opt *Options) init() { opt.ConnMaxIdleTime = 30 * time.Minute } + opt.ConnMaxLifetimeJitter = min(opt.ConnMaxLifetimeJitter, opt.ConnMaxLifetime) + switch opt.MaxRetries { case -1: opt.MaxRetries = 0 @@ -286,13 +408,40 @@ func (opt *Options) init() { case 0: opt.MaxRetryBackoff = 512 * time.Millisecond } + + if opt.FailingTimeoutSeconds == 0 { + opt.FailingTimeoutSeconds = 15 + } + + opt.MaintNotificationsConfig = opt.MaintNotificationsConfig.ApplyDefaultsWithPoolConfig(opt.PoolSize, opt.MaxActiveConns) + + // auto-detect endpoint type if not specified + endpointType := opt.MaintNotificationsConfig.EndpointType + if endpointType == "" || endpointType == maintnotifications.EndpointTypeAuto { + // Auto-detect endpoint type if not specified + endpointType = maintnotifications.DetectEndpointType(opt.Addr, opt.TLSConfig != nil) + } + opt.MaintNotificationsConfig.EndpointType = endpointType } func (opt *Options) clone() *Options { clone := *opt + + // Deep clone MaintNotificationsConfig to avoid sharing between clients + if opt.MaintNotificationsConfig != nil { + configClone := *opt.MaintNotificationsConfig + clone.MaintNotificationsConfig = &configClone + } + return &clone } +// NewDialer returns a function that will be used as the default dialer +// when none is specified in Options.Dialer. +func (opt *Options) NewDialer() func(context.Context, string, string) (net.Conn, error) { + return NewDialer(opt) +} + // NewDialer returns a function that will be used as the default dialer // when none is specified in Options.Dialer. func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) { @@ -504,11 +653,8 @@ func (o *queryOptions) remaining() []string { if len(o.q) == 0 { return nil } - keys := make([]string, 0, len(o.q)) - for k := range o.q { - keys = append(keys, k) - } - sort.Strings(keys) + keys := slices.Collect(maps.Keys(o.q)) + slices.Sort(keys) return keys } @@ -539,6 +685,7 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) { o.MinIdleConns = q.int("min_idle_conns") o.MaxIdleConns = q.int("max_idle_conns") o.MaxActiveConns = q.int("max_active_conns") + o.MaxConcurrentDials = q.int("max_concurrent_dials") if q.has("conn_max_idle_time") { o.ConnMaxIdleTime = q.duration("conn_max_idle_time") } else { @@ -549,6 +696,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) { } else { o.ConnMaxLifetime = q.duration("max_conn_age") } + if q.has("conn_max_lifetime_jitter") { + o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime) + } if q.err != nil { return nil, q.err } @@ -578,19 +728,96 @@ func getUserPassword(u *url.URL) (string, string) { func newConnPool( opt *Options, dialer func(ctx context.Context, network, addr string) (net.Conn, error), -) *pool.ConnPool { + poolName string, +) (*pool.ConnPool, error) { + poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize") + if err != nil { + return nil, err + } + + minIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, "MinIdleConns") + if err != nil { + return nil, err + } + + maxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, "MaxIdleConns") + if err != nil { + return nil, err + } + + maxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, "MaxActiveConns") + if err != nil { + return nil, err + } + return pool.NewConnPool(&pool.Options{ Dialer: func(ctx context.Context) (net.Conn, error) { return dialer(ctx, opt.Network, opt.Addr) }, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - DialTimeout: opt.DialTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, - }) + PoolFIFO: opt.PoolFIFO, + PoolSize: poolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + MinIdleConns: minIdleConns, + MaxIdleConns: maxIdleConns, + MaxActiveConns: maxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, + PushNotificationsEnabled: opt.Protocol == 3, + Name: poolName, + }), nil +} + +func newPubSubPool( + opt *Options, + dialer func(ctx context.Context, network, addr string) (net.Conn, error), + poolName string, +) (*pool.PubSubPool, error) { + poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize") + if err != nil { + return nil, err + } + + minIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, "MinIdleConns") + if err != nil { + return nil, err + } + + maxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, "MaxIdleConns") + if err != nil { + return nil, err + } + + maxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, "MaxActiveConns") + if err != nil { + return nil, err + } + + return pool.NewPubSubPool(&pool.Options{ + PoolFIFO: opt.PoolFIFO, + PoolSize: poolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + MinIdleConns: minIdleConns, + MaxIdleConns: maxIdleConns, + MaxActiveConns: maxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, + ReadBufferSize: 32 * 1024, + WriteBufferSize: 32 * 1024, + PushNotificationsEnabled: opt.Protocol == 3, + Name: poolName, + }, dialer), nil } diff --git a/vendor/github.com/redis/go-redis/v9/osscluster.go b/vendor/github.com/redis/go-redis/v9/osscluster.go index 6c6b7563803..efd52960c4c 100644 --- a/vendor/github.com/redis/go-redis/v9/osscluster.go +++ b/vendor/github.com/redis/go-redis/v9/osscluster.go @@ -1,13 +1,17 @@ package redis import ( + "cmp" "context" "crypto/tls" + "errors" "fmt" "math" + "math/rand" "net" "net/url" "runtime" + "slices" "sort" "strings" "sync" @@ -17,16 +21,23 @@ import ( "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" + "github.com/redis/go-redis/v9/internal/otel" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" - "github.com/redis/go-redis/v9/internal/rand" + "github.com/redis/go-redis/v9/internal/routing" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/redis/go-redis/v9/push" ) const ( minLatencyMeasurementInterval = 10 * time.Second ) -var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes") +var ( + errClusterNoNodes = errors.New("redis: cluster has no nodes") + errNoWatchKeys = errors.New("redis: Watch requires at least one key") + errWatchCrosslot = errors.New("redis: Watch requires all keys to be in the same slot") +) // ClusterOptions are used to configure a cluster client and should be // passed to NewClusterClient. @@ -38,6 +49,7 @@ type ClusterOptions struct { ClientName string // NewClient creates a cluster node client with provided name and options. + // If NewClient is set by the user, the user is responsible for handling maintnotifications upgrades and push notifications. NewClient func(opt *Options) *Client // The maximum number of retries before giving up. Command is retried @@ -74,26 +86,69 @@ type ClusterOptions struct { CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) StreamingCredentialsProvider auth.StreamingCredentialsProvider + // MaxRetries is the maximum number of retries before giving up. + // For ClusterClient, retries are disabled by default (set to -1), + // because the cluster client handles all kinds of retries internally. + // This is intentional and differs from the standalone Options default. MaxRetries int MinRetryBackoff time.Duration MaxRetryBackoff time.Duration - DialTimeout time.Duration + DialTimeout time.Duration + + // DialerRetries is the maximum number of retry attempts when dialing fails. + // + // default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // + // default: 100 milliseconds + DialerRetryTimeout time.Duration + + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool - PoolFIFO bool - PoolSize int // applies per cluster node and not for the whole cluster - PoolTimeout time.Duration - MinIdleConns int - MaxIdleConns int - MaxActiveConns int // applies per cluster node and not for the whole cluster - ConnMaxIdleTime time.Duration - ConnMaxLifetime time.Duration + // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines. + // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize. + MaxConcurrentDials int + + PoolFIFO bool + PoolSize int // applies per cluster node and not for the whole cluster + PoolTimeout time.Duration + MinIdleConns int + MaxIdleConns int + MaxActiveConns int // applies per cluster node and not for the whole cluster + ConnMaxIdleTime time.Duration + ConnMaxLifetime time.Duration + ConnMaxLifetimeJitter time.Duration + + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + WriteBufferSize int TLSConfig *tls.Config + // DisableRoutingPolicies disables the request/response policy routing system. + // When disabled, all commands use the legacy routing behavior. + // Experimental. Will be removed when shard picker is fully implemented. + DisableRoutingPolicies bool + // DisableIndentity - Disable set-lib on connect. // // default: false @@ -108,8 +163,35 @@ type ClusterOptions struct { IdentitySuffix string // Add suffix to client name. Default is empty. - // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool + + // PushNotificationProcessor is the processor for handling push notifications. + // If nil, a default processor will be created for RESP3 connections. + PushNotificationProcessor push.NotificationProcessor + + // FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing. + // When a node is marked as failing, it will be avoided for this duration. + // Default is 15 seconds. + FailingTimeoutSeconds int + + // MaintNotificationsConfig provides custom configuration for maintnotifications upgrades. + // When MaintNotificationsConfig.Mode is not "disabled", the client will handle + // cluster upgrade notifications gracefully and manage connection/pool state + // transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications. + // If nil, maintnotifications upgrades are in "auto" mode and will be enabled if the server supports it. + // The ClusterClient supports SMIGRATING and SMIGRATED notifications for cluster state management. + // Individual node clients handle other maintenance notifications (MOVING, MIGRATING, etc.). + MaintNotificationsConfig *maintnotifications.Config + // ShardPicker is used to pick a shard when the request_policy is + // ReqDefault and the command has no keys. + ShardPicker routing.ShardPicker + + // ClusterStateReloadInterval is the interval for reloading the cluster state. + // Default is 10 seconds. + ClusterStateReloadInterval time.Duration } func (opt *ClusterOptions) init() { @@ -124,9 +206,30 @@ func (opt *ClusterOptions) init() { opt.ReadOnly = true } + if opt.DialTimeout == 0 { + opt.DialTimeout = 5 * time.Second + } + if opt.DialerRetries == 0 { + opt.DialerRetries = 5 + } + if opt.DialerRetryTimeout == 0 { + opt.DialerRetryTimeout = 100 * time.Millisecond + } + if opt.PoolSize == 0 { opt.PoolSize = 5 * runtime.GOMAXPROCS(0) } + if opt.MaxConcurrentDials <= 0 { + opt.MaxConcurrentDials = opt.PoolSize + } else if opt.MaxConcurrentDials > opt.PoolSize { + opt.MaxConcurrentDials = opt.PoolSize + } + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } switch opt.ReadTimeout { case -1: @@ -160,6 +263,18 @@ func (opt *ClusterOptions) init() { if opt.NewClient == nil { opt.NewClient = NewClient } + + if opt.FailingTimeoutSeconds == 0 { + opt.FailingTimeoutSeconds = 15 + } + + if opt.ShardPicker == nil { + opt.ShardPicker = &routing.RoundRobinPicker{} + } + + if opt.ClusterStateReloadInterval == 0 { + opt.ClusterStateReloadInterval = 10 * time.Second + } } // ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis. @@ -254,16 +369,23 @@ func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, er o.MinRetryBackoff = q.duration("min_retry_backoff") o.MaxRetryBackoff = q.duration("max_retry_backoff") o.DialTimeout = q.duration("dial_timeout") + o.DialerRetries = q.int("dialer_retries") + o.DialerRetryTimeout = q.duration("dialer_retry_timeout") o.ReadTimeout = q.duration("read_timeout") o.WriteTimeout = q.duration("write_timeout") o.PoolFIFO = q.bool("pool_fifo") o.PoolSize = q.int("pool_size") + o.MaxConcurrentDials = q.int("max_concurrent_dials") o.MinIdleConns = q.int("min_idle_conns") o.MaxIdleConns = q.int("max_idle_conns") o.MaxActiveConns = q.int("max_active_conns") o.PoolTimeout = q.duration("pool_timeout") o.ConnMaxLifetime = q.duration("conn_max_lifetime") + if q.has("conn_max_lifetime_jitter") { + o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime) + } o.ConnMaxIdleTime = q.duration("conn_max_idle_time") + o.FailingTimeoutSeconds = q.int("failing_timeout_seconds") if q.err != nil { return nil, q.err @@ -289,6 +411,13 @@ func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, er } func (opt *ClusterOptions) clientOptions() *Options { + // Clone MaintNotificationsConfig to avoid sharing between cluster node clients + var maintNotificationsConfig *maintnotifications.Config + if opt.MaintNotificationsConfig != nil { + configClone := *opt.MaintNotificationsConfig + maintNotificationsConfig = &configClone + } + return &Options{ ClientName: opt.ClientName, Dialer: opt.Dialer, @@ -305,30 +434,41 @@ func (opt *ClusterOptions) clientOptions() *Options { MinRetryBackoff: opt.MinRetryBackoff, MaxRetryBackoff: opt.MaxRetryBackoff, - DialTimeout: opt.DialTimeout, - ReadTimeout: opt.ReadTimeout, - WriteTimeout: opt.WriteTimeout, + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + ContextTimeoutEnabled: opt.ContextTimeoutEnabled, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, - DisableIdentity: opt.DisableIdentity, - DisableIndentity: opt.DisableIdentity, - IdentitySuffix: opt.IdentitySuffix, - TLSConfig: opt.TLSConfig, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + MaxActiveConns: opt.MaxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, + DisableIdentity: opt.DisableIdentity, + DisableIndentity: opt.DisableIdentity, + IdentitySuffix: opt.IdentitySuffix, + FailingTimeoutSeconds: opt.FailingTimeoutSeconds, + TLSConfig: opt.TLSConfig, // If ClusterSlots is populated, then we probably have an artificial // cluster whose nodes are not in clustering mode (otherwise there isn't // much use for ClusterSlots config). This means we cannot execute the // READONLY command against that node -- setting readOnly to false in such // situations in the options below will prevent that from happening. - readOnly: opt.ReadOnly && opt.ClusterSlots == nil, - UnstableResp3: opt.UnstableResp3, + readOnly: opt.ReadOnly && opt.ClusterSlots == nil, + UnstableResp3: opt.UnstableResp3, + MaintNotificationsConfig: maintNotificationsConfig, + PushNotificationProcessor: opt.PushNotificationProcessor, } } @@ -340,15 +480,16 @@ type clusterNode struct { latency uint32 // atomic generation uint32 // atomic failing uint32 // atomic + loaded uint32 // atomic - // last time the latency measurement was performed for the node, stored in nanoseconds - // from epoch + // last time the latency measurement was performed for the node, stored in nanoseconds from epoch lastLatencyMeasurement int64 // atomic } -func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode { +func newClusterNodeWithNodeAddress(clOpt *ClusterOptions, addr, nodeAddress string) *clusterNode { opt := clOpt.clientOptions() opt.Addr = addr + opt.NodeAddress = nodeAddress node := clusterNode{ Client: clOpt.NewClient(opt), } @@ -391,7 +532,7 @@ func (n *clusterNode) updateLatency() { if successes == 0 { // If none of the pings worked, set latency to some arbitrarily high value so this node gets // least priority. - latency = float64((maximumNodeLatency) / time.Microsecond) + latency = float64(maximumNodeLatency / time.Microsecond) } else { latency = float64(dur) / float64(successes) } @@ -406,10 +547,11 @@ func (n *clusterNode) Latency() time.Duration { func (n *clusterNode) MarkAsFailing() { atomic.StoreUint32(&n.failing, uint32(time.Now().Unix())) + atomic.StoreUint32(&n.loaded, 0) } func (n *clusterNode) Failing() bool { - const timeout = 15 // 15 seconds + timeout := int64(n.Client.opt.FailingTimeoutSeconds) failing := atomic.LoadUint32(&n.failing) if failing == 0 { @@ -449,11 +591,21 @@ func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) { } func (n *clusterNode) Loading() bool { + loaded := atomic.LoadUint32(&n.loaded) + if loaded == 1 { + return false + } + + // check if the node is loading ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() err := n.Client.Ping(ctx).Err() - return err != nil && isLoadingError(err) + loading := err != nil && isLoadingError(err) + if !loading { + atomic.StoreUint32(&n.loaded, 1) + } + return loading } //------------------------------------------------------------------------------ @@ -468,13 +620,12 @@ type clusterNodes struct { closed bool onNewNode []func(rdb *Client) - _generation uint32 // atomic + generation uint32 // atomic } func newClusterNodes(opt *ClusterOptions) *clusterNodes { return &clusterNodes{ - opt: opt, - + opt: opt, addrs: opt.Addrs, nodes: make(map[string]*clusterNode), } @@ -534,12 +685,11 @@ func (c *clusterNodes) Addrs() ([]string, error) { } func (c *clusterNodes) NextGeneration() uint32 { - return atomic.AddUint32(&c._generation, 1) + return atomic.AddUint32(&c.generation, 1) } // GC removes unused nodes. func (c *clusterNodes) GC(generation uint32) { - //nolint:prealloc var collected []*clusterNode c.mu.Lock() @@ -567,6 +717,10 @@ func (c *clusterNodes) GC(generation uint32) { } func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) { + return c.GetOrCreateWithNodeAddress(addr, "") +} + +func (c *clusterNodes) GetOrCreateWithNodeAddress(addr, nodeAddress string) (*clusterNode, error) { node, err := c.get(addr) if err != nil { return nil, err @@ -587,28 +741,25 @@ func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) { return node, nil } - node = newClusterNode(c.opt, addr) + node = newClusterNodeWithNodeAddress(c.opt, addr, nodeAddress) for _, fn := range c.onNewNode { fn(node.Client) } - c.addrs = appendIfNotExists(c.addrs, addr) + c.addrs = appendIfNotExist(c.addrs, addr) c.nodes[addr] = node return node, nil } func (c *clusterNodes) get(addr string) (*clusterNode, error) { - var node *clusterNode - var err error c.mu.RLock() + defer c.mu.RUnlock() + if c.closed { - err = pool.ErrClosed - } else { - node = c.nodes[addr] + return nil, pool.ErrClosed } - c.mu.RUnlock() - return node, err + return c.nodes[addr], nil } func (c *clusterNodes) All() ([]*clusterNode, error) { @@ -639,22 +790,9 @@ func (c *clusterNodes) Random() (*clusterNode, error) { //------------------------------------------------------------------------------ type clusterSlot struct { - start, end int - nodes []*clusterNode -} - -type clusterSlotSlice []*clusterSlot - -func (p clusterSlotSlice) Len() int { - return len(p) -} - -func (p clusterSlotSlice) Less(i, j int) bool { - return p[i].start < p[j].start -} - -func (p clusterSlotSlice) Swap(i, j int) { - p[i], p[j] = p[j], p[i] + start int + end int + nodes []*clusterNode } type clusterState struct { @@ -680,18 +818,25 @@ func newClusterState( createdAt: time.Now(), } - originHost, _, _ := net.SplitHostPort(origin) + originHost, originPort, _ := net.SplitHostPort(origin) isLoopbackOrigin := isLoopback(originHost) for _, slot := range slots { var nodes []*clusterNode for i, slotNode := range slot.Nodes { - addr := slotNode.Addr + // slotNode.Addr is the node address from CLUSTER SLOTS + nodeAddress := slotNode.Addr + addr := nodeAddress if !isLoopbackOrigin { addr = replaceLoopbackHost(addr, originHost) } + // TLS-only clusters (`--port 0 --tls-port 6379`) report port 0 + // in CLUSTER SLOTS. Fall back to the origin port — by definition + // reachable, since it is the port that returned this slot map. + // See https://github.com/redis/go-redis/issues/3726. + addr = replaceZeroPort(addr, originPort) - node, err := c.nodes.GetOrCreate(addr) + node, err := c.nodes.GetOrCreateWithNodeAddress(addr, nodeAddress) if err != nil { return nil, err } @@ -700,9 +845,9 @@ func newClusterState( nodes = append(nodes, node) if i == 0 { - c.Masters = appendUniqueNode(c.Masters, node) + c.Masters = appendIfNotExist(c.Masters, node) } else { - c.Slaves = appendUniqueNode(c.Slaves, node) + c.Slaves = appendIfNotExist(c.Slaves, node) } } @@ -713,7 +858,9 @@ func newClusterState( }) } - sort.Sort(clusterSlotSlice(c.slots)) + slices.SortFunc(c.slots, func(a, b *clusterSlot) int { + return cmp.Compare(a.start, b.start) + }) time.AfterFunc(time.Minute, func() { nodes.GC(c.generation) @@ -741,12 +888,40 @@ func replaceLoopbackHost(nodeAddr, originHost string) string { return net.JoinHostPort(originHost, nodePort) } +// replaceZeroPort substitutes originPort for a node port of "0", which is +// what CLUSTER SLOTS reports for TLS-only clusters started with +// `--port 0 --tls-port `. Non-zero ports and addresses without a +// recoverable origin port are returned unchanged. +func replaceZeroPort(nodeAddr, originPort string) string { + if originPort == "" || originPort == "0" { + return nodeAddr + } + nodeHost, nodePort, err := net.SplitHostPort(nodeAddr) + if err != nil || nodePort != "0" { + return nodeAddr + } + return net.JoinHostPort(nodeHost, originPort) +} + +// isLoopback returns true if the host is a loopback address. +// For IP addresses, it uses net.IP.IsLoopback(). +// For hostnames, it recognizes well-known loopback hostnames like "localhost" +// and Docker-specific loopback patterns like "*.docker.internal". func isLoopback(host string) bool { ip := net.ParseIP(host) - if ip == nil { + if ip != nil { + return ip.IsLoopback() + } + + if strings.ToLower(host) == "localhost" { return true } - return ip.IsLoopback() + + if strings.HasSuffix(strings.ToLower(host), ".docker.internal") { + return true + } + + return false } func (c *clusterState) slotMasterNode(slot int) (*clusterNode, error) { @@ -791,7 +966,7 @@ func (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) { return c.nodes.Random() } - var allNodesFailing = true + allNodesFailing := true var ( closestNonFailingNode *clusterNode closestNode *clusterNode @@ -845,6 +1020,29 @@ func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) { return nodes[randomNodes[0]], nil } +func (c *clusterState) slotShardPickerSlaveNode(slot int, shardPicker routing.ShardPicker) (*clusterNode, error) { + nodes := c.slotNodes(slot) + if len(nodes) == 0 { + return c.nodes.Random() + } + + // nodes[0] is master, nodes[1:] are slaves + // First, try all slave nodes for this slot using ShardPicker order + slaves := nodes[1:] + if len(slaves) > 0 { + for i := 0; i < len(slaves); i++ { + idx := shardPicker.Next(len(slaves)) + slave := slaves[idx] + if !slave.Failing() && !slave.Loading() { + return slave, nil + } + } + } + + // All slaves are failing or loading - return master + return nodes[0], nil +} + func (c *clusterState) slotNodes(slot int) []*clusterNode { i := sort.Search(len(c.slots), func(i int) bool { return c.slots[i].end >= slot @@ -864,13 +1062,16 @@ func (c *clusterState) slotNodes(slot int) []*clusterNode { type clusterStateHolder struct { load func(ctx context.Context) (*clusterState, error) - state atomic.Value - reloading uint32 // atomic + reloadInterval time.Duration + state atomic.Value + reloading uint32 // atomic + reloadPending uint32 // atomic - set to 1 when reload is requested during active reload } -func newClusterStateHolder(fn func(ctx context.Context) (*clusterState, error)) *clusterStateHolder { +func newClusterStateHolder(load func(ctx context.Context) (*clusterState, error), reloadInterval time.Duration) *clusterStateHolder { return &clusterStateHolder{ - load: fn, + load: load, + reloadInterval: reloadInterval, } } @@ -884,17 +1085,37 @@ func (c *clusterStateHolder) Reload(ctx context.Context) (*clusterState, error) } func (c *clusterStateHolder) LazyReload() { + // If already reloading, mark that another reload is pending if !atomic.CompareAndSwapUint32(&c.reloading, 0, 1) { + atomic.StoreUint32(&c.reloadPending, 1) return } + go func() { - defer atomic.StoreUint32(&c.reloading, 0) + for { + _, err := c.Reload(context.Background()) + if err != nil { + atomic.StoreUint32(&c.reloadPending, 0) + atomic.StoreUint32(&c.reloading, 0) + return + } - _, err := c.Reload(context.Background()) - if err != nil { - return + // Clear pending flag after reload completes, before cooldown + // This captures notifications that arrived during the reload + atomic.StoreUint32(&c.reloadPending, 0) + + // Wait cooldown period + time.Sleep(200 * time.Millisecond) + + // Check if another reload was requested during cooldown + if atomic.LoadUint32(&c.reloadPending) == 0 { + // No pending reload, we're done + atomic.StoreUint32(&c.reloading, 0) + return + } + + // Pending reload requested, loop to reload again } - time.Sleep(200 * time.Millisecond) }() } @@ -905,7 +1126,7 @@ func (c *clusterStateHolder) Get(ctx context.Context) (*clusterState, error) { } state := v.(*clusterState) - if time.Since(state.createdAt) > 10*time.Second { + if time.Since(state.createdAt) > c.reloadInterval { c.LazyReload() } return state, nil @@ -925,16 +1146,18 @@ func (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, er // or more underlying connections. It's safe for concurrent use by // multiple goroutines. type ClusterClient struct { - opt *ClusterOptions - nodes *clusterNodes - state *clusterStateHolder - cmdsInfoCache *cmdsInfoCache + opt *ClusterOptions + nodes *clusterNodes + state *clusterStateHolder + cmdsInfoCache *cmdsInfoCache + cmdInfoResolver *commandInfoResolver cmdable hooksMixin } // NewClusterClient returns a Redis Cluster client as described in -// http://redis.io/topics/cluster-spec. +// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec. +// Passing nil ClusterOptions will cause a panic. func NewClusterClient(opt *ClusterOptions) *ClusterClient { if opt == nil { panic("redis: NewClusterClient nil options") @@ -946,10 +1169,13 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient { nodes: newClusterNodes(opt), } - c.state = newClusterStateHolder(c.loadState) c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo) - c.cmdable = c.Process + c.state = newClusterStateHolder(c.loadState, opt.ClusterStateReloadInterval) + + c.SetCommandInfoResolver(NewDefaultCommandPolicyResolver()) + + c.cmdable = c.Process c.initHooks(hooks{ dial: nil, process: c.process, @@ -957,10 +1183,31 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient { txPipeline: c.processTxPipeline, }) + // Set up SMIGRATED notification handling for cluster state reload + // When a node client receives a SMIGRATED notification, it should trigger + // cluster state reload on the parent ClusterClient + if opt.MaintNotificationsConfig != nil { + c.nodes.OnNewNode(func(nodeClient *Client) { + manager := nodeClient.GetMaintNotificationsManager() + if manager != nil { + manager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) { + // Log the migration details for now + if internal.LogLevel.InfoOrAbove() { + internal.Logger.Printf(ctx, "cluster: slots %v migrated to %s, reloading cluster state", slotRanges, hostPort) + } + // Currently we reload the entire cluster state + // In the future, this could be optimized to reload only the specific slots + c.state.LazyReload() + }) + } + }) + } + return c } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *ClusterOptions that were used to create the client. +// Any alteration of the returned *ClusterOptions may result in undefined behaviour. func (c *ClusterClient) Options() *ClusterOptions { return c.opt } @@ -979,13 +1226,6 @@ func (c *ClusterClient) Close() error { return c.nodes.Close() } -// Do create a Cmd from the args and processes the cmd. -func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) @@ -993,7 +1233,7 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { } func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { - slot := c.cmdSlot(cmd) + slot := c.cmdSlot(cmd, -1) var node *clusterNode var moved bool var ask bool @@ -1009,7 +1249,11 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { if node == nil { var err error - node, err = c.cmdNode(ctx, cmd.Name(), slot) + if !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil { + node, err = c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker) + } else { + node, err = c.cmdNode(ctx, cmd.Name(), slot) + } if err != nil { return err } @@ -1017,13 +1261,16 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { if ask { ask = false - pipe := node.Client.Pipeline() _ = pipe.Process(ctx, NewCmd(ctx, "asking")) _ = pipe.Process(ctx, cmd) _, lastErr = pipe.Exec(ctx) } else { - lastErr = node.Client.Process(ctx, cmd) + if !c.opt.DisableRoutingPolicies { + lastErr = c.routeAndRun(ctx, cmd, node) + } else { + lastErr = node.Client.Process(ctx, cmd) + } } // If there is no error - we are done. @@ -1050,6 +1297,18 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { if moved || ask { c.state.LazyReload() + // Record error metrics + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorType := "MOVED" + statusCode := "MOVED" + if ask { + errorType = "ASK" + statusCode = "ASK" + } + // MOVED/ASK are not internal errors, and this is the first attempt (retry count = 0) + errorCallback(ctx, errorType, nil, statusCode, false, 0) + } + var err error node, err = c.nodes.GetOrCreate(addr) if err != nil { @@ -1058,7 +1317,7 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { continue } - if shouldRetry(lastErr, cmd.readTimeout() == nil) { + if shouldRetry(lastErr, cmd.readTimeout() == nil) && !cmd.NoRetry() { // First retry the same node. if attempt == 0 { continue @@ -1213,6 +1472,8 @@ func (c *ClusterClient) PoolStats() *PoolStats { acc.Hits += s.Hits acc.Misses += s.Misses acc.Timeouts += s.Timeouts + acc.WaitCount += s.WaitCount + acc.WaitDurationNs += s.WaitDurationNs acc.TotalConns += s.TotalConns acc.IdleConns += s.IdleConns @@ -1224,6 +1485,8 @@ func (c *ClusterClient) PoolStats() *PoolStats { acc.Hits += s.Hits acc.Misses += s.Misses acc.Timeouts += s.Timeouts + acc.WaitCount += s.WaitCount + acc.WaitDurationNs += s.WaitDurationNs acc.TotalConns += s.TotalConns acc.IdleConns += s.IdleConns @@ -1268,7 +1531,7 @@ func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) { continue } - return newClusterState(c.nodes, slots, node.Client.opt.Addr) + return newClusterState(c.nodes, slots, addr) } /* @@ -1297,17 +1560,35 @@ func (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error) } func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error { + // Only call time.Now() if pipeline operation duration callback is set to avoid overhead + var operationStart time.Time + pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback() + if pipelineOpDurationCallback != nil { + operationStart = time.Now() + } + totalAttempts := 0 + cmdsMap := newCmdsMap() if err := c.mapCmdsByNode(ctx, cmdsMap, cmds); err != nil { setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), 1, err, nil, 0) + } return err } + var lastErr error for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + totalAttempts++ if attempt > 0 { if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), totalAttempts, err, nil, 0) + } return err } } @@ -1328,6 +1609,17 @@ func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error break } cmdsMap = failedCmds + lastErr = cmdsFirstErr(cmds) + } + + // Record pipeline operation duration + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + finalErr := cmdsFirstErr(cmds) + if finalErr == nil { + finalErr = lastErr + } + pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), totalAttempts, finalErr, nil, 0) } return cmdsFirstErr(cmds) @@ -1341,10 +1633,31 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) { for _, cmd := range cmds { - slot := c.cmdSlot(cmd) - node, err := c.slotReadOnlyNode(state, slot) - if err != nil { - return err + var policy *routing.CommandPolicy + if c.cmdInfoResolver != nil { + policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd) + } + if policy != nil && !policy.CanBeUsedInPipeline() { + return fmt.Errorf( + "redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(), + ) + } + slot := c.cmdSlot(cmd, -1) + var node *clusterNode + // For keyless commands (slot == -1), use ShardPicker if routing policies are enabled + if slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil { + if len(state.Masters) == 0 { + return errClusterNoNodes + } + // For read-only keyless commands, pick from all nodes (masters + slaves) + allNodes := append(state.Masters, state.Slaves...) + idx := c.opt.ShardPicker.Next(len(allNodes)) + node = allNodes[idx] + } else { + node, err = c.slotReadOnlyNode(state, slot) + if err != nil { + return err + } } cmdsMap.Add(node, cmd) } @@ -1352,10 +1665,29 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd } for _, cmd := range cmds { - slot := c.cmdSlot(cmd) - node, err := state.slotMasterNode(slot) - if err != nil { - return err + var policy *routing.CommandPolicy + if c.cmdInfoResolver != nil { + policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd) + } + if policy != nil && !policy.CanBeUsedInPipeline() { + return fmt.Errorf( + "redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(), + ) + } + slot := c.cmdSlot(cmd, -1) + var node *clusterNode + // For keyless commands (slot == -1), use ShardPicker if routing policies are enabled + if slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil { + if len(state.Masters) == 0 { + return errClusterNoNodes + } + idx := c.opt.ShardPicker.Next(len(state.Masters)) + node = state.Masters[idx] + } else { + node, err = state.slotMasterNode(slot) + if err != nil { + return err + } } cmdsMap.Add(node, cmd) } @@ -1405,7 +1737,7 @@ func (c *ClusterClient) processPipelineNodeConn( if isBadConn(err, false, node.Client.getAddr()) { node.MarkAsFailing() } - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds, err) @@ -1441,7 +1773,7 @@ func (c *ClusterClient) pipelineReadCmds( } if !isRedisError(err) { - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds[i+1:], err) @@ -1449,7 +1781,7 @@ func (c *ClusterClient) pipelineReadCmds( } } - if err := cmds[0].Err(); err != nil && shouldRetry(err, true) { + if err := cmds[0].Err(); err != nil && shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) return err } @@ -1501,61 +1833,137 @@ func (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) erro } func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error { + // Only call time.Now() if pipeline operation duration callback is set to avoid overhead + var operationStart time.Time + pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback() + if pipelineOpDurationCallback != nil { + operationStart = time.Now() + } + totalAttempts := 0 + // Trim multi .. exec. cmds = cmds[1 : len(cmds)-1] + if len(cmds) == 0 { + return nil + } + state, err := c.state.Get(ctx) if err != nil { setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, err, nil, 0) + } return err } - cmdsMap := c.mapCmdsBySlot(cmds) - for slot, cmds := range cmdsMap { - node, err := state.slotMasterNode(slot) - if err != nil { - setCmdsErr(cmds, err) - continue + keyedCmdsBySlot := c.slottedKeyedCommands(ctx, cmds) + slot := -1 + switch len(keyedCmdsBySlot) { + case 0: + slot = hashtag.RandomSlot() + case 1: + for sl := range keyedCmdsBySlot { + slot = sl + break + } + default: + // TxPipeline does not support cross slot transaction. + setCmdsErr(cmds, ErrCrossSlot) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, ErrCrossSlot, nil, 0) } + return ErrCrossSlot + } - cmdsMap := map[*clusterNode][]Cmder{node: cmds} - for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { - if attempt > 0 { - if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { - setCmdsErr(cmds, err) - return err + node, err := state.slotMasterNode(slot) + if err != nil { + setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, err, nil, 0) + } + return err + } + + var lastErr error + cmdsMap := map[*clusterNode][]Cmder{node: cmds} + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + totalAttempts++ + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), totalAttempts, err, nil, 0) } + return err } + } + + failedCmds := newCmdsMap() + var wg sync.WaitGroup - failedCmds := newCmdsMap() - var wg sync.WaitGroup + for node, cmds := range cmdsMap { + wg.Add(1) + go func(node *clusterNode, cmds []Cmder) { + defer wg.Done() + c.processTxPipelineNode(ctx, node, cmds, failedCmds) + }(node, cmds) + } - for node, cmds := range cmdsMap { - wg.Add(1) - go func(node *clusterNode, cmds []Cmder) { - defer wg.Done() - c.processTxPipelineNode(ctx, node, cmds, failedCmds) - }(node, cmds) - } + wg.Wait() + if len(failedCmds.m) == 0 { + break + } + cmdsMap = failedCmds.m + lastErr = cmdsFirstErr(cmds) + } - wg.Wait() - if len(failedCmds.m) == 0 { - break - } - cmdsMap = failedCmds.m + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + finalErr := cmdsFirstErr(cmds) + if finalErr == nil { + finalErr = lastErr } + pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), totalAttempts, finalErr, nil, 0) } return cmdsFirstErr(cmds) } -func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder { - cmdsMap := make(map[int][]Cmder) +// slottedKeyedCommands returns a map of slot to commands taking into account +// only commands that have keys. +func (c *ClusterClient) slottedKeyedCommands(ctx context.Context, cmds []Cmder) map[int][]Cmder { + cmdsSlots := map[int][]Cmder{} + + // Peek once outside the loop, one RLock for the whole batch instead of + // two per command (one for the keyless check, one inside cmdSlot). + cachedInfo := c.cmdsInfoCache.Peek() + + prefferedRandomSlot := -1 for _, cmd := range cmds { - slot := c.cmdSlot(cmd) - cmdsMap[slot] = append(cmdsMap[slot], cmd) + var info *CommandInfo + if cachedInfo != nil { + info = cachedInfo[cmd.Name()] + } + + pos := cmdFirstKeyPosWithInfo(cmd, info) + if pos == 0 { + continue + } + + slot := c.cmdSlotWithPos(cmd, pos, prefferedRandomSlot) + if prefferedRandomSlot == -1 { + prefferedRandomSlot = slot + } + + cmdsSlots[slot] = append(cmdsSlots[slot], cmd) } - return cmdsMap + + return cmdsSlots } func (c *ClusterClient) processTxPipelineNode( @@ -1581,12 +1989,12 @@ func (c *ClusterClient) processTxPipelineNode( } func (c *ClusterClient) processTxPipelineNodeConn( - ctx context.Context, _ *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, + ctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, ) error { if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmds(wr, cmds) }); err != nil { - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds, err) @@ -1599,7 +2007,7 @@ func (c *ClusterClient) processTxPipelineNodeConn( trimmedCmds := cmds[1 : len(cmds)-1] if err := c.txPipelineReadQueued( - ctx, rd, statusCmd, trimmedCmds, failedCmds, + ctx, node, cn, rd, statusCmd, trimmedCmds, failedCmds, ); err != nil { setCmdsErr(cmds, err) @@ -1611,30 +2019,56 @@ func (c *ClusterClient) processTxPipelineNodeConn( return err } - return pipelineReadCmds(rd, trimmedCmds) + return node.Client.pipelineReadCmds(ctx, cn, rd, trimmedCmds) }) } func (c *ClusterClient) txPipelineReadQueued( ctx context.Context, + node *clusterNode, + cn *pool.Conn, rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder, failedCmds *cmdsMap, ) error { // Parse queued replies. + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + // Log the error but don't fail the command execution + // Push notification processing errors shouldn't break normal Redis operations + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } if err := statusCmd.readReply(rd); err != nil { return err } for _, cmd := range cmds { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + // Log the error but don't fail the command execution + // Push notification processing errors shouldn't break normal Redis operations + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } err := statusCmd.readReply(rd) - if err == nil || c.checkMovedErr(ctx, cmd, err, failedCmds) || isRedisError(err) { - continue + if err != nil { + if c.checkMovedErr(ctx, cmd, err, failedCmds) { + // will be processed later + continue + } + cmd.SetErr(err) + if !isRedisError(err) { + return err + } } - return err } + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + // Log the error but don't fail the command execution + // Push notification processing errors shouldn't break normal Redis operations + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } // Parse number of replies. line, err := rd.ReadLine() if err != nil { @@ -1682,14 +2116,13 @@ func (c *ClusterClient) cmdsMoved( func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error { if len(keys) == 0 { - return fmt.Errorf("redis: Watch requires at least one key") + return errNoWatchKeys } slot := hashtag.Slot(keys[0]) for _, key := range keys[1:] { if hashtag.Slot(key) != slot { - err := fmt.Errorf("redis: Watch requires all keys to be in the same slot") - return err + return errWatchCrosslot } } @@ -1705,10 +2138,18 @@ func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...s } } - err = node.Client.Watch(ctx, fn, keys...) + // Track callback errors separately to avoid retrying user failures through cluster retry classification. + var fnErr error + err = node.Client.Watch(ctx, func(tx *Tx) error { + fnErr = fn(tx) + return fnErr + }, keys...) if err == nil { break } + if fnErr != nil { + return fnErr + } moved, ask, addr := isMovedError(err) if moved || ask { @@ -1740,38 +2181,64 @@ func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...s return err } +// maintenance notifications won't work here for now func (c *ClusterClient) pubSub() *PubSub { var node *clusterNode pubsub := &PubSub{ opt: c.opt.clientOptions(), - - newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) { + newConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) { if node != nil { panic("node != nil") } var err error + if len(channels) > 0 { slot := hashtag.Slot(channels[0]) - node, err = c.slotMasterNode(ctx, slot) + + // newConn in PubSub is only used for subscription connections, so it is safe to + // assume that a slave node can always be used when client options specify ReadOnly. + if c.opt.ReadOnly { + state, err := c.state.Get(ctx) + if err != nil { + return nil, err + } + + node, err = c.slotReadOnlyNode(state, slot) + if err != nil { + return nil, err + } + } else { + node, err = c.slotMasterNode(ctx, slot) + if err != nil { + return nil, err + } + } } else { node, err = c.nodes.Random() + if err != nil { + return nil, err + } } + cn, err := node.Client.pubSubPool.NewConn(ctx, node.Client.opt.Network, node.Client.opt.Addr, channels) if err != nil { + node = nil return nil, err } - - cn, err := node.Client.newConn(context.TODO()) + // will return nil if already initialized + err = node.Client.initConn(ctx, cn) if err != nil { + _ = cn.Close() node = nil - return nil, err } - + node.Client.pubSubPool.TrackConn(cn) return cn, nil }, closeConn: func(cn *pool.Conn) error { - err := node.Client.connPool.CloseConn(cn) + // Untrack connection from PubSubPool + node.Client.pubSubPool.UntrackConn(cn) + err := cn.Close() node = nil return err }, @@ -1832,7 +2299,6 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, for _, idx := range perm { addr := addrs[idx] - node, err := c.nodes.GetOrCreate(addr) if err != nil { if firstErr == nil { @@ -1845,6 +2311,7 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, if err == nil { return info, nil } + if firstErr == nil { firstErr = err } @@ -1856,32 +2323,64 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, return nil, firstErr } +// cmdInfo will fetch and cache the command policies after the first execution func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { - cmdsInfo, err := c.cmdsInfoCache.Get(ctx) + // Use a separate context that won't be canceled to ensure command info lookup + // doesn't fail due to original context cancellation + cmdInfoCtx := c.context(ctx) + if c.opt.ContextTimeoutEnabled && ctx != nil { + // If context timeout is enabled, still use a reasonable timeout + var cancel context.CancelFunc + cmdInfoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + } + + cmdsInfo, err := c.cmdsInfoCache.Get(cmdInfoCtx) if err != nil { - internal.Logger.Printf(context.TODO(), "getting command info: %s", err) + internal.Logger.Printf(cmdInfoCtx, "getting command info: %s", err) return nil } info := cmdsInfo[name] if info == nil { - internal.Logger.Printf(context.TODO(), "info for cmd=%s not found", name) + internal.Logger.Printf(cmdInfoCtx, "info for cmd=%s not found", name) } + return info } -func (c *ClusterClient) cmdSlot(cmd Cmder) int { +// cmdInfoPeek returns the cached CommandInfo for the named command without +// triggering a round-trip to Redis. It returns nil when the cache is cold. +func (c *ClusterClient) cmdInfoPeek(name string) *CommandInfo { + if cmds := c.cmdsInfoCache.Peek(); cmds != nil { + return cmds[name] + } + return nil +} + +func (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int { + info := c.cmdInfoPeek(cmd.Name()) + return c.cmdSlotWithPos(cmd, cmdFirstKeyPosWithInfo(cmd, info), prefferedSlot) +} + +// cmdSlotWithPos computes the cluster slot for cmd given a pre-resolved first key +// position. Separating pos resolution from slot computation lets callers that +// already know pos avoid a redundant Peek() call. +func (c *ClusterClient) cmdSlotWithPos(cmd Cmder, pos int, prefferedSlot int) int { args := cmd.Args() if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { return args[2].(int) } - - return cmdSlot(cmd, cmdFirstKeyPos(cmd)) + return cmdSlot(cmd, pos, prefferedSlot) } -func cmdSlot(cmd Cmder, pos int) int { +func cmdSlot(cmd Cmder, pos int, prefferedRandomSlot int) int { if pos == 0 { - return hashtag.RandomSlot() + if prefferedRandomSlot != -1 { + return prefferedRandomSlot + } + // Return -1 for keyless commands to signal that ShardPicker should be used + return -1 } firstKey := cmd.stringArg(pos) return hashtag.Slot(firstKey) @@ -1906,6 +2405,36 @@ func (c *ClusterClient) cmdNode( return state.slotMasterNode(slot) } +func (c *ClusterClient) cmdNodeWithShardPicker( + ctx context.Context, + cmdName string, + slot int, + shardPicker routing.ShardPicker, +) (*clusterNode, error) { + state, err := c.state.Get(ctx) + if err != nil { + return nil, err + } + + // For keyless commands (slot == -1), use ShardPicker to select a shard + // This respects the user's configured ShardPicker policy + if slot == -1 { + if len(state.Masters) == 0 { + return nil, errClusterNoNodes + } + idx := shardPicker.Next(len(state.Masters)) + return state.Masters[idx], nil + } + + if c.opt.ReadOnly { + cmdInfo := c.cmdInfo(ctx, cmdName) + if cmdInfo != nil && cmdInfo.ReadOnly { + return c.slotReadOnlyNode(state, slot) + } + } + return state.slotMasterNode(slot) +} + func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) { if c.opt.RouteByLatency { return state.slotClosestNode(slot) @@ -1913,6 +2442,11 @@ func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*cluste if c.opt.RouteRandomly { return state.slotRandomNode(slot) } + + if c.opt.ShardPicker != nil { + return state.slotShardPickerSlaveNode(slot, c.opt.ShardPicker) + } + return state.slotSlaveNode(slot) } @@ -1950,7 +2484,7 @@ func (c *ClusterClient) MasterForKey(ctx context.Context, key string) (*Client, if err != nil { return nil, err } - return node.Client, err + return node.Client, nil } func (c *ClusterClient) context(ctx context.Context) context.Context { @@ -1960,26 +2494,38 @@ func (c *ClusterClient) context(ctx context.Context) context.Context { return context.Background() } -func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode { - for _, n := range nodes { - if n == node { - return nodes - } +func (c *ClusterClient) GetResolver() *commandInfoResolver { + return c.cmdInfoResolver +} + +func (c *ClusterClient) SetCommandInfoResolver(cmdInfoResolver *commandInfoResolver) { + c.cmdInfoResolver = cmdInfoResolver +} + +// extractCommandInfo retrieves the routing policy for a command +func (c *ClusterClient) extractCommandInfo(ctx context.Context, cmd Cmder) *routing.CommandPolicy { + if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.CommandPolicy != nil { + return cmdInfo.CommandPolicy + } + + return nil +} + +// NewDynamicResolver returns a CommandInfoResolver +// that uses the underlying cmdInfo cache to resolve the policies +func (c *ClusterClient) NewDynamicResolver() *commandInfoResolver { + return &commandInfoResolver{ + resolveFunc: c.extractCommandInfo, } - return append(nodes, node) } -func appendIfNotExists(ss []string, es ...string) []string { -loop: - for _, e := range es { - for _, s := range ss { - if s == e { - continue loop - } +func appendIfNotExist[T comparable](vals []T, newVal T) []T { + for _, v := range vals { + if v == newVal { + return vals } - ss = append(ss, e) } - return ss + return append(vals, newVal) } //------------------------------------------------------------------------------ diff --git a/vendor/github.com/redis/go-redis/v9/osscluster_router.go b/vendor/github.com/redis/go-redis/v9/osscluster_router.go new file mode 100644 index 00000000000..0da29530ad5 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/osscluster_router.go @@ -0,0 +1,1002 @@ +package redis + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + "time" + + "github.com/redis/go-redis/v9/internal/hashtag" + "github.com/redis/go-redis/v9/internal/routing" +) + +var ( + errInvalidCmdPointer = errors.New("redis: invalid command pointer") + errNoCmdsToAggregate = errors.New("redis: no commands to aggregate") + errNoResToAggregate = errors.New("redis: no results to aggregate") + errInvalidCursorCmdArgsCount = errors.New("redis: FT.CURSOR command requires at least 3 arguments") + errInvalidCursorIdType = errors.New("redis: invalid cursor ID type") +) + +// slotResult represents the result of executing a command on a specific slot +type slotResult struct { + cmd Cmder + keys []string + err error +} + +// routeAndRun routes a command to the appropriate cluster nodes and executes it +func (c *ClusterClient) routeAndRun(ctx context.Context, cmd Cmder, node *clusterNode) error { + var policy *routing.CommandPolicy + if c.cmdInfoResolver != nil { + policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd) + } + + // Set stepCount from cmdInfo if not already set + if cmd.stepCount() == 0 { + if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.StepCount > 0 { + cmd.SetStepCount(cmdInfo.StepCount) + } + } + + if policy == nil { + return c.executeDefault(ctx, cmd, policy, node) + } + switch policy.Request { + case routing.ReqAllNodes: + return c.executeOnAllNodes(ctx, cmd, policy) + case routing.ReqAllShards: + return c.executeOnAllShards(ctx, cmd, policy) + case routing.ReqMultiShard: + return c.executeMultiShard(ctx, cmd, policy) + case routing.ReqSpecial: + return c.executeSpecialCommand(ctx, cmd, policy, node) + default: + return c.executeDefault(ctx, cmd, policy, node) + } +} + +// executeDefault handles standard command routing based on keys +func (c *ClusterClient) executeDefault(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error { + if policy != nil && !c.hasKeys(cmd) { + if c.readOnlyEnabled() && policy.IsReadOnly() { + return c.executeOnArbitraryNode(ctx, cmd) + } + } + + return node.Client.Process(ctx, cmd) +} + +// executeOnArbitraryNode routes command to an arbitrary node +func (c *ClusterClient) executeOnArbitraryNode(ctx context.Context, cmd Cmder) error { + node := c.pickArbitraryNode(ctx) + if node == nil { + return errClusterNoNodes + } + return node.Client.Process(ctx, cmd) +} + +// executeOnAllNodes executes command on all nodes (masters and replicas) +func (c *ClusterClient) executeOnAllNodes(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error { + state, err := c.state.Get(ctx) + if err != nil { + return err + } + + nodes := append(state.Masters, state.Slaves...) + if len(nodes) == 0 { + return errClusterNoNodes + } + + return c.executeParallel(ctx, cmd, nodes, policy) +} + +// executeOnAllShards executes command on all master shards +func (c *ClusterClient) executeOnAllShards(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error { + state, err := c.state.Get(ctx) + if err != nil { + return err + } + + if len(state.Masters) == 0 { + return errClusterNoNodes + } + + return c.executeParallel(ctx, cmd, state.Masters, policy) +} + +// executeMultiShard handles commands that operate on multiple keys across shards +func (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error { + args := cmd.Args() + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) + stepCount := int(cmd.stepCount()) + if stepCount == 0 { + stepCount = 1 // Default to 1 if not set + } + + if firstKeyPos == 0 || firstKeyPos >= len(args) { + return fmt.Errorf("redis: multi-shard command %s has no key arguments", cmd.Name()) + } + + // Group keys by slot + slotMap := make(map[int][]string) + keyOrder := make([]string, 0) + + for i := firstKeyPos; i < len(args); i += stepCount { + key, ok := args[i].(string) + if !ok { + return fmt.Errorf("redis: non-string key at position %d: %v", i, args[i]) + } + + slot := hashtag.Slot(key) + slotMap[slot] = append(slotMap[slot], key) + for j := 1; j < stepCount; j++ { + if i+j >= len(args) { + break + } + slotMap[slot] = append(slotMap[slot], args[i+j].(string)) + } + keyOrder = append(keyOrder, key) + } + + return c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy, firstKeyPos) +} + +// executeMultiSlot executes commands across multiple slots concurrently +func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy, firstKeyPos int) error { + results := make(chan slotResult, len(slotMap)) + var wg sync.WaitGroup + + // Execute on each slot concurrently + for slot, keys := range slotMap { + wg.Add(1) + go func(slot int, keys []string) { + defer wg.Done() + + node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker) + if err != nil { + results <- slotResult{nil, keys, err} + return + } + + // Create a command for this specific slot's keys + subCmd := c.createSlotSpecificCommand(ctx, cmd, keys, firstKeyPos) + err = node.Client.Process(ctx, subCmd) + results <- slotResult{subCmd, keys, err} + }(slot, keys) + } + + go func() { + wg.Wait() + close(results) + }() + + return c.aggregateMultiSlotResults(ctx, cmd, results, keyOrder, policy) +} + +// createSlotSpecificCommand creates a new command for a specific slot's keys. +// firstKeyPos is passed in from the caller (computed once in executeMultiShard) +// so this function never independently re-peeks the cache — avoids the +// cold --> warm inconsistency the reviewer flagged. +func (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string, firstKeyPos int) Cmder { + originalArgs := originalCmd.Args() + + // Build new args with only the specified keys + newArgs := make([]interface{}, 0, firstKeyPos+len(keys)) + + // Copy command name and arguments before the keys + newArgs = append(newArgs, originalArgs[:firstKeyPos]...) + + // Add the slot-specific keys + for _, key := range keys { + newArgs = append(newArgs, key) + } + + // Create a new command of the same type using the helper function + return createCommandByType(ctx, originalCmd.GetCmdType(), newArgs...) +} + +// createCommandByType creates a new command of the specified type with the given arguments +func createCommandByType(ctx context.Context, cmdType CmdType, args ...interface{}) Cmder { + switch cmdType { + case CmdTypeString: + return NewStringCmd(ctx, args...) + case CmdTypeInt: + return NewIntCmd(ctx, args...) + case CmdTypeBool: + return NewBoolCmd(ctx, args...) + case CmdTypeFloat: + return NewFloatCmd(ctx, args...) + case CmdTypeStringSlice: + return NewStringSliceCmd(ctx, args...) + case CmdTypeIntSlice: + return NewIntSliceCmd(ctx, args...) + case CmdTypeFloatSlice: + return NewFloatSliceCmd(ctx, args...) + case CmdTypeBoolSlice: + return NewBoolSliceCmd(ctx, args...) + case CmdTypeStatus: + return NewStatusCmd(ctx, args...) + case CmdTypeTime: + return NewTimeCmd(ctx, args...) + case CmdTypeMapStringString: + return NewMapStringStringCmd(ctx, args...) + case CmdTypeMapStringInt: + return NewMapStringIntCmd(ctx, args...) + case CmdTypeMapStringInterface: + return NewMapStringInterfaceCmd(ctx, args...) + case CmdTypeMapStringInterfaceSlice: + return NewMapStringInterfaceSliceCmd(ctx, args...) + case CmdTypeSlice: + return NewSliceCmd(ctx, args...) + case CmdTypeStringStructMap: + return NewStringStructMapCmd(ctx, args...) + case CmdTypeXMessageSlice: + return NewXMessageSliceCmd(ctx, args...) + case CmdTypeXStreamSlice: + return NewXStreamSliceCmd(ctx, args...) + case CmdTypeXPending: + return NewXPendingCmd(ctx, args...) + case CmdTypeXPendingExt: + return NewXPendingExtCmd(ctx, args...) + case CmdTypeXAutoClaim: + return NewXAutoClaimCmd(ctx, args...) + case CmdTypeXAutoClaimWithDeleted: + return NewXAutoClaimWithDeletedCmd(ctx, args...) + case CmdTypeXAutoClaimJustID: + return NewXAutoClaimJustIDCmd(ctx, args...) + case CmdTypeXInfoStreamFull: + return NewXInfoStreamFullCmd(ctx, args...) + case CmdTypeZSlice: + return NewZSliceCmd(ctx, args...) + case CmdTypeZWithKey: + return NewZWithKeyCmd(ctx, args...) + case CmdTypeClusterSlots: + return NewClusterSlotsCmd(ctx, args...) + case CmdTypeGeoPos: + return NewGeoPosCmd(ctx, args...) + case CmdTypeCommandsInfo: + return NewCommandsInfoCmd(ctx, args...) + case CmdTypeSlowLog: + return NewSlowLogCmd(ctx, args...) + case CmdTypeKeyValues: + return NewKeyValuesCmd(ctx, args...) + case CmdTypeZSliceWithKey: + return NewZSliceWithKeyCmd(ctx, args...) + case CmdTypeFunctionList: + return NewFunctionListCmd(ctx, args...) + case CmdTypeFunctionStats: + return NewFunctionStatsCmd(ctx, args...) + case CmdTypeKeyFlags: + return NewKeyFlagsCmd(ctx, args...) + case CmdTypeDuration: + return NewDurationCmd(ctx, time.Millisecond, args...) + } + return NewCmd(ctx, args...) +} + +// executeSpecialCommand handles commands with special routing requirements +func (c *ClusterClient) executeSpecialCommand(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error { + switch cmd.Name() { + case "ft.cursor": + return c.executeCursorCommand(ctx, cmd) + default: + return c.executeDefault(ctx, cmd, policy, node) + } +} + +// executeCursorCommand handles FT.CURSOR commands with sticky routing +func (c *ClusterClient) executeCursorCommand(ctx context.Context, cmd Cmder) error { + args := cmd.Args() + if len(args) < 4 { + return errInvalidCursorCmdArgsCount + } + + cursorID, ok := args[3].(string) + if !ok { + return errInvalidCursorIdType + } + + // Route based on cursor ID to maintain stickiness + slot := hashtag.Slot(cursorID) + node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker) + if err != nil { + return err + } + + return node.Client.Process(ctx, cmd) +} + +// executeParallel executes a command on multiple nodes concurrently +func (c *ClusterClient) executeParallel(ctx context.Context, cmd Cmder, nodes []*clusterNode, policy *routing.CommandPolicy) error { + if len(nodes) == 0 { + return errClusterNoNodes + } + + if len(nodes) == 1 { + return nodes[0].Client.Process(ctx, cmd) + } + + type nodeResult struct { + cmd Cmder + err error + } + + results := make(chan nodeResult, len(nodes)) + var wg sync.WaitGroup + + for _, node := range nodes { + wg.Add(1) + go func(n *clusterNode) { + defer wg.Done() + cmdCopy := cmd.Clone() + err := n.Client.Process(ctx, cmdCopy) + results <- nodeResult{cmdCopy, err} + }(node) + } + + go func() { + wg.Wait() + close(results) + }() + + // Collect results and check for errors + cmds := make([]Cmder, 0, len(nodes)) + var firstErr error + + for result := range results { + if result.err != nil && firstErr == nil { + firstErr = result.err + } + cmds = append(cmds, result.cmd) + } + + // If there was an error and no policy specified, fail fast + if firstErr != nil && (policy == nil || policy.Response == routing.RespDefaultKeyless) { + cmd.SetErr(firstErr) + return firstErr + } + + return c.aggregateResponses(cmd, cmds, policy) +} + +// aggregateMultiSlotResults aggregates results from multi-slot execution +func (c *ClusterClient) aggregateMultiSlotResults(ctx context.Context, cmd Cmder, results <-chan slotResult, keyOrder []string, policy *routing.CommandPolicy) error { + keyedResults := make(map[string]routing.AggregatorResErr) + var firstErr error + + for result := range results { + if result.err != nil && firstErr == nil { + firstErr = result.err + } + if result.cmd != nil && result.err == nil { + value, err := ExtractCommandValue(result.cmd) + + // Check if the result is a slice (e.g., from MGET) + if sliceValue, ok := value.([]interface{}); ok { + // Map each element to its corresponding key + for i, key := range result.keys { + if i < len(sliceValue) { + keyedResults[key] = routing.AggregatorResErr{Result: sliceValue[i], Err: err} + } else { + keyedResults[key] = routing.AggregatorResErr{Result: nil, Err: err} + } + } + } else { + // For non-slice results, map the entire result to each key + for _, key := range result.keys { + keyedResults[key] = routing.AggregatorResErr{Result: value, Err: err} + } + } + } + + // TODO: return multiple errors by order when we will implement multiple errors returning + if result.err != nil { + firstErr = result.err + } + } + + return c.aggregateKeyedValues(cmd, keyedResults, keyOrder, policy) +} + +// aggregateKeyedValues aggregates individual key-value pairs while preserving key order +func (c *ClusterClient) aggregateKeyedValues(cmd Cmder, keyedResults map[string]routing.AggregatorResErr, keyOrder []string, policy *routing.CommandPolicy) error { + if len(keyedResults) == 0 { + return errNoResToAggregate + } + + aggregator := c.createAggregator(policy, cmd, true) + + // Set key order for keyed aggregators + var keyedAgg *routing.DefaultKeyedAggregator + var isKeyedAgg bool + var err error + if keyedAgg, isKeyedAgg = aggregator.(*routing.DefaultKeyedAggregator); isKeyedAgg { + err = keyedAgg.BatchAddWithKeyOrder(keyedResults, keyOrder) + } else { + err = aggregator.BatchAdd(keyedResults) + } + + if err != nil { + return err + } + + return c.finishAggregation(cmd, aggregator) +} + +// aggregateResponses aggregates multiple shard responses +func (c *ClusterClient) aggregateResponses(cmd Cmder, cmds []Cmder, policy *routing.CommandPolicy) error { + if len(cmds) == 0 { + return errNoCmdsToAggregate + } + + if len(cmds) == 1 { + shardCmd := cmds[0] + if err := shardCmd.Err(); err != nil { + cmd.SetErr(err) + return err + } + value, _ := ExtractCommandValue(shardCmd) + return c.setCommandValue(cmd, value) + } + + aggregator := c.createAggregator(policy, cmd, false) + + batchWithErrs := []routing.AggregatorResErr{} + // Add all results to aggregator + for _, shardCmd := range cmds { + value, err := ExtractCommandValue(shardCmd) + batchWithErrs = append(batchWithErrs, routing.AggregatorResErr{ + Result: value, + Err: err, + }) + } + + err := aggregator.BatchSlice(batchWithErrs) + if err != nil { + return err + } + + return c.finishAggregation(cmd, aggregator) +} + +// createAggregator creates the appropriate response aggregator +func (c *ClusterClient) createAggregator(policy *routing.CommandPolicy, cmd Cmder, isKeyed bool) routing.ResponseAggregator { + if policy != nil { + return routing.NewResponseAggregator(policy.Response, cmd.Name()) + } + + if !isKeyed { + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) + isKeyed = firstKeyPos > 0 + } + + return routing.NewDefaultAggregator(isKeyed) +} + +// finishAggregation completes the aggregation process and sets the result +func (c *ClusterClient) finishAggregation(cmd Cmder, aggregator routing.ResponseAggregator) error { + finalValue, finalErr := aggregator.Result() + if finalErr != nil { + cmd.SetErr(finalErr) + return finalErr + } + + return c.setCommandValue(cmd, finalValue) +} + +// pickArbitraryNode selects a master or slave shard using the configured ShardPicker +func (c *ClusterClient) pickArbitraryNode(ctx context.Context) *clusterNode { + state, err := c.state.Get(ctx) + if err != nil || len(state.Masters) == 0 { + return nil + } + + allNodes := append(state.Masters, state.Slaves...) + + idx := c.opt.ShardPicker.Next(len(allNodes)) + return allNodes[idx] +} + +// hasKeys checks if a command operates on keys +func (c *ClusterClient) hasKeys(cmd Cmder) bool { + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) + return firstKeyPos > 0 +} + +func (c *ClusterClient) readOnlyEnabled() bool { + return c.opt.ReadOnly +} + +// setCommandValue sets the aggregated value on a command using the enum-based approach +func (c *ClusterClient) setCommandValue(cmd Cmder, value interface{}) error { + // If value is nil, it might mean ExtractCommandValue couldn't extract the value + // but the command might have executed successfully. In this case, don't set an error. + if value == nil { + // ExtractCommandValue returned nil - this means the command type is not supported + // in the aggregation flow. This is a programming error, not a runtime error. + if cmd.Err() != nil { + // Command already has an error, preserve it + return cmd.Err() + } + // Command executed successfully but we can't extract/set the aggregated value + // This indicates the command type needs to be added to ExtractCommandValue + return fmt.Errorf("redis: cannot aggregate command %s: unsupported command type %d", + cmd.Name(), cmd.GetCmdType()) + } + + switch cmd.GetCmdType() { + case CmdTypeGeneric: + if c, ok := cmd.(*Cmd); ok { + c.SetVal(value) + } + case CmdTypeString: + if c, ok := cmd.(*StringCmd); ok { + if v, ok := value.(string); ok { + c.SetVal(v) + } + } + case CmdTypeInt: + if c, ok := cmd.(*IntCmd); ok { + if v, ok := value.(int64); ok { + c.SetVal(v) + } else if v, ok := value.(float64); ok { + c.SetVal(int64(v)) + } + } + case CmdTypeBool: + if c, ok := cmd.(*BoolCmd); ok { + if v, ok := value.(bool); ok { + c.SetVal(v) + } + } + case CmdTypeFloat: + if c, ok := cmd.(*FloatCmd); ok { + if v, ok := value.(float64); ok { + c.SetVal(v) + } + } + case CmdTypeStringSlice: + if c, ok := cmd.(*StringSliceCmd); ok { + if v, ok := value.([]string); ok { + c.SetVal(v) + } + } + case CmdTypeIntSlice: + if c, ok := cmd.(*IntSliceCmd); ok { + if v, ok := value.([]int64); ok { + c.SetVal(v) + } else if v, ok := value.([]float64); ok { + els := len(v) + intSlc := make([]int, els) + for i := range v { + intSlc[i] = int(v[i]) + } + } + } + case CmdTypeFloatSlice: + if c, ok := cmd.(*FloatSliceCmd); ok { + if v, ok := value.([]float64); ok { + c.SetVal(v) + } + } + case CmdTypeBoolSlice: + if c, ok := cmd.(*BoolSliceCmd); ok { + if v, ok := value.([]bool); ok { + c.SetVal(v) + } + } + case CmdTypeMapStringString: + if c, ok := cmd.(*MapStringStringCmd); ok { + if v, ok := value.(map[string]string); ok { + c.SetVal(v) + } + } + case CmdTypeMapStringInt: + if c, ok := cmd.(*MapStringIntCmd); ok { + if v, ok := value.(map[string]int64); ok { + c.SetVal(v) + } + } + case CmdTypeMapStringInterface: + if c, ok := cmd.(*MapStringInterfaceCmd); ok { + if v, ok := value.(map[string]interface{}); ok { + c.SetVal(v) + } + } + case CmdTypeSlice: + if c, ok := cmd.(*SliceCmd); ok { + if v, ok := value.([]interface{}); ok { + c.SetVal(v) + } + } + case CmdTypeStatus: + if c, ok := cmd.(*StatusCmd); ok { + if v, ok := value.(string); ok { + c.SetVal(v) + } + } + case CmdTypeDuration: + if c, ok := cmd.(*DurationCmd); ok { + if v, ok := value.(time.Duration); ok { + c.SetVal(v) + } + } + case CmdTypeTime: + if c, ok := cmd.(*TimeCmd); ok { + if v, ok := value.(time.Time); ok { + c.SetVal(v) + } + } + case CmdTypeKeyValueSlice: + if c, ok := cmd.(*KeyValueSliceCmd); ok { + if v, ok := value.([]KeyValue); ok { + c.SetVal(v) + } + } + case CmdTypeStringStructMap: + if c, ok := cmd.(*StringStructMapCmd); ok { + if v, ok := value.(map[string]struct{}); ok { + c.SetVal(v) + } + } + case CmdTypeXMessageSlice: + if c, ok := cmd.(*XMessageSliceCmd); ok { + if v, ok := value.([]XMessage); ok { + c.SetVal(v) + } + } + case CmdTypeXStreamSlice: + if c, ok := cmd.(*XStreamSliceCmd); ok { + if v, ok := value.([]XStream); ok { + c.SetVal(v) + } + } + case CmdTypeXPending: + if c, ok := cmd.(*XPendingCmd); ok { + if v, ok := value.(*XPending); ok { + c.SetVal(v) + } + } + case CmdTypeXPendingExt: + if c, ok := cmd.(*XPendingExtCmd); ok { + if v, ok := value.([]XPendingExt); ok { + c.SetVal(v) + } + } + case CmdTypeXAutoClaim: + if c, ok := cmd.(*XAutoClaimCmd); ok { + if v, ok := value.(CmdTypeXAutoClaimValue); ok { + c.SetVal(v.messages, v.start) + } + } + case CmdTypeXAutoClaimWithDeleted: + if c, ok := cmd.(*XAutoClaimWithDeletedCmd); ok { + if v, ok := value.(CmdTypeXAutoClaimWithDeletedValue); ok { + c.SetVal(v.messages, v.start, v.deletedIDs) + } + } + case CmdTypeXAutoClaimJustID: + if c, ok := cmd.(*XAutoClaimJustIDCmd); ok { + if v, ok := value.(CmdTypeXAutoClaimJustIDValue); ok { + c.SetVal(v.ids, v.start) + } + } + case CmdTypeXInfoConsumers: + if c, ok := cmd.(*XInfoConsumersCmd); ok { + if v, ok := value.([]XInfoConsumer); ok { + c.SetVal(v) + } + } + case CmdTypeXInfoGroups: + if c, ok := cmd.(*XInfoGroupsCmd); ok { + if v, ok := value.([]XInfoGroup); ok { + c.SetVal(v) + } + } + case CmdTypeXInfoStream: + if c, ok := cmd.(*XInfoStreamCmd); ok { + if v, ok := value.(*XInfoStream); ok { + c.SetVal(v) + } + } + case CmdTypeXInfoStreamFull: + if c, ok := cmd.(*XInfoStreamFullCmd); ok { + if v, ok := value.(*XInfoStreamFull); ok { + c.SetVal(v) + } + } + case CmdTypeZSlice: + if c, ok := cmd.(*ZSliceCmd); ok { + if v, ok := value.([]Z); ok { + c.SetVal(v) + } + } + case CmdTypeZWithKey: + if c, ok := cmd.(*ZWithKeyCmd); ok { + if v, ok := value.(*ZWithKey); ok { + c.SetVal(v) + } + } + case CmdTypeScan: + if c, ok := cmd.(*ScanCmd); ok { + if v, ok := value.(CmdTypeScanValue); ok { + c.SetVal(v.keys, v.cursor) + } + } + case CmdTypeClusterSlots: + if c, ok := cmd.(*ClusterSlotsCmd); ok { + if v, ok := value.([]ClusterSlot); ok { + c.SetVal(v) + } + } + case CmdTypeGeoLocation: + if c, ok := cmd.(*GeoLocationCmd); ok { + if v, ok := value.([]GeoLocation); ok { + c.SetVal(v) + } + } + case CmdTypeGeoSearchLocation: + if c, ok := cmd.(*GeoSearchLocationCmd); ok { + if v, ok := value.([]GeoLocation); ok { + c.SetVal(v) + } + } + case CmdTypeGeoPos: + if c, ok := cmd.(*GeoPosCmd); ok { + if v, ok := value.([]*GeoPos); ok { + c.SetVal(v) + } + } + case CmdTypeCommandsInfo: + if c, ok := cmd.(*CommandsInfoCmd); ok { + if v, ok := value.(map[string]*CommandInfo); ok { + c.SetVal(v) + } + } + case CmdTypeSlowLog: + if c, ok := cmd.(*SlowLogCmd); ok { + if v, ok := value.([]SlowLog); ok { + c.SetVal(v) + } + } + case CmdTypeMapStringStringSlice: + if c, ok := cmd.(*MapStringStringSliceCmd); ok { + if v, ok := value.([]map[string]string); ok { + c.SetVal(v) + } + } + case CmdTypeMapMapStringInterface: + if c, ok := cmd.(*MapMapStringInterfaceCmd); ok { + if v, ok := value.(map[string]interface{}); ok { + c.SetVal(v) + } + } + case CmdTypeMapStringInterfaceSlice: + if c, ok := cmd.(*MapStringInterfaceSliceCmd); ok { + if v, ok := value.([]map[string]interface{}); ok { + c.SetVal(v) + } + } + case CmdTypeKeyValues: + if c, ok := cmd.(*KeyValuesCmd); ok { + // KeyValuesCmd needs a key string and values slice + if v, ok := value.(CmdTypeKeyValuesValue); ok { + c.SetVal(v.key, v.values) + } + } + case CmdTypeZSliceWithKey: + if c, ok := cmd.(*ZSliceWithKeyCmd); ok { + // ZSliceWithKeyCmd needs a key string and Z slice + if v, ok := value.(CmdTypeZSliceWithKeyValue); ok { + c.SetVal(v.key, v.zSlice) + } + } + case CmdTypeFunctionList: + if c, ok := cmd.(*FunctionListCmd); ok { + if v, ok := value.([]Library); ok { + c.SetVal(v) + } + } + case CmdTypeFunctionStats: + if c, ok := cmd.(*FunctionStatsCmd); ok { + if v, ok := value.(FunctionStats); ok { + c.SetVal(v) + } + } + case CmdTypeLCS: + if c, ok := cmd.(*LCSCmd); ok { + if v, ok := value.(*LCSMatch); ok { + c.SetVal(v) + } + } + case CmdTypeKeyFlags: + if c, ok := cmd.(*KeyFlagsCmd); ok { + if v, ok := value.([]KeyFlags); ok { + c.SetVal(v) + } + } + case CmdTypeClusterLinks: + if c, ok := cmd.(*ClusterLinksCmd); ok { + if v, ok := value.([]ClusterLink); ok { + c.SetVal(v) + } + } + case CmdTypeClusterShards: + if c, ok := cmd.(*ClusterShardsCmd); ok { + if v, ok := value.([]ClusterShard); ok { + c.SetVal(v) + } + } + case CmdTypeRankWithScore: + if c, ok := cmd.(*RankWithScoreCmd); ok { + if v, ok := value.(RankScore); ok { + c.SetVal(v) + } + } + case CmdTypeClientInfo: + if c, ok := cmd.(*ClientInfoCmd); ok { + if v, ok := value.(*ClientInfo); ok { + c.SetVal(v) + } + } + case CmdTypeACLLog: + if c, ok := cmd.(*ACLLogCmd); ok { + if v, ok := value.([]*ACLLogEntry); ok { + c.SetVal(v) + } + } + case CmdTypeInfo: + if c, ok := cmd.(*InfoCmd); ok { + if v, ok := value.(map[string]map[string]string); ok { + c.SetVal(v) + } + } + case CmdTypeMonitor: + // MonitorCmd doesn't have SetVal method + // Skip setting value for MonitorCmd + case CmdTypeJSON: + if c, ok := cmd.(*JSONCmd); ok { + if v, ok := value.(string); ok { + c.SetVal(v) + } + } + case CmdTypeJSONSlice: + if c, ok := cmd.(*JSONSliceCmd); ok { + if v, ok := value.([]interface{}); ok { + c.SetVal(v) + } + } + case CmdTypeIntPointerSlice: + if c, ok := cmd.(*IntPointerSliceCmd); ok { + if v, ok := value.([]*int64); ok { + c.SetVal(v) + } + } + case CmdTypeScanDump: + if c, ok := cmd.(*ScanDumpCmd); ok { + if v, ok := value.(ScanDump); ok { + c.SetVal(v) + } + } + case CmdTypeBFInfo: + if c, ok := cmd.(*BFInfoCmd); ok { + if v, ok := value.(BFInfo); ok { + c.SetVal(v) + } + } + case CmdTypeCFInfo: + if c, ok := cmd.(*CFInfoCmd); ok { + if v, ok := value.(CFInfo); ok { + c.SetVal(v) + } + } + case CmdTypeCMSInfo: + if c, ok := cmd.(*CMSInfoCmd); ok { + if v, ok := value.(CMSInfo); ok { + c.SetVal(v) + } + } + case CmdTypeTopKInfo: + if c, ok := cmd.(*TopKInfoCmd); ok { + if v, ok := value.(TopKInfo); ok { + c.SetVal(v) + } + } + case CmdTypeTDigestInfo: + if c, ok := cmd.(*TDigestInfoCmd); ok { + if v, ok := value.(TDigestInfo); ok { + c.SetVal(v) + } + } + case CmdTypeFTSynDump: + if c, ok := cmd.(*FTSynDumpCmd); ok { + if v, ok := value.([]FTSynDumpResult); ok { + c.SetVal(v) + } + } + case CmdTypeAggregate: + if c, ok := cmd.(*AggregateCmd); ok { + if v, ok := value.(*FTAggregateResult); ok { + c.SetVal(v) + } + } + case CmdTypeFTInfo: + if c, ok := cmd.(*FTInfoCmd); ok { + if v, ok := value.(FTInfoResult); ok { + c.SetVal(v) + } + } + case CmdTypeFTSpellCheck: + if c, ok := cmd.(*FTSpellCheckCmd); ok { + if v, ok := value.([]SpellCheckResult); ok { + c.SetVal(v) + } + } + case CmdTypeFTSearch: + if c, ok := cmd.(*FTSearchCmd); ok { + if v, ok := value.(FTSearchResult); ok { + c.SetVal(v) + } + } + case CmdTypeTSTimestampValue: + if c, ok := cmd.(*TSTimestampValueCmd); ok { + if v, ok := value.(TSTimestampValue); ok { + c.SetVal(v) + } + } + case CmdTypeTSTimestampValueSlice: + if c, ok := cmd.(*TSTimestampValueSliceCmd); ok { + if v, ok := value.([]TSTimestampValue); ok { + c.SetVal(v) + } + } + default: + // Fallback to reflection for unknown types + return c.setCommandValueReflection(cmd, value) + } + + return nil +} + +// setCommandValueReflection is a fallback function that uses reflection +func (c *ClusterClient) setCommandValueReflection(cmd Cmder, value interface{}) error { + cmdValue := reflect.ValueOf(cmd) + if cmdValue.Kind() != reflect.Ptr || cmdValue.IsNil() { + return errInvalidCmdPointer + } + + setValMethod := cmdValue.MethodByName("SetVal") + if !setValMethod.IsValid() { + return fmt.Errorf("redis: command %T does not have SetVal method", cmd) + } + + args := []reflect.Value{reflect.ValueOf(value)} + + switch cmd.(type) { + case *XAutoClaimCmd, *XAutoClaimJustIDCmd: + args = append(args, reflect.ValueOf("")) + case *ScanCmd: + args = append(args, reflect.ValueOf(uint64(0))) + case *KeyValuesCmd, *ZSliceWithKeyCmd: + if key, ok := value.(string); ok { + args = []reflect.Value{reflect.ValueOf(key)} + if _, ok := cmd.(*ZSliceWithKeyCmd); ok { + args = append(args, reflect.ValueOf([]Z{})) + } else { + args = append(args, reflect.ValueOf([]string{})) + } + } + } + + defer func() { + if r := recover(); r != nil { + cmd.SetErr(fmt.Errorf("redis: failed to set command value: %v", r)) + } + }() + + setValMethod.Call(args) + return nil +} diff --git a/vendor/github.com/redis/go-redis/v9/otel.go b/vendor/github.com/redis/go-redis/v9/otel.go new file mode 100644 index 00000000000..1ea359364f2 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/otel.go @@ -0,0 +1,235 @@ +package redis + +import ( + "context" + "net" + "time" + + "github.com/redis/go-redis/v9/internal/otel" + "github.com/redis/go-redis/v9/internal/pool" +) + +// ConnInfo provides information about a Redis connection for metrics. +type ConnInfo interface { + RemoteAddr() net.Addr + PoolName() string +} + +type Pooler interface { + PoolStats() *pool.Stats +} + +type PubSubPooler interface { + Stats() *pool.PubSubStats +} + +// OTelRecorder is the interface for recording OpenTelemetry metrics. + +type OTelRecorder interface { + // RecordOperationDuration records the total operation duration (including all retries) + RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn ConnInfo, dbIndex int) + + // RecordPipelineOperationDuration records the total pipeline/transaction duration. + // operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions. + RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn ConnInfo, dbIndex int) + + // RecordConnectionCreateTime records the time it took to create a new connection + RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn ConnInfo) + + // RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed + // delta: +1 for relaxed, -1 for unrelaxed + // poolName: name of the connection pool (e.g., "main", "pubsub") + // notificationType: the notification type that triggered the timeout relaxation (e.g., "MOVING", "HANDOFF") + RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn ConnInfo, poolName, notificationType string) + + // RecordConnectionHandoff records when a connection is handed off to another node + // poolName: name of the connection pool (e.g., "main", "pubsub") + RecordConnectionHandoff(ctx context.Context, cn ConnInfo, poolName string) + + // RecordError records client errors (ASK, MOVED, handshake failures, etc.) + // errorType: type of error (e.g., "ASK", "MOVED", "HANDSHAKE_FAILED") + // statusCode: Redis response status code if available (e.g., "MOVED", "ASK") + // isInternal: whether this is an internal error + // retryAttempts: number of retry attempts made + RecordError(ctx context.Context, errorType string, cn ConnInfo, statusCode string, isInternal bool, retryAttempts int) + + // RecordMaintenanceNotification records when a maintenance notification is received + // notificationType: the type of notification (e.g., "MOVING", "MIGRATING", etc.) + RecordMaintenanceNotification(ctx context.Context, cn ConnInfo, notificationType string) + + // RecordConnectionWaitTime records the time spent waiting for a connection from the pool + RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn ConnInfo) + + // RecordConnectionClosed records when a connection is closed + // reason: reason for closing (e.g., "idle", "max_lifetime", "error", "pool_closed") + // err: the error that caused the close (nil for non-error closures) + RecordConnectionClosed(ctx context.Context, cn ConnInfo, reason string, err error) + + // RecordPubSubMessage records a Pub/Sub message + // direction: "sent" or "received" + // channel: channel name (may be hidden for cardinality reduction) + // sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE) + RecordPubSubMessage(ctx context.Context, cn ConnInfo, direction, channel string, sharded bool) + + // RecordStreamLag records the lag for stream consumer group processing + // lag: time difference between message creation and consumption + // streamName: name of the stream (may be hidden for cardinality reduction) + // consumerGroup: name of the consumer group + // consumerName: name of the consumer + RecordStreamLag(ctx context.Context, lag time.Duration, cn ConnInfo, streamName, consumerGroup, consumerName string) +} + +// OTelConnectionCounter is an optional capability interface for recording +// connection count and pending request changes via UpDownCounters. +// Implementations of OTelRecorder can optionally implement this interface +// to receive connection count and pending request delta notifications. +// This is kept separate from OTelRecorder to avoid breaking existing +// third-party implementations when new methods are added. +type OTelConnectionCounter interface { + // RecordConnectionCount records a change in connection count (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + RecordConnectionCount(ctx context.Context, delta int, cn ConnInfo, state string, isPubSub bool) + + // RecordPendingRequests records a change in pending requests (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + RecordPendingRequests(ctx context.Context, delta int, cn ConnInfo, poolName string) +} + +// This is used for async gauge metrics that need to pull stats from pools periodically. +type OTelPoolRegistrar interface { + // RegisterPool is called when a new client is created with its main connection pool. + // poolName: unique identifier for the pool (e.g., "main_abc123") + RegisterPool(poolName string, pool Pooler) + // UnregisterPool is called when a client is closed to remove its pool from the registry. + UnregisterPool(pool Pooler) + // RegisterPubSubPool is called when a new client is created with a PubSub pool. + // poolName: unique identifier for the pool (e.g., "main_abc123_pubsub") + RegisterPubSubPool(poolName string, pool PubSubPooler) + // UnregisterPubSubPool is called when a PubSub client is closed to remove its pool. + UnregisterPubSubPool(pool PubSubPooler) +} + +// SetOTelRecorder sets the global OpenTelemetry recorder. +func SetOTelRecorder(r OTelRecorder) { + if r == nil { + otel.SetGlobalRecorder(nil) + return + } + otel.SetGlobalRecorder(&otelRecorderAdapter{r}) +} + +type otelRecorderAdapter struct { + recorder OTelRecorder +} + +// toConnInfo converts *pool.Conn to ConnInfo interface properly. +// This ensures that a nil *pool.Conn becomes a true nil interface, +// not a non-nil interface containing a nil pointer. +func toConnInfo(cn *pool.Conn) ConnInfo { + if cn == nil { + return nil + } + return cn +} + +func (a *otelRecorderAdapter) RecordOperationDuration(ctx context.Context, duration time.Duration, cmd otel.Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) { + // Convert internal Cmder to public Cmder + if publicCmd, ok := cmd.(Cmder); ok { + a.recorder.RecordOperationDuration(ctx, duration, publicCmd, attempts, err, toConnInfo(cn), dbIndex) + } +} + +func (a *otelRecorderAdapter) RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) { + a.recorder.RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, toConnInfo(cn), dbIndex) +} + +func (a *otelRecorderAdapter) RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) { + a.recorder.RecordConnectionCreateTime(ctx, duration, toConnInfo(cn)) +} + +func (a *otelRecorderAdapter) RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) { + a.recorder.RecordConnectionRelaxedTimeout(ctx, delta, toConnInfo(cn), poolName, notificationType) +} + +func (a *otelRecorderAdapter) RecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string) { + a.recorder.RecordConnectionHandoff(ctx, toConnInfo(cn), poolName) +} + +func (a *otelRecorderAdapter) RecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) { + a.recorder.RecordError(ctx, errorType, toConnInfo(cn), statusCode, isInternal, retryAttempts) +} + +func (a *otelRecorderAdapter) RecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string) { + a.recorder.RecordMaintenanceNotification(ctx, toConnInfo(cn), notificationType) +} + +func (a *otelRecorderAdapter) RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn) { + a.recorder.RecordConnectionWaitTime(ctx, duration, toConnInfo(cn)) +} + +func (a *otelRecorderAdapter) RecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error) { + a.recorder.RecordConnectionClosed(ctx, toConnInfo(cn), reason, err) +} + +func (a *otelRecorderAdapter) RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) { + a.recorder.RecordPubSubMessage(ctx, toConnInfo(cn), direction, channel, sharded) +} + +func (a *otelRecorderAdapter) RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) { + a.recorder.RecordStreamLag(ctx, lag, toConnInfo(cn), streamName, consumerGroup, consumerName) +} + +func (a *otelRecorderAdapter) RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) { + if counter, ok := a.recorder.(OTelConnectionCounter); ok { + counter.RecordConnectionCount(ctx, delta, toConnInfo(cn), state, isPubSub) + } +} + +func (a *otelRecorderAdapter) RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string) { + if counter, ok := a.recorder.(OTelConnectionCounter); ok { + counter.RecordPendingRequests(ctx, delta, toConnInfo(cn), poolName) + } +} + +func (a *otelRecorderAdapter) RegisterPool(poolName string, p pool.Pooler) { + if registrar, ok := a.recorder.(OTelPoolRegistrar); ok { + registrar.RegisterPool(poolName, &poolerAdapter{p}) + } +} + +func (a *otelRecorderAdapter) UnregisterPool(p pool.Pooler) { + if registrar, ok := a.recorder.(OTelPoolRegistrar); ok { + registrar.UnregisterPool(&poolerAdapter{p}) + } +} + +func (a *otelRecorderAdapter) RegisterPubSubPool(poolName string, p otel.PubSubPooler) { + if registrar, ok := a.recorder.(OTelPoolRegistrar); ok { + registrar.RegisterPubSubPool(poolName, &pubSubPoolerAdapter{p}) + } +} + +func (a *otelRecorderAdapter) UnregisterPubSubPool(p otel.PubSubPooler) { + if registrar, ok := a.recorder.(OTelPoolRegistrar); ok { + registrar.UnregisterPubSubPool(&pubSubPoolerAdapter{p}) + } +} + +type poolerAdapter struct { + p pool.Pooler +} + +func (a *poolerAdapter) PoolStats() *pool.Stats { + return a.p.Stats() +} + +type pubSubPoolerAdapter struct { + p otel.PubSubPooler +} + +func (a *pubSubPoolerAdapter) Stats() *pool.PubSubStats { + return a.p.Stats() +} diff --git a/vendor/github.com/redis/go-redis/v9/pipeline.go b/vendor/github.com/redis/go-redis/v9/pipeline.go index 1c114205c05..41b8322137e 100644 --- a/vendor/github.com/redis/go-redis/v9/pipeline.go +++ b/vendor/github.com/redis/go-redis/v9/pipeline.go @@ -7,7 +7,7 @@ import ( type pipelineExecer func(context.Context, []Cmder) error -// Pipeliner is an mechanism to realise Redis Pipeline technique. +// Pipeliner is a mechanism to realise Redis Pipeline technique. // // Pipelining is a technique to extremely speed up processing by packing // operations to batches, send them at once to Redis and read a replies in a @@ -23,27 +23,33 @@ type pipelineExecer func(context.Context, []Cmder) error type Pipeliner interface { StatefulCmdable - // Len is to obtain the number of commands in the pipeline that have not yet been executed. + // Len obtains the number of commands in the pipeline that have not yet been executed. Len() int // Do is an API for executing any command. // If a certain Redis command is not yet supported, you can use Do to execute it. Do(ctx context.Context, args ...interface{}) *Cmd - // Process is to put the commands to be executed into the pipeline buffer. + // Process queues the cmd for later execution. Process(ctx context.Context, cmd Cmder) error - // Discard is to discard all commands in the cache that have not yet been executed. + // BatchProcess adds multiple commands to be executed into the pipeline buffer. + BatchProcess(ctx context.Context, cmd ...Cmder) error + + // Discard discards all commands in the pipeline buffer that have not yet been executed. Discard() - // Exec is to send all the commands buffered in the pipeline to the redis-server. + // Exec sends all the commands buffered in the pipeline to the redis server. Exec(ctx context.Context) ([]Cmder, error) + + // Cmds returns the list of queued commands. + Cmds() []Cmder } var _ Pipeliner = (*Pipeline)(nil) // Pipeline implements pipelining as described in -// http://redis.io/topics/pipelining. +// https://redis.io/docs/latest/develop/using-commands/pipelining. // Please note: it is not safe for concurrent use by multiple goroutines. type Pipeline struct { cmdable @@ -76,7 +82,12 @@ func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd { // Process queues the cmd for later execution. func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error { - c.cmds = append(c.cmds, cmd) + return c.BatchProcess(ctx, cmd) +} + +// BatchProcess queues multiple cmds for later execution. +func (c *Pipeline) BatchProcess(ctx context.Context, cmd ...Cmder) error { + c.cmds = append(c.cmds, cmd...) return nil } @@ -119,3 +130,7 @@ func (c *Pipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([ func (c *Pipeline) TxPipeline() Pipeliner { return c } + +func (c *Pipeline) Cmds() []Cmder { + return c.cmds +} diff --git a/vendor/github.com/redis/go-redis/v9/probabilistic.go b/vendor/github.com/redis/go-redis/v9/probabilistic.go index 02ca263cbd8..ee67911e696 100644 --- a/vendor/github.com/redis/go-redis/v9/probabilistic.go +++ b/vendor/github.com/redis/go-redis/v9/probabilistic.go @@ -225,8 +225,9 @@ type ScanDumpCmd struct { func newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd { return &ScanDumpCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeScanDump, }, } } @@ -270,6 +271,13 @@ func (cmd *ScanDumpCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *ScanDumpCmd) Clone() Cmder { + return &ScanDumpCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // ScanDump is a simple struct, can be copied directly + } +} + // Returns information about a Bloom filter. // For more information - https://redis.io/commands/bf.info/ func (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd { @@ -296,8 +304,9 @@ type BFInfoCmd struct { func NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd { return &BFInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeBFInfo, }, } } @@ -388,6 +397,13 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *BFInfoCmd) Clone() Cmder { + return &BFInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // BFInfo is a simple struct, can be copied directly + } +} + // BFInfoCapacity returns information about the capacity of a Bloom filter. // For more information - https://redis.io/commands/bf.info/ func (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd { @@ -625,8 +641,9 @@ type CFInfoCmd struct { func NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd { return &CFInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeCFInfo, }, } } @@ -692,6 +709,13 @@ func (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *CFInfoCmd) Clone() Cmder { + return &CFInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // CFInfo is a simple struct, can be copied directly + } +} + // CFInfo returns information about a Cuckoo filter. // For more information - https://redis.io/commands/cf.info/ func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd { @@ -787,8 +811,9 @@ type CMSInfoCmd struct { func NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd { return &CMSInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeCMSInfo, }, } } @@ -843,6 +868,13 @@ func (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *CMSInfoCmd) Clone() Cmder { + return &CMSInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // CMSInfo is a simple struct, can be copied directly + } +} + // CMSInfo returns information about a Count-Min Sketch filter. // For more information - https://redis.io/commands/cms.info/ func (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd { @@ -980,8 +1012,9 @@ type TopKInfoCmd struct { func NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd { return &TopKInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeTopKInfo, }, } } @@ -1038,6 +1071,13 @@ func (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *TopKInfoCmd) Clone() Cmder { + return &TopKInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // TopKInfo is a simple struct, can be copied directly + } +} + // TopKInfo returns information about a Top-K filter. // For more information - https://redis.io/commands/topk.info/ func (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd { @@ -1116,18 +1156,14 @@ func (c cmdable) TopKListWithCount(ctx context.Context, key string) *MapStringIn // Returns OK on success or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.add/ func (c cmdable) TDigestAdd(ctx context.Context, key string, elements ...float64) *StatusCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.ADD" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewStatusCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1138,18 +1174,14 @@ func (c cmdable) TDigestAdd(ctx context.Context, key string, elements ...float64 // Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.byrank/ func (c cmdable) TDigestByRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(rank)) + args := make([]interface{}, 2+len(rank)) args[0] = "TDIGEST.BYRANK" args[1] = key - // Convert uint slice to []interface{} - interfaceSlice := make([]interface{}, len(rank)) - for i, v := range rank { - interfaceSlice[i] = v + for i, r := range rank { + args[2+i] = r } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1160,18 +1192,14 @@ func (c cmdable) TDigestByRank(ctx context.Context, key string, rank ...uint64) // Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.byrevrank/ func (c cmdable) TDigestByRevRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(rank)) + args := make([]interface{}, 2+len(rank)) args[0] = "TDIGEST.BYREVRANK" args[1] = key - // Convert uint slice to []interface{} - interfaceSlice := make([]interface{}, len(rank)) - for i, v := range rank { - interfaceSlice[i] = v + for i, r := range rank { + args[2+i] = r } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1182,18 +1210,14 @@ func (c cmdable) TDigestByRevRank(ctx context.Context, key string, rank ...uint6 // Returns an array of floats representing the CDF values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.cdf/ func (c cmdable) TDigestCDF(ctx context.Context, key string, elements ...float64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.CDF" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1243,8 +1267,9 @@ type TDigestInfoCmd struct { func NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd { return &TDigestInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeTDigestInfo, }, } } @@ -1311,6 +1336,13 @@ func (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *TDigestInfoCmd) Clone() Cmder { + return &TDigestInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, // TDigestInfo is a simple struct, can be copied directly + } +} + // TDigestInfo returns information about a t-Digest data structure. // For more information - https://redis.io/commands/tdigest.info/ func (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd { @@ -1376,18 +1408,14 @@ func (c cmdable) TDigestMin(ctx context.Context, key string) *FloatCmd { // Returns an array of floats representing the quantile values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.quantile/ func (c cmdable) TDigestQuantile(ctx context.Context, key string, elements ...float64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.QUANTILE" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1398,18 +1426,14 @@ func (c cmdable) TDigestQuantile(ctx context.Context, key string, elements ...fl // Returns an array of integers representing the rank values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.rank/ func (c cmdable) TDigestRank(ctx context.Context, key string, values ...float64) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) + args := make([]interface{}, 2+len(values)) args[0] = "TDIGEST.RANK" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(values)) for i, v := range values { - interfaceSlice[i] = v + args[i+2] = v } - args = append(args, interfaceSlice...) - cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1431,18 +1455,14 @@ func (c cmdable) TDigestReset(ctx context.Context, key string) *StatusCmd { // Returns an array of integers representing the reverse rank values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.revrank/ func (c cmdable) TDigestRevRank(ctx context.Context, key string, values ...float64) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) + args := make([]interface{}, 2+len(values)) args[0] = "TDIGEST.REVRANK" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(values)) for i, v := range values { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd diff --git a/vendor/github.com/redis/go-redis/v9/pubsub.go b/vendor/github.com/redis/go-redis/v9/pubsub.go index 2a0e7a81e1d..9d696105951 100644 --- a/vendor/github.com/redis/go-redis/v9/pubsub.go +++ b/vendor/github.com/redis/go-redis/v9/pubsub.go @@ -3,17 +3,21 @@ package redis import ( "context" "fmt" + "maps" + "slices" "strings" "sync" "time" "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/otel" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" + "github.com/redis/go-redis/v9/push" ) // PubSub implements Pub/Sub commands as described in -// http://redis.io/topics/pubsub. Message receiving is NOT safe +// https://redis.io/docs/latest/develop/pubsub. Message receiving is NOT safe // for concurrent use by multiple goroutines. // // PubSub automatically reconnects to Redis Server and resubscribes @@ -21,7 +25,7 @@ import ( type PubSub struct { opt *Options - newConn func(ctx context.Context, channels []string) (*pool.Conn, error) + newConn func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) closeConn func(*pool.Conn) error mu sync.Mutex @@ -38,6 +42,12 @@ type PubSub struct { chOnce sync.Once msgCh *channel allCh *channel + + // Push notification processor for handling generic push notifications + pushProcessor push.NotificationProcessor + + // Cleanup callback for maintenanceNotifications upgrade tracking + onClose func() } func (c *PubSub) init() { @@ -48,9 +58,9 @@ func (c *PubSub) String() string { c.mu.Lock() defer c.mu.Unlock() - channels := mapKeys(c.channels) - channels = append(channels, mapKeys(c.patterns)...) - channels = append(channels, mapKeys(c.schannels)...) + channels := slices.Collect(maps.Keys(c.channels)) + channels = append(channels, slices.Collect(maps.Keys(c.patterns))...) + channels = append(channels, slices.Collect(maps.Keys(c.schannels))...) return fmt.Sprintf("PubSub(%s)", strings.Join(channels, ", ")) } @@ -69,10 +79,27 @@ func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, er return c.cn, nil } - channels := mapKeys(c.channels) + if c.opt.Addr == "" { + // TODO(maintenanceNotifications): + // this is probably cluster client + // c.newConn will ignore the addr argument + // will be changed when we have maintenanceNotifications upgrades for cluster clients + c.opt.Addr = internal.RedisNull + } + + // Include c.schannels so reconnect-time routing of an SSubscribe-only + // PubSub picks the slot owner (channels[0] in ClusterClient.pubSub()'s + // newConn closure) instead of a random node. + // See https://github.com/redis/go-redis/issues/3806. + // c.patterns is intentionally NOT included: patterns are not slot- + // addressable, and adding them would force PSubscribe-only PubSubs to + // pin to a single node based on pattern-string hash, regressing the + // existing random-node behaviour. + channels := slices.Collect(maps.Keys(c.channels)) + channels = append(channels, slices.Collect(maps.Keys(c.schannels))...) channels = append(channels, newChannels...) - cn, err := c.newConn(ctx, channels) + cn, err := c.newConn(ctx, c.opt.Addr, channels) if err != nil { return nil, err } @@ -96,18 +123,18 @@ func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error { var firstErr error if len(c.channels) > 0 { - firstErr = c._subscribe(ctx, cn, "subscribe", mapKeys(c.channels)) + firstErr = c._subscribe(ctx, cn, "subscribe", slices.Collect(maps.Keys(c.channels))) } if len(c.patterns) > 0 { - err := c._subscribe(ctx, cn, "psubscribe", mapKeys(c.patterns)) + err := c._subscribe(ctx, cn, "psubscribe", slices.Collect(maps.Keys(c.patterns))) if err != nil && firstErr == nil { firstErr = err } } if len(c.schannels) > 0 { - err := c._subscribe(ctx, cn, "ssubscribe", mapKeys(c.schannels)) + err := c._subscribe(ctx, cn, "ssubscribe", slices.Collect(maps.Keys(c.schannels))) if err != nil && firstErr == nil { firstErr = err } @@ -116,16 +143,6 @@ func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error { return firstErr } -func mapKeys(m map[string]struct{}) []string { - s := make([]string, len(m)) - i := 0 - for k := range m { - s[i] = k - i++ - } - return s -} - func (c *PubSub) _subscribe( ctx context.Context, cn *pool.Conn, redisCmd string, channels []string, ) error { @@ -153,12 +170,32 @@ func (c *PubSub) releaseConn(ctx context.Context, cn *pool.Conn, err error, allo if c.cn != cn { return } + + if !cn.IsUsable() || cn.ShouldHandoff() { + c.reconnect(ctx, fmt.Errorf("pubsub: connection is not usable")) + return + } + if isBadConn(err, allowTimeout, c.opt.Addr) { c.reconnect(ctx, err) } } func (c *PubSub) reconnect(ctx context.Context, reason error) { + if c.cn != nil && c.cn.ShouldHandoff() { + newEndpoint := c.cn.GetHandoffEndpoint() + // If new endpoint is NULL, use the original address + if newEndpoint == internal.RedisNull { + newEndpoint = c.opt.Addr + } + + if newEndpoint != "" { + // Update the address in the options + oldAddr := c.cn.RemoteAddr().String() + c.opt.Addr = newEndpoint + internal.Logger.Printf(ctx, "pubsub: reconnecting to new endpoint %s (was %s)", newEndpoint, oldAddr) + } + } _ = c.closeTheCn(reason) _, _ = c.conn(ctx, nil) } @@ -167,9 +204,6 @@ func (c *PubSub) closeTheCn(reason error) error { if c.cn == nil { return nil } - if !c.closed { - internal.Logger.Printf(c.getContext(), "redis: discarding bad PubSub connection: %s", reason) - } err := c.closeConn(c.cn) c.cn = nil return err @@ -185,6 +219,11 @@ func (c *PubSub) Close() error { c.closed = true close(c.exit) + // Call cleanup callback if set + if c.onClose != nil { + c.onClose() + } + return c.closeTheCn(pool.ErrClosed) } @@ -247,9 +286,7 @@ func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error { } } else { // Unsubscribe from all channels. - for channel := range c.channels { - delete(c.channels, channel) - } + clear(c.channels) } err := c.subscribe(ctx, "unsubscribe", channels...) @@ -268,9 +305,7 @@ func (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error { } } else { // Unsubscribe from all patterns. - for pattern := range c.patterns { - delete(c.patterns, pattern) - } + clear(c.patterns) } err := c.subscribe(ctx, "punsubscribe", patterns...) @@ -289,9 +324,7 @@ func (c *PubSub) SUnsubscribe(ctx context.Context, channels ...string) error { } } else { // Unsubscribe from all channels. - for channel := range c.schannels { - delete(c.schannels, channel) - } + clear(c.schannels) } err := c.subscribe(ctx, "sunsubscribe", channels...) @@ -329,6 +362,25 @@ func (c *PubSub) Ping(ctx context.Context, payload ...string) error { return err } +// ClientSetName assigns a namee to the PubSub connection using CLIENT SETNAME, +// The name is visible in CLIENT LIST output and is useful for debugging +// and identifying connections in a redis instance. +func (c *PubSub) ClientSetName(ctx context.Context, name string) error { + cmd := NewStatusCmd(ctx, "client", "setname", name) + + c.mu.Lock() + defer c.mu.Unlock() + + cn, err := c.conn(ctx, nil) + if err != nil { + return err + } + + err = c.writeCmd(ctx, cn, cmd) + c.releaseConn(ctx, cn, err, false) + return err +} + // Subscription received after a successful subscription to channel. type Subscription struct { // Can be "subscribe", "unsubscribe", "psubscribe" or "punsubscribe". @@ -367,7 +419,7 @@ func (p *Pong) String() string { return "Pong" } -func (c *PubSub) newMessage(reply interface{}) (interface{}, error) { +func (c *PubSub) newMessage(ctx context.Context, cn *pool.Conn, reply interface{}) (interface{}, error) { switch reply := reply.(type) { case string: return &Pong{ @@ -384,30 +436,42 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) { Count: int(reply[2].(int64)), }, nil case "message", "smessage": + channel := reply[1].(string) + sharded := kind == "smessage" switch payload := reply[2].(type) { case string: - return &Message{ - Channel: reply[1].(string), + msg := &Message{ + Channel: channel, Payload: payload, - }, nil + } + // Record PubSub message received + otel.RecordPubSubMessage(ctx, cn, "received", channel, sharded) + return msg, nil case []interface{}: ss := make([]string, len(payload)) for i, s := range payload { ss[i] = s.(string) } - return &Message{ - Channel: reply[1].(string), + msg := &Message{ + Channel: channel, PayloadSlice: ss, - }, nil + } + // Record PubSub message received + otel.RecordPubSubMessage(ctx, cn, "received", channel, sharded) + return msg, nil default: return nil, fmt.Errorf("redis: unsupported pubsub message payload: %T", payload) } case "pmessage": - return &Message{ + channel := reply[2].(string) + msg := &Message{ Pattern: reply[1].(string), - Channel: reply[2].(string), + Channel: channel, Payload: reply[3].(string), - }, nil + } + // Record PubSub message received (pattern message, not sharded) + otel.RecordPubSubMessage(ctx, cn, "received", channel, false) + return msg, nil case "pong": return &Pong{ Payload: reply[1].(string), @@ -429,28 +493,38 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int } // Don't hold the lock to allow subscriptions and pings. - cn, err := c.connWithLock(ctx) if err != nil { return nil, err } err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + // Log the error but don't fail the command execution + // Push notification processing errors shouldn't break normal Redis operations + internal.Logger.Printf(ctx, "push: conn[%d] error processing pending notifications before reading reply: %v", cn.GetID(), err) + } return c.cmd.readReply(rd) }) - c.releaseConnWithLock(ctx, cn, err, timeout > 0) if err != nil { return nil, err } - return c.newMessage(c.cmd.Val()) + return c.newMessage(ctx, cn, c.cmd.Val()) } // Receive returns a message as a Subscription, Message, Pong or error. // See PubSub example for details. This is low-level API and in most cases // Channel should be used instead. +// Receive returns a message as a Subscription, Message, Pong, or an error. +// See PubSub example for details. This is a low-level API and in most cases +// Channel should be used instead. +// This method blocks until a message is received or an error occurs. +// It may return early with an error if the context is canceled, the connection fails, +// or other internal errors occur. func (c *PubSub) Receive(ctx context.Context) (interface{}, error) { return c.ReceiveTimeout(ctx, 0) } @@ -532,6 +606,27 @@ func (c *PubSub) ChannelWithSubscriptions(opts ...ChannelOption) <-chan interfac return c.allCh.allCh } +func (c *PubSub) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error { + // Only process push notifications for RESP3 connections with a processor + if c.opt.Protocol != 3 || c.pushProcessor == nil { + return nil + } + + // Create handler context with client, connection pool, and connection information + handlerCtx := c.pushNotificationHandlerContext(cn) + return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd) +} + +func (c *PubSub) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext { + // PubSub doesn't have a client or connection pool, so we pass nil for those + // PubSub connections are blocking + return push.NotificationHandlerContext{ + PubSub: c, + Conn: cn, + IsBlocking: true, + } +} + type ChannelOption func(c *channel) // WithChannelSize specifies the Go chan size that is used to buffer incoming messages. @@ -667,7 +762,7 @@ func (c *channel) initMsgChan() { } case <-timer.C: internal.Logger.Printf( - ctx, "redis: %s channel is full for %s (message is dropped)", + ctx, "redis: %v channel is full for %s (message is dropped)", c, c.chanSendTimeout) } default: @@ -721,7 +816,7 @@ func (c *channel) initAllChan() { } case <-timer.C: internal.Logger.Printf( - ctx, "redis: %s channel is full for %s (message is dropped)", + ctx, "redis: %v channel is full for %s (message is dropped)", c, c.chanSendTimeout) } default: diff --git a/vendor/github.com/redis/go-redis/v9/pubsub_commands.go b/vendor/github.com/redis/go-redis/v9/pubsub_commands.go index 28622aa6bc3..ccc0ed524fa 100644 --- a/vendor/github.com/redis/go-redis/v9/pubsub_commands.go +++ b/vendor/github.com/redis/go-redis/v9/pubsub_commands.go @@ -1,6 +1,10 @@ package redis -import "context" +import ( + "context" + + "github.com/redis/go-redis/v9/internal/otel" +) type PubSubCmdable interface { Publish(ctx context.Context, channel string, message interface{}) *IntCmd @@ -16,12 +20,20 @@ type PubSubCmdable interface { func (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd { cmd := NewIntCmd(ctx, "publish", channel, message) _ = c(ctx, cmd) + // Record PubSub message sent (if command succeeded) + if cmd.Err() == nil { + otel.RecordPubSubMessage(ctx, nil, "sent", channel, false) + } return cmd } func (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd { cmd := NewIntCmd(ctx, "spublish", channel, message) _ = c(ctx, cmd) + // Record PubSub message sent (if command succeeded) + if cmd.Err() == nil { + otel.RecordPubSubMessage(ctx, nil, "sent", channel, true) + } return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/push/errors.go b/vendor/github.com/redis/go-redis/v9/push/errors.go new file mode 100644 index 00000000000..c10c98aa861 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/errors.go @@ -0,0 +1,176 @@ +package push + +import ( + "errors" + "fmt" +) + +// Push notification error definitions +// This file contains all error types and messages used by the push notification system + +// Error reason constants +const ( + // HandlerReasons + ReasonHandlerNil = "handler cannot be nil" + ReasonHandlerExists = "cannot overwrite existing handler" + ReasonHandlerProtected = "handler is protected" + + // ProcessorReasons + ReasonPushNotificationsDisabled = "push notifications are disabled" +) + +// ProcessorType represents the type of processor involved in the error +// defined as a custom type for better readability and easier maintenance +type ProcessorType string + +const ( + // ProcessorTypes + ProcessorTypeProcessor = ProcessorType("processor") + ProcessorTypeVoidProcessor = ProcessorType("void_processor") + ProcessorTypeCustom = ProcessorType("custom") +) + +// ProcessorOperation represents the operation being performed by the processor +// defined as a custom type for better readability and easier maintenance +type ProcessorOperation string + +const ( + // ProcessorOperations + ProcessorOperationProcess = ProcessorOperation("process") + ProcessorOperationRegister = ProcessorOperation("register") + ProcessorOperationUnregister = ProcessorOperation("unregister") + ProcessorOperationUnknown = ProcessorOperation("unknown") +) + +// Common error variables for reuse +var ( + // ErrHandlerNil is returned when attempting to register a nil handler + ErrHandlerNil = errors.New(ReasonHandlerNil) +) + +// Registry errors + +// ErrHandlerExists creates an error for when attempting to overwrite an existing handler +func ErrHandlerExists(pushNotificationName string) error { + return NewHandlerError(ProcessorOperationRegister, pushNotificationName, ReasonHandlerExists, nil) +} + +// ErrProtectedHandler creates an error for when attempting to unregister a protected handler +func ErrProtectedHandler(pushNotificationName string) error { + return NewHandlerError(ProcessorOperationUnregister, pushNotificationName, ReasonHandlerProtected, nil) +} + +// VoidProcessor errors + +// ErrVoidProcessorRegister creates an error for when attempting to register a handler on void processor +func ErrVoidProcessorRegister(pushNotificationName string) error { + return NewProcessorError(ProcessorTypeVoidProcessor, ProcessorOperationRegister, pushNotificationName, ReasonPushNotificationsDisabled, nil) +} + +// ErrVoidProcessorUnregister creates an error for when attempting to unregister a handler on void processor +func ErrVoidProcessorUnregister(pushNotificationName string) error { + return NewProcessorError(ProcessorTypeVoidProcessor, ProcessorOperationUnregister, pushNotificationName, ReasonPushNotificationsDisabled, nil) +} + +// Error type definitions for advanced error handling + +// HandlerError represents errors related to handler operations +type HandlerError struct { + Operation ProcessorOperation + PushNotificationName string + Reason string + Err error +} + +func (e *HandlerError) Error() string { + if e.Err != nil { + return fmt.Sprintf("handler %s failed for '%s': %s (%v)", e.Operation, e.PushNotificationName, e.Reason, e.Err) + } + return fmt.Sprintf("handler %s failed for '%s': %s", e.Operation, e.PushNotificationName, e.Reason) +} + +func (e *HandlerError) Unwrap() error { + return e.Err +} + +// NewHandlerError creates a new HandlerError +func NewHandlerError(operation ProcessorOperation, pushNotificationName, reason string, err error) *HandlerError { + return &HandlerError{ + Operation: operation, + PushNotificationName: pushNotificationName, + Reason: reason, + Err: err, + } +} + +// ProcessorError represents errors related to processor operations +type ProcessorError struct { + ProcessorType ProcessorType // "processor", "void_processor" + Operation ProcessorOperation // "process", "register", "unregister" + PushNotificationName string // Name of the push notification involved + Reason string + Err error +} + +func (e *ProcessorError) Error() string { + notifInfo := "" + if e.PushNotificationName != "" { + notifInfo = fmt.Sprintf(" for '%s'", e.PushNotificationName) + } + if e.Err != nil { + return fmt.Sprintf("%s %s failed%s: %s (%v)", e.ProcessorType, e.Operation, notifInfo, e.Reason, e.Err) + } + return fmt.Sprintf("%s %s failed%s: %s", e.ProcessorType, e.Operation, notifInfo, e.Reason) +} + +func (e *ProcessorError) Unwrap() error { + return e.Err +} + +// NewProcessorError creates a new ProcessorError +func NewProcessorError(processorType ProcessorType, operation ProcessorOperation, pushNotificationName, reason string, err error) *ProcessorError { + return &ProcessorError{ + ProcessorType: processorType, + Operation: operation, + PushNotificationName: pushNotificationName, + Reason: reason, + Err: err, + } +} + +// Helper functions for common error scenarios + +// IsHandlerNilError checks if an error is due to a nil handler +func IsHandlerNilError(err error) bool { + return errors.Is(err, ErrHandlerNil) +} + +// IsHandlerExistsError checks if an error is due to attempting to overwrite an existing handler. +// This function works correctly even when the error is wrapped. +func IsHandlerExistsError(err error) bool { + var handlerErr *HandlerError + if errors.As(err, &handlerErr) { + return handlerErr.Operation == ProcessorOperationRegister && handlerErr.Reason == ReasonHandlerExists + } + return false +} + +// IsProtectedHandlerError checks if an error is due to attempting to unregister a protected handler. +// This function works correctly even when the error is wrapped. +func IsProtectedHandlerError(err error) bool { + var handlerErr *HandlerError + if errors.As(err, &handlerErr) { + return handlerErr.Operation == ProcessorOperationUnregister && handlerErr.Reason == ReasonHandlerProtected + } + return false +} + +// IsVoidProcessorError checks if an error is due to void processor operations. +// This function works correctly even when the error is wrapped. +func IsVoidProcessorError(err error) bool { + var procErr *ProcessorError + if errors.As(err, &procErr) { + return procErr.ProcessorType == ProcessorTypeVoidProcessor && procErr.Reason == ReasonPushNotificationsDisabled + } + return false +} diff --git a/vendor/github.com/redis/go-redis/v9/push/handler.go b/vendor/github.com/redis/go-redis/v9/push/handler.go new file mode 100644 index 00000000000..815edce3784 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/handler.go @@ -0,0 +1,14 @@ +package push + +import ( + "context" +) + +// NotificationHandler defines the interface for push notification handlers. +type NotificationHandler interface { + // HandlePushNotification processes a push notification with context information. + // The handlerCtx provides information about the client, connection pool, and connection + // on which the notification was received, allowing handlers to make informed decisions. + // Returns an error if the notification could not be handled. + HandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error +} diff --git a/vendor/github.com/redis/go-redis/v9/push/handler_context.go b/vendor/github.com/redis/go-redis/v9/push/handler_context.go new file mode 100644 index 00000000000..c39e186b0da --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/handler_context.go @@ -0,0 +1,44 @@ +package push + +// No imports needed for this file + +// NotificationHandlerContext provides context information about where a push notification was received. +// This struct allows handlers to make informed decisions based on the source of the notification +// with strongly typed access to different client types using concrete types. +type NotificationHandlerContext struct { + // Client is the Redis client instance that received the notification. + // It is interface to both allow for future expansion and to avoid + // circular dependencies. The developer is responsible for type assertion. + // It can be one of the following types: + // - *redis.baseClient + // - *redis.Client + // - *redis.ClusterClient + // - *redis.Conn + Client interface{} + + // ConnPool is the connection pool from which the connection was obtained. + // It is interface to both allow for future expansion and to avoid + // circular dependencies. The developer is responsible for type assertion. + // It can be one of the following types: + // - *pool.ConnPool + // - *pool.SingleConnPool + // - *pool.StickyConnPool + ConnPool interface{} + + // PubSub is the PubSub instance that received the notification. + // It is interface to both allow for future expansion and to avoid + // circular dependencies. The developer is responsible for type assertion. + // It can be one of the following types: + // - *redis.PubSub + PubSub interface{} + + // Conn is the specific connection on which the notification was received. + // It is interface to both allow for future expansion and to avoid + // circular dependencies. The developer is responsible for type assertion. + // It can be one of the following types: + // - *pool.Conn + Conn interface{} + + // IsBlocking indicates if the notification was received on a blocking connection. + IsBlocking bool +} diff --git a/vendor/github.com/redis/go-redis/v9/push/processor.go b/vendor/github.com/redis/go-redis/v9/push/processor.go new file mode 100644 index 00000000000..b8112ddc83d --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/processor.go @@ -0,0 +1,203 @@ +package push + +import ( + "context" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/proto" +) + +// NotificationProcessor defines the interface for push notification processors. +type NotificationProcessor interface { + // GetHandler returns the handler for a specific push notification name. + GetHandler(pushNotificationName string) NotificationHandler + // ProcessPendingNotifications checks for and processes any pending push notifications. + // To be used when it is known that there are notifications on the socket. + // It will try to read from the socket and if it is empty - it may block. + ProcessPendingNotifications(ctx context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error + // RegisterHandler registers a handler for a specific push notification name. + RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error + // UnregisterHandler removes a handler for a specific push notification name. + UnregisterHandler(pushNotificationName string) error +} + +// Processor handles push notifications with a registry of handlers +type Processor struct { + registry *Registry +} + +// NewProcessor creates a new push notification processor +func NewProcessor() *Processor { + return &Processor{ + registry: NewRegistry(), + } +} + +// GetHandler returns the handler for a specific push notification name +func (p *Processor) GetHandler(pushNotificationName string) NotificationHandler { + return p.registry.GetHandler(pushNotificationName) +} + +// RegisterHandler registers a handler for a specific push notification name +func (p *Processor) RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error { + return p.registry.RegisterHandler(pushNotificationName, handler, protected) +} + +// UnregisterHandler removes a handler for a specific push notification name +func (p *Processor) UnregisterHandler(pushNotificationName string) error { + return p.registry.UnregisterHandler(pushNotificationName) +} + +// ProcessPendingNotifications checks for and processes any pending push notifications +// This method should be called by the client in WithReader before reading the reply +// It will try to read from the socket and if it is empty - it may block. +func (p *Processor) ProcessPendingNotifications(ctx context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error { + if rd == nil { + return nil + } + + for { + // Check if there's data available to read + replyType, err := rd.PeekReplyType() + if err != nil { + // No more data available or error reading + // if timeout, it will be handled by the caller + break + } + + // Only process push notifications (arrays starting with >) + if replyType != proto.RespPush { + break + } + + // see if we should skip this notification + notificationName, err := rd.PeekPushNotificationName() + if err != nil { + break + } + + if willHandleNotificationInClient(notificationName) { + break + } + + // Read the push notification + reply, err := rd.ReadReply() + if err != nil { + internal.Logger.Printf(ctx, "push: error reading push notification: %v", err) + break + } + + // Convert to slice of interfaces + notification, ok := reply.([]interface{}) + if !ok { + break + } + + // Handle the notification directly + if len(notification) > 0 { + // Extract the notification type (first element) + if notificationType, ok := notification[0].(string); ok { + // Get the handler for this notification type + if handler := p.registry.GetHandler(notificationType); handler != nil { + // Handle the notification + err := handler.HandlePushNotification(ctx, handlerCtx, notification) + if err != nil { + internal.Logger.Printf(ctx, "push: error handling push notification: %v", err) + } + } + } + } + } + + return nil +} + +// VoidProcessor discards all push notifications without processing them +type VoidProcessor struct{} + +// NewVoidProcessor creates a new void push notification processor +func NewVoidProcessor() *VoidProcessor { + return &VoidProcessor{} +} + +// GetHandler returns nil for void processor since it doesn't maintain handlers +func (v *VoidProcessor) GetHandler(_ string) NotificationHandler { + return nil +} + +// RegisterHandler returns an error for void processor since it doesn't maintain handlers +func (v *VoidProcessor) RegisterHandler(pushNotificationName string, _ NotificationHandler, _ bool) error { + return ErrVoidProcessorRegister(pushNotificationName) +} + +// UnregisterHandler returns an error for void processor since it doesn't maintain handlers +func (v *VoidProcessor) UnregisterHandler(pushNotificationName string) error { + return ErrVoidProcessorUnregister(pushNotificationName) +} + +// ProcessPendingNotifications for VoidProcessor does nothing since push notifications +// are only available in RESP3 and this processor is used for RESP2 connections. +// This avoids unnecessary buffer scanning overhead. +// It does however read and discard all push notifications from the buffer to avoid +// them being interpreted as a reply. +// This method should be called by the client in WithReader before reading the reply +// to be sure there are no buffered push notifications. +// It will try to read from the socket and if it is empty - it may block. +func (v *VoidProcessor) ProcessPendingNotifications(_ context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error { + // read and discard all push notifications + if rd == nil { + return nil + } + + for { + // Check if there's data available to read + replyType, err := rd.PeekReplyType() + if err != nil { + // No more data available or error reading + // if timeout, it will be handled by the caller + break + } + + // Only process push notifications (arrays starting with >) + if replyType != proto.RespPush { + break + } + // see if we should skip this notification + notificationName, err := rd.PeekPushNotificationName() + if err != nil { + break + } + + if willHandleNotificationInClient(notificationName) { + break + } + + // Read the push notification + _, err = rd.ReadReply() + if err != nil { + internal.Logger.Printf(context.Background(), "push: error reading push notification: %v", err) + return nil + } + } + return nil +} + +// willHandleNotificationInClient checks if a notification type should be ignored by the push notification +// processor and handled by other specialized systems instead (pub/sub, streams, keyspace, etc.). +func willHandleNotificationInClient(notificationType string) bool { + switch notificationType { + // Pub/Sub notifications - handled by pub/sub system + case "message", // Regular pub/sub message + "pmessage", // Pattern pub/sub message + "subscribe", // Subscription confirmation + "unsubscribe", // Unsubscription confirmation + "psubscribe", // Pattern subscription confirmation + "punsubscribe", // Pattern unsubscription confirmation + "smessage", // Sharded pub/sub message (Redis 7.0+) + "ssubscribe", // Sharded subscription confirmation + "sunsubscribe": // Sharded unsubscription confirmation + return true + default: + return false + } +} diff --git a/vendor/github.com/redis/go-redis/v9/push/push.go b/vendor/github.com/redis/go-redis/v9/push/push.go new file mode 100644 index 00000000000..e6adeaa456c --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/push.go @@ -0,0 +1,7 @@ +// Package push provides push notifications for Redis. +// This is an EXPERIMENTAL API for handling push notifications from Redis. +// It is not yet stable and may change in the future. +// Although this is in a public package, in its current form public use is not advised. +// Pending push notifications should be processed before executing any readReply from the connection +// as per RESP3 specification push notifications can be sent at any time. +package push diff --git a/vendor/github.com/redis/go-redis/v9/push/registry.go b/vendor/github.com/redis/go-redis/v9/push/registry.go new file mode 100644 index 00000000000..a265ae92f98 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push/registry.go @@ -0,0 +1,61 @@ +package push + +import ( + "sync" +) + +// Registry manages push notification handlers +type Registry struct { + mu sync.RWMutex + handlers map[string]NotificationHandler + protected map[string]bool +} + +// NewRegistry creates a new push notification registry +func NewRegistry() *Registry { + return &Registry{ + handlers: make(map[string]NotificationHandler), + protected: make(map[string]bool), + } +} + +// RegisterHandler registers a handler for a specific push notification name +func (r *Registry) RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error { + if handler == nil { + return ErrHandlerNil + } + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if handler already exists + if _, exists := r.protected[pushNotificationName]; exists { + return ErrHandlerExists(pushNotificationName) + } + + r.handlers[pushNotificationName] = handler + r.protected[pushNotificationName] = protected + return nil +} + +// GetHandler returns the handler for a specific push notification name +func (r *Registry) GetHandler(pushNotificationName string) NotificationHandler { + r.mu.RLock() + defer r.mu.RUnlock() + return r.handlers[pushNotificationName] +} + +// UnregisterHandler removes a handler for a specific push notification name +func (r *Registry) UnregisterHandler(pushNotificationName string) error { + r.mu.Lock() + defer r.mu.Unlock() + + // Check if handler is protected + if protected, exists := r.protected[pushNotificationName]; exists && protected { + return ErrProtectedHandler(pushNotificationName) + } + + delete(r.handlers, pushNotificationName) + delete(r.protected, pushNotificationName) + return nil +} diff --git a/vendor/github.com/redis/go-redis/v9/push_notifications.go b/vendor/github.com/redis/go-redis/v9/push_notifications.go new file mode 100644 index 00000000000..572955fecbb --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/push_notifications.go @@ -0,0 +1,21 @@ +package redis + +import ( + "github.com/redis/go-redis/v9/push" +) + +// NewPushNotificationProcessor creates a new push notification processor +// This processor maintains a registry of handlers and processes push notifications +// It is used for RESP3 connections where push notifications are available +func NewPushNotificationProcessor() push.NotificationProcessor { + return push.NewProcessor() +} + +// NewVoidPushNotificationProcessor creates a new void push notification processor +// This processor does not maintain any handlers and always returns nil for all operations +// It is used for RESP2 connections where push notifications are not available +// It can also be used to disable push notifications for RESP3 connections, where +// it will discard all push notifications without processing them +func NewVoidPushNotificationProcessor() push.NotificationProcessor { + return push.NewVoidProcessor() +} diff --git a/vendor/github.com/redis/go-redis/v9/redis.go b/vendor/github.com/redis/go-redis/v9/redis.go index bafe82f7527..dd34518907f 100644 --- a/vendor/github.com/redis/go-redis/v9/redis.go +++ b/vendor/github.com/redis/go-redis/v9/redis.go @@ -11,9 +11,13 @@ import ( "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/auth/streaming" "github.com/redis/go-redis/v9/internal/hscan" + "github.com/redis/go-redis/v9/internal/otel" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/redis/go-redis/v9/push" ) // Scanner internal/hscan.Scanner exposed interface. @@ -22,11 +26,29 @@ type Scanner = hscan.Scanner // Nil reply returned by Redis when key does not exist. const Nil = proto.Nil +// String representations of special float values. +// Values are lowercase for consistency with Redis RESP2 protocol responses. +const ( + NaN = internal.NaN // Not a Number + Inf = internal.Inf // Positive infinity + NInf = internal.NInf // Negative infinity +) + // SetLogger set custom log +// Use with VoidLogger to disable logging. +// If logger is nil, the call is ignored and the existing logger is kept. func SetLogger(logger internal.Logging) { + if logger == nil { + return + } internal.Logger = logger } +// SetLogLevel sets the log level for the library. +func SetLogLevel(logLevel internal.LogLevelT) { + internal.LogLevel = logLevel +} + //------------------------------------------------------------------------------ type Hook interface { @@ -201,21 +223,150 @@ func (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) e //------------------------------------------------------------------------------ +// Stable identifiers for baseClient.onClose hooks. Each component that +// registers a close callback owns a dedicated id here so the set of known +// hooks is discoverable in one place and id collisions are caught at +// compile time. New ids should be added as additional constants. +const ( + // onCloseHookIDSentinelFailover identifies the close callback installed + // by NewFailoverClient to tear down sentinel failover background work. + onCloseHookIDSentinelFailover = "sentinel-failover" +) + +// onCloseHooks is a small registry of named close callbacks attached to a +// baseClient. Each callback is identified by a stable string id; registering +// the same id twice replaces the previous callback rather than chaining onto +// it. This guarantees the registry stays bounded regardless of how often a +// hook is (re)registered and avoids the unbounded closure chain that +// motivated issue #3772. +// +// Hooks are invoked in registration order. All hooks run regardless of +// individual errors; the first non-nil error is returned. +// +// A zero-value onCloseHooks is ready to use. It is safe for concurrent use. +// Clones of a baseClient share the same *onCloseHooks so registrations and +// close semantics are preserved across WithTimeout / WithContext / etc. +type onCloseHooks struct { + mu sync.Mutex + order []string + hooks map[string]func() error +} + +// register adds or replaces the callback associated with id. Re-registering +// an existing id overwrites the previous callback in place; new ids are +// appended to the invocation order. +func (h *onCloseHooks) register(id string, fn func() error) { + h.mu.Lock() + defer h.mu.Unlock() + if h.hooks == nil { + h.hooks = make(map[string]func() error) + } + if _, exists := h.hooks[id]; !exists { + h.order = append(h.order, id) + } + h.hooks[id] = fn +} + +// unregister removes the callback associated with id, if any. It is kept +// for API symmetry with register so future callers (e.g. dynamic hook +// owners that need to detach before client Close) do not have to +// reinvent it. +// +//nolint:unused // kept for API symmetry with register; see comment above. +func (h *onCloseHooks) unregister(id string) { + h.mu.Lock() + defer h.mu.Unlock() + if _, exists := h.hooks[id]; !exists { + return + } + delete(h.hooks, id) + for i, x := range h.order { + if x == id { + h.order = append(h.order[:i], h.order[i+1:]...) + break + } + } +} + +// run invokes all registered callbacks in registration order and returns +// the first non-nil error encountered. All callbacks are executed even if +// an earlier one returns an error. +func (h *onCloseHooks) run() error { + if h == nil { + return nil + } + h.mu.Lock() + fns := make([]func() error, 0, len(h.order)) + for _, id := range h.order { + if fn := h.hooks[id]; fn != nil { + fns = append(fns, fn) + } + } + h.mu.Unlock() + + var firstErr error + for _, fn := range fns { + if err := fn(); err != nil && firstErr == nil { + firstErr = err + } + } + return firstErr +} + type baseClient struct { - opt *Options - connPool pool.Pooler + opt *Options + optLock sync.RWMutex + connPool pool.Pooler + pubSubPool *pool.PubSubPool hooksMixin - onClose func() error // hook called when client is closed + // onClose holds named callbacks invoked when the client is closed. + // Registering a new callback never removes previously registered ones; + // only re-registering the same id replaces the existing callback. This + // lets composing components (e.g. sentinel failover) add close logic + // safely without fear of overwriting each other and without building + // unbounded closure chains on repeated registration. + onClose *onCloseHooks + + // Push notification processing + pushProcessor push.NotificationProcessor + + // Maintenance notifications manager + maintNotificationsManager *maintnotifications.Manager + maintNotificationsManagerLock sync.RWMutex + + // streamingCredentialsManager is used to manage streaming credentials + streamingCredentialsManager *streaming.Manager } func (c *baseClient) clone() *baseClient { - clone := *c - return &clone + c.maintNotificationsManagerLock.RLock() + maintNotificationsManager := c.maintNotificationsManager + c.maintNotificationsManagerLock.RUnlock() + + clone := &baseClient{ + opt: c.opt, + connPool: c.connPool, + pubSubPool: c.pubSubPool, + onClose: c.onClose, + pushProcessor: c.pushProcessor, + maintNotificationsManager: maintNotificationsManager, + streamingCredentialsManager: c.streamingCredentialsManager, + } + return clone +} + +// cloneOpt clones c.opt while holding optLock to prevent races with initConn +// which writes to MaintNotificationsConfig.Mode under the same lock. +func (c *baseClient) cloneOpt() *Options { + c.optLock.RLock() + clone := c.opt.clone() + c.optLock.RUnlock() + return clone } func (c *baseClient) withTimeout(timeout time.Duration) *baseClient { - opt := c.opt.clone() + opt := c.cloneOpt() opt.ReadTimeout = timeout opt.WriteTimeout = timeout @@ -229,21 +380,6 @@ func (c *baseClient) String() string { return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB) } -func (c *baseClient) newConn(ctx context.Context) (*pool.Conn, error) { - cn, err := c.connPool.NewConn(ctx) - if err != nil { - return nil, err - } - - err = c.initConn(ctx, cn) - if err != nil { - _ = c.connPool.CloseConn(cn) - return nil, err - } - - return cn, nil -} - func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) { if c.opt.Limiter != nil { err := c.opt.Limiter.Allow() @@ -269,7 +405,7 @@ func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) { return nil, err } - if cn.Inited { + if cn.IsInited() { return cn, nil } @@ -281,39 +417,54 @@ func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) { return nil, err } - return cn, nil -} + if dialStartNs := cn.GetDialStartNs(); dialStartNs > 0 { + if cb := pool.GetMetricConnectionCreateTimeCallback(); cb != nil { + duration := time.Duration(time.Now().UnixNano() - dialStartNs) + cb(ctx, duration, cn) + } + } + + // initConn will transition to IDLE state, so we need to acquire it + // before returning it to the user. + if !cn.TryAcquire() { + return nil, fmt.Errorf("redis: connection is not usable") + } -func (c *baseClient) newReAuthCredentialsListener(poolCn *pool.Conn) auth.CredentialsListener { - return auth.NewReAuthCredentialsListener( - c.reAuthConnection(poolCn), - c.onAuthenticationErr(poolCn), - ) + return cn, nil } -func (c *baseClient) reAuthConnection(poolCn *pool.Conn) func(credentials auth.Credentials) error { - return func(credentials auth.Credentials) error { +func (c *baseClient) reAuthConnection() func(poolCn *pool.Conn, credentials auth.Credentials) error { + return func(poolCn *pool.Conn, credentials auth.Credentials) error { var err error username, password := credentials.BasicAuth() + + // Use background context - timeout is handled by ReadTimeout in WithReader/WithWriter ctx := context.Background() + connPool := pool.NewSingleConnPool(c.connPool, poolCn) - // hooksMixin are intentionally empty here - cn := newConn(c.opt, connPool, nil) + + // Pass hooks so that reauth commands are recorded/traced + cn := newConn(c.opt, connPool, &c.hooksMixin) if username != "" { err = cn.AuthACL(ctx, username, password).Err() } else { err = cn.Auth(ctx, password).Err() } + return err } } -func (c *baseClient) onAuthenticationErr(poolCn *pool.Conn) func(err error) { - return func(err error) { +func (c *baseClient) onAuthenticationErr() func(poolCn *pool.Conn, err error) { + return func(poolCn *pool.Conn, err error) { if err != nil { if isBadConn(err, false, c.opt.Addr) { // Close the connection to force a reconnection. - err := c.connPool.CloseConn(poolCn) + // Re-auth happens on connections that were idle in the pool (the pool hook + // waits for IDLE state before transitioning to UNUSABLE for re-auth). + // From metrics perspective, the connection was never "used" by a client. + // Note: Using context.Background() as this callback doesn't have access to caller's context. + err := c.connPool.CloseConn(context.Background(), poolCn, pool.CloseReasonAuthError, pool.MetricStateIdle) if err != nil { internal.Logger.Printf(context.Background(), "redis: failed to close connection: %v", err) // try to close the network connection directly @@ -329,51 +480,129 @@ func (c *baseClient) onAuthenticationErr(poolCn *pool.Conn) func(err error) { } } -func (c *baseClient) wrappedOnClose(newOnClose func() error) func() error { - onClose := c.onClose - return func() error { - var firstErr error - err := newOnClose() - // Even if we have an error we would like to execute the onClose hook - // if it exists. We will return the first error that occurred. - // This is to keep error handling consistent with the rest of the code. +func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { + // This function is called in two scenarios: + // 1. First-time init: Connection is in CREATED state (from pool.Get()) + // - We need to transition CREATED → INITIALIZING and do the initialization + // - If another goroutine is already initializing, we WAIT for it to finish + // 2. Re-initialization: Connection is in INITIALIZING state (from SetNetConnAndInitConn()) + // - We're already in INITIALIZING, so just proceed with initialization + + currentState := cn.GetStateMachine().GetState() + + // Fast path: Check if already initialized (IDLE or IN_USE) + if currentState == pool.StateIdle || currentState == pool.StateInUse { + return nil + } + + // If in CREATED state, try to transition to INITIALIZING + if currentState == pool.StateCreated { + finalState, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateCreated}, pool.StateInitializing) if err != nil { - firstErr = err - } - if onClose != nil { - err = onClose() - if err != nil && firstErr == nil { - firstErr = err + // Another goroutine is initializing or connection is in unexpected state + // Check what state we're in now + if finalState == pool.StateIdle || finalState == pool.StateInUse { + // Already initialized by another goroutine + return nil } - } - return firstErr - } -} -func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { - if cn.Inited { - return nil + if finalState == pool.StateInitializing { + // Another goroutine is initializing - WAIT for it to complete + // Use a context with timeout = min(remaining command timeout, DialTimeout) + // This prevents waiting too long while respecting the caller's deadline + var waitCtx context.Context + var cancel context.CancelFunc + dialTimeout := c.opt.DialTimeout + + if cmdDeadline, hasCmdDeadline := ctx.Deadline(); hasCmdDeadline { + // Calculate remaining time until command deadline + remainingTime := time.Until(cmdDeadline) + // Use the minimum of remaining time and DialTimeout + if remainingTime < dialTimeout { + // Command deadline is sooner, use it + waitCtx = ctx + } else { + // DialTimeout is shorter, cap the wait at DialTimeout + waitCtx, cancel = context.WithTimeout(ctx, dialTimeout) + } + } else { + // No command deadline, use DialTimeout to prevent waiting indefinitely + waitCtx, cancel = context.WithTimeout(ctx, dialTimeout) + } + if cancel != nil { + defer cancel() + } + + finalState, err := cn.GetStateMachine().AwaitAndTransition( + waitCtx, + []pool.ConnState{pool.StateIdle, pool.StateInUse}, + pool.StateIdle, // Target is IDLE (but we're already there, so this is a no-op) + ) + if err != nil { + return err + } + // Verify we're now initialized + if finalState == pool.StateIdle || finalState == pool.StateInUse { + return nil + } + // Unexpected state after waiting + return fmt.Errorf("connection in unexpected state after initialization: %s", finalState) + } + + // Unexpected state (CLOSED, UNUSABLE, etc.) + return err + } } - var err error - cn.Inited = true + // At this point, we're in INITIALIZING state and we own the initialization + // If we fail, we must transition to CLOSED + var initErr error connPool := pool.NewSingleConnPool(c.connPool, cn) conn := newConn(c.opt, connPool, &c.hooksMixin) username, password := "", "" if c.opt.StreamingCredentialsProvider != nil { - credentials, unsubscribeFromCredentialsProvider, err := c.opt.StreamingCredentialsProvider. - Subscribe(c.newReAuthCredentialsListener(cn)) - if err != nil { - return fmt.Errorf("failed to subscribe to streaming credentials: %w", err) + credListener, initErr := c.streamingCredentialsManager.Listener( + cn, + c.reAuthConnection(), + c.onAuthenticationErr(), + ) + if initErr != nil { + cn.GetStateMachine().Transition(pool.StateClosed) + return fmt.Errorf("failed to create credentials listener: %w", initErr) } - c.onClose = c.wrappedOnClose(unsubscribeFromCredentialsProvider) + + credentials, unsubscribeFromCredentialsProvider, initErr := c.opt.StreamingCredentialsProvider. + Subscribe(credListener) + if initErr != nil { + cn.GetStateMachine().Transition(pool.StateClosed) + return fmt.Errorf("failed to subscribe to streaming credentials: %w", initErr) + } + + // Per-connection unsubscribe is attached to the connection itself so it + // runs when this specific connection is closed. Do not register it on + // c.onClose: initConn runs for every (re)initialized connection, and + // attaching per-connection state to the shared baseClient registry would + // either leak entries (one per connection id, never trimmed) or — with + // the pre-fix wrappedOnClose approach — build an unbounded closure chain + // retaining every prior connection's unsubscribe (see issue #3772). + // + // Note: pool.Conn.SetOnClose OVERWRITES any prior callback (see the + // doc on that method). That is safe here because the streaming + // credentials Manager deduplicates listeners by connection id, so a + // second initConn on the same cn re-Subscribes the SAME listener and + // the returned unsubscribe is equivalent to the one already installed. + // Any future code path that could hand out a distinct unsubscribe on + // re-initialization must first invoke the existing one to avoid + // orphaning the old subscription on the credentials provider. cn.SetOnClose(unsubscribeFromCredentialsProvider) + username, password = credentials.BasicAuth() } else if c.opt.CredentialsProviderContext != nil { - username, password, err = c.opt.CredentialsProviderContext(ctx) - if err != nil { - return fmt.Errorf("failed to get credentials from context provider: %w", err) + username, password, initErr = c.opt.CredentialsProviderContext(ctx) + if initErr != nil { + cn.GetStateMachine().Transition(pool.StateClosed) + return fmt.Errorf("failed to get credentials from context provider: %w", initErr) } } else if c.opt.CredentialsProvider != nil { username, password = c.opt.CredentialsProvider() @@ -383,9 +612,14 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { + // helloOK tracks whether HELLO succeeded. If it did not, the connection + // falls back to RESP2 regardless of c.opt.Protocol, and features that + // require RESP3 (e.g. maintenance notifications) must be skipped. + helloOK := false + if initErr = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); initErr == nil { // Authentication successful with HELLO command - } else if !isRedisError(err) { + helloOK = true + } else if !isRedisError(initErr) { // When the server responds with the RESP protocol and the result is not a normal // execution result of the HELLO command, we consider it to be an indication that // the server does not support the HELLO command. @@ -393,20 +627,22 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // or it could be DragonflyDB or a third-party redis-proxy. They all respond // with different error string results for unsupported commands, making it // difficult to rely on error strings to determine all results. - return err + cn.GetStateMachine().Transition(pool.StateClosed) + return initErr } else if password != "" { // Try legacy AUTH command if HELLO failed if username != "" { - err = conn.AuthACL(ctx, username, password).Err() + initErr = conn.AuthACL(ctx, username, password).Err() } else { - err = conn.Auth(ctx, password).Err() + initErr = conn.Auth(ctx, password).Err() } - if err != nil { - return fmt.Errorf("failed to authenticate: %w", err) + if initErr != nil { + cn.GetStateMachine().Transition(pool.StateClosed) + return fmt.Errorf("failed to authenticate: %w", initErr) } } - _, err = conn.Pipelined(ctx, func(pipe Pipeliner) error { + _, initErr = conn.Pipelined(ctx, func(pipe Pipeliner) error { if c.opt.DB > 0 { pipe.Select(ctx, c.opt.DB) } @@ -421,8 +657,95 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return nil }) - if err != nil { - return fmt.Errorf("failed to initialize connection options: %w", err) + if initErr != nil { + cn.GetStateMachine().Transition(pool.StateClosed) + return fmt.Errorf("failed to initialize connection options: %w", initErr) + } + + // Enable maintnotifications if maintnotifications are configured + c.optLock.RLock() + maintNotifEnabled := c.opt.MaintNotificationsConfig != nil && c.opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled + protocol := c.opt.Protocol + var endpointType maintnotifications.EndpointType + var maintNotifMode maintnotifications.Mode + if maintNotifEnabled { + endpointType = c.opt.MaintNotificationsConfig.EndpointType + maintNotifMode = c.opt.MaintNotificationsConfig.Mode + } + c.optLock.RUnlock() + + // Maintenance notifications require RESP3 push frames. If HELLO failed + // and the connection fell back to RESP2, there is no point in sending + // CLIENT MAINT_NOTIFICATIONS: the server either rejects it (making the + // error misleading) or accepts it silently, leaving the client unable + // to receive any notifications. Decide based on the actual negotiated + // protocol rather than the requested one. + if maintNotifEnabled && protocol == 3 && !helloOK { + if maintNotifMode == maintnotifications.ModeEnabled { + // Explicitly requested - fail fast with a clear reason. + cn.GetStateMachine().Transition(pool.StateClosed) + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorCallback(ctx, "HANDSHAKE_FAILED", cn, "HANDSHAKE_FAILED", true, 0) + } + return fmt.Errorf("failed to enable maintnotifications: server does not support RESP3 (HELLO command failed)") + } + // auto/other modes: silently disable maintnotifications for this client. + c.optLock.Lock() + c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled + c.optLock.Unlock() + if err := c.disableMaintNotificationsUpgrades(); err != nil { + internal.Logger.Printf(ctx, "failed to disable maintnotifications in auto mode: %v", err) + } + maintNotifEnabled = false + } + + var maintNotifHandshakeErr error + if maintNotifEnabled && protocol == 3 { + maintNotifHandshakeErr = conn.ClientMaintNotifications( + ctx, + true, + endpointType.String(), + ).Err() + if maintNotifHandshakeErr != nil { + if !isRedisError(maintNotifHandshakeErr) { + // if not redis error, fail the connection + cn.GetStateMachine().Transition(pool.StateClosed) + return maintNotifHandshakeErr + } + c.optLock.Lock() + // handshake failed - check and modify config atomically + switch c.opt.MaintNotificationsConfig.Mode { + case maintnotifications.ModeEnabled: + // enabled mode, fail the connection + c.optLock.Unlock() + cn.GetStateMachine().Transition(pool.StateClosed) + + // Record handshake failure metric + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorCallback(ctx, "HANDSHAKE_FAILED", cn, "HANDSHAKE_FAILED", true, 0) + } + + return fmt.Errorf("failed to enable maintnotifications: %w", maintNotifHandshakeErr) + default: // will handle auto and any other + // Disabling logging here as it's too noisy. + // TODO: Enable when we have a better logging solution for log levels + // internal.Logger.Printf(ctx, "auto mode fallback: maintnotifications disabled due to handshake error: %v", maintNotifHandshakeErr) + c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled + c.optLock.Unlock() + // auto mode, disable maintnotifications and continue + if initErr := c.disableMaintNotificationsUpgrades(); initErr != nil { + // Log error but continue - auto mode should be resilient + internal.Logger.Printf(ctx, "failed to disable maintnotifications in auto mode: %v", initErr) + } + } + } else { + // handshake was executed successfully + // to make sure that the handshake will be executed on other connections as well if it was successfully + // executed on this connection, we will force the handshake to be executed on all connections + c.optLock.Lock() + c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeEnabled + c.optLock.Unlock() + } } if !c.opt.DisableIdentity && !c.opt.DisableIndentity { @@ -436,13 +759,31 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { p.ClientSetInfo(ctx, WithLibraryVersion(libVer)) // Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid // out of order responses later on. - if _, err = p.Exec(ctx); err != nil && !isRedisError(err) { - return err + if _, initErr = p.Exec(ctx); initErr != nil && !isRedisError(initErr) { + cn.GetStateMachine().Transition(pool.StateClosed) + return initErr } } + // Set the connection initialization function for potential reconnections + // This must be set before transitioning to IDLE so that handoff/reauth can use it + cn.SetInitConnFunc(c.createInitConnFunc()) + + // Initialization succeeded - transition to IDLE state + // This marks the connection as initialized and ready for use + // NOTE: The connection is still owned by the calling goroutine at this point + // and won't be available to other goroutines until it's Put() back into the pool + cn.GetStateMachine().Transition(pool.StateIdle) + + // Call OnConnect hook if configured + // The connection is in IDLE state but still owned by this goroutine + // If OnConnect needs to send commands, it can use the connection safely if c.opt.OnConnect != nil { - return c.opt.OnConnect(ctx, conn) + if initErr = c.opt.OnConnect(ctx, conn); initErr != nil { + // OnConnect failed - transition to closed + cn.GetStateMachine().Transition(pool.StateClosed) + return initErr + } } return nil @@ -456,6 +797,10 @@ func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) if isBadConn(err, false, c.opt.Addr) { c.connPool.Remove(ctx, cn, err) } else { + // process any pending push notifications before returning the connection to the pool + if err := c.processPushNotifications(ctx, cn); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before releasing connection: %v", err) + } c.connPool.Put(ctx, cn) } } @@ -483,42 +828,143 @@ func (c *baseClient) dial(ctx context.Context, network, addr string) (net.Conn, } func (c *baseClient) process(ctx context.Context, cmd Cmder) error { + // Start measuring total operation duration (includes all retries) + // Only call time.Now() if operation duration callback is set to avoid overhead + var operationStart time.Time + opDurationCallback := otel.GetOperationDurationCallback() + if opDurationCallback != nil { + operationStart = time.Now() + } + var lastConn *pool.Conn + var lastErr error + totalAttempts := 0 for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { + totalAttempts++ attempt := attempt - retry, err := c._process(ctx, cmd, attempt) - if err == nil || !retry { + retry, cn, err := c._process(ctx, cmd, attempt) + if cn != nil { + lastConn = cn + } + // Don't retry if command explicitly disables retries (e.g., RawWriteToCmd + // which writes directly to an io.Writer and cannot undo partial writes) + if err == nil || !retry || cmd.NoRetry() { + // Record total operation duration + if opDurationCallback != nil { + operationDuration := time.Since(operationStart) + opDurationCallback(ctx, operationDuration, cmd, totalAttempts, err, lastConn, c.opt.DB) + } + + if err != nil { + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorType, statusCode, isInternal := classifyCommandError(err) + errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1) + } + } return err } lastErr = err } + + // Record failed operation after all retries + if opDurationCallback != nil { + operationDuration := time.Since(operationStart) + opDurationCallback(ctx, operationDuration, cmd, totalAttempts, lastErr, lastConn, c.opt.DB) + } + + // Record error metric for exhausted retries + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorType, statusCode, isInternal := classifyCommandError(lastErr) + errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1) + } + return lastErr } -func (c *baseClient) assertUnstableCommand(cmd Cmder) bool { - switch cmd.(type) { - case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd: - if c.opt.UnstableResp3 { - return true - } else { - panic("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3 . See the [README](https://github.com/redis/go-redis/blob/master/README.md) and the release notes for guidance.") +// classifyCommandError classifies an error for metrics reporting. +// Returns: errorType, statusCode, isInternal +// - errorType: A string describing the error type (e.g., "TIMEOUT", "NETWORK", "ERR") +// - statusCode: The Redis error prefix or error category +// - isInternal: true for network/timeout errors, false for Redis server errors +func classifyCommandError(err error) (errorType, statusCode string, isInternal bool) { + if err == nil { + return "", "", false + } + + errStr := err.Error() + + // Check for timeout errors + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + return "TIMEOUT", "TIMEOUT", true + } + + // Check for network errors + if _, ok := err.(net.Error); ok { + return "NETWORK", "NETWORK", true + } + + // Check for context errors + if errors.Is(err, context.Canceled) { + return "CONTEXT_CANCELED", "CONTEXT_CANCELED", true + } + if errors.Is(err, context.DeadlineExceeded) { + return "CONTEXT_TIMEOUT", "CONTEXT_TIMEOUT", true + } + + // Check for Redis errors + // Examples: "ERR ...", "WRONGTYPE ...", "CLUSTERDOWN ..." + if len(errStr) > 0 { + // Find the first space to extract the prefix + spaceIdx := 0 + for i, c := range errStr { + if c == ' ' { + spaceIdx = i + break + } + } + if spaceIdx == 0 { + spaceIdx = len(errStr) + } + prefix := errStr[:spaceIdx] + isUppercase := true + for _, c := range prefix { + if c < 'A' || c > 'Z' { + isUppercase = false + break + } + } + if isUppercase && len(prefix) > 0 { + return prefix, prefix, false } - default: - return false } + + return "UNKNOWN", "UNKNOWN", true +} + +func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) { + // All search commands (FTSearchCmd, AggregateCmd, FTInfoCmd, FTSpellCheckCmd, FTSynDumpCmd) + // now have stable RESP3 parsing. No commands require the UnstableResp3 flag anymore. + return false, nil } -func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) { +func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, *pool.Conn, error) { if attempt > 0 { if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { - return false, err + return false, nil, err } } + var usedConn *pool.Conn retryTimeout := uint32(0) if err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + usedConn = cn + // Process any pending push notifications before executing the command + if err := c.processPushNotifications(ctx, cn); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before command: %v", err) + } + if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmd(wr, cmd) }); err != nil { @@ -527,10 +973,22 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool } readReplyFunc := cmd.readReply // Apply unstable RESP3 search module. - if c.opt.Protocol != 2 && c.assertUnstableCommand(cmd) { - readReplyFunc = cmd.readRawReply + if c.opt.Protocol != 2 { + useRawReply, err := c.assertUnstableCommand(cmd) + if err != nil { + return err + } + if useRawReply { + readReplyFunc = cmd.readRawReply + } } - if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), readReplyFunc); err != nil { + if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), func(rd *proto.Reader) error { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } + return readReplyFunc(rd) + }); err != nil { if cmd.readTimeout() == nil { atomic.StoreUint32(&retryTimeout, 1) } else { @@ -542,10 +1000,10 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool return nil }); err != nil { retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1) - return retry, err + return retry, usedConn, err } - return false, nil + return false, usedConn, nil } func (c *baseClient) retryBackoff(attempt int) time.Duration { @@ -573,19 +1031,78 @@ func (c *baseClient) context(ctx context.Context) context.Context { return context.Background() } +// createInitConnFunc creates a connection initialization function that can be used for reconnections. +func (c *baseClient) createInitConnFunc() func(context.Context, *pool.Conn) error { + return func(ctx context.Context, cn *pool.Conn) error { + return c.initConn(ctx, cn) + } +} + +// enableMaintNotificationsUpgrades initializes the maintnotifications upgrade manager and pool hook. +// This function is called during client initialization. +// will register push notification handlers for all maintenance upgrade events. +// will start background workers for handoff processing in the pool hook. +func (c *baseClient) enableMaintNotificationsUpgrades() error { + // Create client adapter + clientAdapterInstance := newClientAdapter(c) + + // Create maintnotifications manager directly + manager, err := maintnotifications.NewManager(clientAdapterInstance, c.connPool, c.opt.MaintNotificationsConfig) + if err != nil { + return err + } + // Set the manager reference and initialize pool hook + c.maintNotificationsManagerLock.Lock() + c.maintNotificationsManager = manager + c.maintNotificationsManagerLock.Unlock() + + // Initialize pool hook (safe to call without lock since manager is now set) + manager.InitPoolHook(c.dialHook) + return nil +} + +func (c *baseClient) disableMaintNotificationsUpgrades() error { + c.maintNotificationsManagerLock.Lock() + defer c.maintNotificationsManagerLock.Unlock() + + // Close the maintnotifications manager + if c.maintNotificationsManager != nil { + // Closing the manager will also shutdown the pool hook + // and remove it from the pool + c.maintNotificationsManager.Close() + c.maintNotificationsManager = nil + } + return nil +} + // Close closes the client, releasing any open resources. // // It is rare to Close a Client, as the Client is meant to be // long-lived and shared between many goroutines. func (c *baseClient) Close() error { var firstErr error - if c.onClose != nil { - if err := c.onClose(); err != nil { + + // Close maintnotifications manager first + if err := c.disableMaintNotificationsUpgrades(); err != nil { + firstErr = err + } + + if err := c.onClose.run(); err != nil && firstErr == nil { + firstErr = err + } + + // Unregister pools from OTel before closing them + otel.UnregisterPools(c.connPool, c.pubSubPool) + + if c.connPool != nil { + if err := c.connPool.Close(); err != nil && firstErr == nil { firstErr = err } } - if err := c.connPool.Close(); err != nil && firstErr == nil { - firstErr = err + if c.pubSubPool != nil { + if err := c.pubSubPool.Close(); err != nil && firstErr == nil { + firstErr = err + } } return firstErr } @@ -595,14 +1112,14 @@ func (c *baseClient) getAddr() string { } func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error { - if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds); err != nil { + if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds, "PIPELINE"); err != nil { return err } return cmdsFirstErr(cmds) } func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error { - if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds); err != nil { + if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds, "MULTI"); err != nil { return err } return cmdsFirstErr(cmds) @@ -611,13 +1128,27 @@ func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error) func (c *baseClient) generalProcessPipeline( - ctx context.Context, cmds []Cmder, p pipelineProcessor, + ctx context.Context, cmds []Cmder, p pipelineProcessor, operationName string, ) error { + // Only call time.Now() if pipeline operation duration callback is set to avoid overhead + var operationStart time.Time + pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback() + if pipelineOpDurationCallback != nil { + operationStart = time.Now() + } + var lastConn *pool.Conn + totalAttempts := 0 + var lastErr error for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { + totalAttempts++ if attempt > 0 { if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { setCmdsErr(cmds, err) + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, err, lastConn, c.opt.DB) + } return err } } @@ -625,20 +1156,59 @@ func (c *baseClient) generalProcessPipeline( // Enable retries by default to retry dial errors returned by withConn. canRetry := true lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + lastConn = cn + // Process any pending push notifications before executing the pipeline + if err := c.processPushNotifications(ctx, cn); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before processing pipeline: %v", err) + } var err error canRetry, err = p(ctx, cn, cmds) return err }) - if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) { + // Don't retry if any command in the pipeline explicitly disables retries + // (e.g., RawWriteToCmd which writes directly to an io.Writer and cannot + // undo partial writes on retry) + if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) || cmdsContainNoRetry(cmds) { + // The error should be set here only when failing to obtain the conn. + if !isRedisError(lastErr) { + setCmdsErr(cmds, lastErr) + } + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB) + } + + if lastErr != nil { + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorType, statusCode, isInternal := classifyCommandError(lastErr) + errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1) + } + } return lastErr } } + + if pipelineOpDurationCallback != nil { + operationDuration := time.Since(operationStart) + pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB) + } + + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorType, statusCode, isInternal := classifyCommandError(lastErr) + errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1) + } + return lastErr } func (c *baseClient) pipelineProcessCmds( ctx context.Context, cn *pool.Conn, cmds []Cmder, ) (bool, error) { + // Process any pending push notifications before executing the pipeline + if err := c.processPushNotifications(ctx, cn); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before writing pipeline: %v", err) + } + if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmds(wr, cmds) }); err != nil { @@ -647,7 +1217,8 @@ func (c *baseClient) pipelineProcessCmds( } if err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error { - return pipelineReadCmds(rd, cmds) + // read all replies + return c.pipelineReadCmds(ctx, cn, rd, cmds) }); err != nil { return true, err } @@ -655,8 +1226,12 @@ func (c *baseClient) pipelineProcessCmds( return false, nil } -func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error { +func (c *baseClient) pipelineReadCmds(ctx context.Context, cn *pool.Conn, rd *proto.Reader, cmds []Cmder) error { for i, cmd := range cmds { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } err := cmd.readReply(rd) cmd.SetErr(err) if err != nil && !isRedisError(err) { @@ -671,6 +1246,11 @@ func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error { func (c *baseClient) txPipelineProcessCmds( ctx context.Context, cn *pool.Conn, cmds []Cmder, ) (bool, error) { + // Process any pending push notifications before executing the transaction pipeline + if err := c.processPushNotifications(ctx, cn); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before transaction: %v", err) + } + if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmds(wr, cmds) }); err != nil { @@ -683,12 +1263,13 @@ func (c *baseClient) txPipelineProcessCmds( // Trim multi and exec. trimmedCmds := cmds[1 : len(cmds)-1] - if err := txPipelineReadQueued(rd, statusCmd, trimmedCmds); err != nil { + if err := c.txPipelineReadQueued(ctx, cn, rd, statusCmd, trimmedCmds); err != nil { setCmdsErr(cmds, err) return err } - return pipelineReadCmds(rd, trimmedCmds) + // Read replies. + return c.pipelineReadCmds(ctx, cn, rd, trimmedCmds) }); err != nil { return false, err } @@ -696,19 +1277,36 @@ func (c *baseClient) txPipelineProcessCmds( return false, nil } -func txPipelineReadQueued(rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error { +// txPipelineReadQueued reads queued replies from the Redis server. +// It returns an error if the server returns an error or if the number of replies does not match the number of commands. +func (c *baseClient) txPipelineReadQueued(ctx context.Context, cn *pool.Conn, rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } // Parse +OK. if err := statusCmd.readReply(rd); err != nil { return err } // Parse +QUEUED. - for range cmds { - if err := statusCmd.readReply(rd); err != nil && !isRedisError(err) { - return err + for _, cmd := range cmds { + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } + if err := statusCmd.readReply(rd); err != nil { + cmd.SetErr(err) + if !isRedisError(err) { + return err + } } } + // To be sure there are no buffered push notifications, we process them before reading the reply + if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil { + internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err) + } // Parse number of replies. line, err := rd.ReadLine() if err != nil { @@ -738,19 +1336,76 @@ type Client struct { } // NewClient returns a client to the Redis Server specified by Options. +// Passing nil Options will cause a panic. func NewClient(opt *Options) *Client { if opt == nil { panic("redis: NewClient nil options") } + // clone to not share options with the caller + opt = opt.clone() opt.init() + // Push notifications are always enabled for RESP3 (cannot be disabled) + c := Client{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } c.init() - c.connPool = newConnPool(opt, c.dialHook) + + // Initialize push notification processor using shared helper + // Use void processor for RESP2 connections (push notifications not available) + c.pushProcessor = initializePushProcessor(opt) + // set opt push processor for child clients + c.opt.PushNotificationProcessor = c.pushProcessor + + // Generate unique pool names for metrics + uniqueID := generateUniqueID() + mainPoolName := opt.Addr + "_" + uniqueID + pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub" + + // Create connection pools + var err error + c.connPool, err = newConnPool(opt, c.dialHook, mainPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create connection pool: %w", err)) + } + c.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err)) + } + + if opt.StreamingCredentialsProvider != nil { + c.streamingCredentialsManager = streaming.NewManager(c.connPool, c.opt.PoolTimeout) + c.connPool.AddPoolHook(c.streamingCredentialsManager.PoolHook()) + } + + // Initialize maintnotifications first if enabled and protocol is RESP3 + if opt.MaintNotificationsConfig != nil && opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled && opt.Protocol == 3 { + err := c.enableMaintNotificationsUpgrades() + if err != nil { + internal.Logger.Printf(context.Background(), "failed to initialize maintnotifications: %v", err) + if opt.MaintNotificationsConfig.Mode == maintnotifications.ModeEnabled { + /* + Design decision: panic here to fail fast if maintnotifications cannot be enabled when explicitly requested. + We choose to panic instead of returning an error to avoid breaking the existing client API, which does not expect + an error from NewClient. This ensures that misconfiguration or critical initialization failures are surfaced + immediately, rather than allowing the client to continue in a partially initialized or inconsistent state. + Clients relying on maintnotifications should be aware that initialization errors will cause a panic, and should + handle this accordingly (e.g., via recover or by validating configuration before calling NewClient). + This approach is only used when MaintNotificationsConfig.Mode is MaintNotificationsEnabled, indicating that maintnotifications + upgrades are required for correct operation. In other modes, initialization failures are logged but do not panic. + */ + panic(fmt.Errorf("failed to enable maintnotifications: %w", err)) + } + } + } + + // Register pools with OTel recorder if it supports pool registration + // This allows async gauge metrics to pull stats from pools periodically + otel.RegisterPools(c.connPool, c.pubSubPool, opt.Addr) return &c } @@ -776,29 +1431,73 @@ func (c *Client) Conn() *Conn { return newConn(c.opt, pool.NewStickyConnPool(c.connPool), &c.hooksMixin) } -// Do create a Cmd from the args and processes the cmd. -func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *Client) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) return err } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *Options that were used to create the client. +// Any alteration of the returned *Options may result in undefined behaviour. func (c *Client) Options() *Options { return c.opt } +// NodeAddress returns the address of the Redis node as reported by the server. +// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation +// (e.g., loopback replacement). For standalone clients, this defaults to Addr. +// +// This is useful for matching the source field in maintenance notifications +// (e.g. SMIGRATED). +func (c *Client) NodeAddress() string { + return c.opt.NodeAddress +} + +// GetMaintNotificationsManager returns the maintnotifications manager instance for monitoring and control. +// Returns nil if maintnotifications are not enabled. +func (c *Client) GetMaintNotificationsManager() *maintnotifications.Manager { + c.maintNotificationsManagerLock.RLock() + defer c.maintNotificationsManagerLock.RUnlock() + return c.maintNotificationsManager +} + +// initializePushProcessor initializes the push notification processor for any client type. +// This is a shared helper to avoid duplication across NewClient, NewFailoverClient, and NewSentinelClient. +func initializePushProcessor(opt *Options) push.NotificationProcessor { + // Always use custom processor if provided + if opt.PushNotificationProcessor != nil { + return opt.PushNotificationProcessor + } + + // Push notifications are always enabled for RESP3, disabled for RESP2 + if opt.Protocol == 3 { + // Create default processor for RESP3 connections + return NewPushNotificationProcessor() + } + + // Create void processor for RESP2 connections (push notifications not available) + return NewVoidPushNotificationProcessor() +} + +// RegisterPushNotificationHandler registers a handler for a specific push notification name. +// Returns an error if a handler is already registered for this push notification name. +// If protected is true, the handler cannot be unregistered. +func (c *Client) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error { + return c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected) +} + +// GetPushNotificationHandler returns the handler for a specific push notification name. +// Returns nil if no handler is registered for the given name. +func (c *Client) GetPushNotificationHandler(pushNotificationName string) push.NotificationHandler { + return c.pushProcessor.GetHandler(pushNotificationName) +} + type PoolStats pool.Stats // PoolStats returns connection pool stats. func (c *Client) PoolStats() *PoolStats { stats := c.connPool.Stats() + stats.PubSubStats = *(c.pubSubPool.Stats()) return (*PoolStats)(stats) } @@ -833,13 +1532,31 @@ func (c *Client) TxPipeline() Pipeliner { func (c *Client) pubSub() *PubSub { pubsub := &PubSub{ opt: c.opt, - - newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) { - return c.newConn(ctx) + newConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) { + cn, err := c.pubSubPool.NewConn(ctx, c.opt.Network, addr, channels) + if err != nil { + return nil, err + } + // will return nil if already initialized + err = c.initConn(ctx, cn) + if err != nil { + _ = cn.Close() + return nil, err + } + // Track connection in PubSubPool + c.pubSubPool.TrackConn(cn) + return cn, nil }, - closeConn: c.connPool.CloseConn, + closeConn: func(cn *pool.Conn) error { + // Untrack connection from PubSubPool + c.pubSubPool.UntrackConn(cn) + _ = cn.Close() + return nil + }, + pushProcessor: c.pushProcessor, } pubsub.init() + return pubsub } @@ -916,6 +1633,7 @@ func newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn baseClient: baseClient{ opt: opt, connPool: connPool, + onClose: &onCloseHooks{}, }, } @@ -923,6 +1641,10 @@ func newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn c.hooksMixin = parentHooks.clone() } + // Initialize push notification processor using shared helper + // Use void processor for RESP2 connections (push notifications not available) + c.pushProcessor = initializePushProcessor(opt) + c.cmdable = c.Process c.statefulCmdable = c.Process c.initHooks(hooks{ @@ -941,6 +1663,13 @@ func (c *Conn) Process(ctx context.Context, cmd Cmder) error { return err } +// RegisterPushNotificationHandler registers a handler for a specific push notification name. +// Returns an error if a handler is already registered for this push notification name. +// If protected is true, the handler cannot be unregistered. +func (c *Conn) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error { + return c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected) +} + func (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { return c.Pipeline().Pipelined(ctx, fn) } @@ -968,3 +1697,78 @@ func (c *Conn) TxPipeline() Pipeliner { pipe.init() return &pipe } + +// processPushNotifications processes all pending push notifications on a connection +// This ensures that cluster topology changes are handled immediately before the connection is used +// This method should be called by the client before using WithReader for command execution +// +// Performance optimization: Skip the expensive MaybeHasData() syscall if a health check +// was performed recently (within 5 seconds). The health check already verified the connection +// is healthy and checked for unexpected data (push notifications). +func (c *baseClient) processPushNotifications(ctx context.Context, cn *pool.Conn) error { + // Only process push notifications for RESP3 connections with a processor + if c.opt.Protocol != 3 || c.pushProcessor == nil { + return nil + } + + // Performance optimization: Skip MaybeHasData() syscall if health check was recent + // If the connection was health-checked within the last 5 seconds, we can skip the + // expensive syscall since the health check already verified no unexpected data. + // This is safe because: + // 0. lastHealthCheckNs is set in pool/conn.go:putConn() after a successful health check + // 1. Health check (connCheck) uses the same syscall (Recvfrom with MSG_PEEK) + // 2. If push notifications arrived, they would have been detected by health check + // 3. 5 seconds is short enough that connection state is still fresh + // 4. Push notifications will be processed by the next WithReader call + // used it is set on getConn, so we should use another timer (lastPutAt?) + lastHealthCheckNs := cn.LastPutAtNs() + if lastHealthCheckNs > 0 { + // Use pool's cached time to avoid expensive time.Now() syscall + nowNs := pool.GetCachedTimeNs() + if nowNs-lastHealthCheckNs < int64(5*time.Second) { + // Recent health check confirmed no unexpected data, skip the syscall + return nil + } + } + + // Check if there is any data to read before processing + // This is an optimization on UNIX systems where MaybeHasData is a syscall + // On Windows, MaybeHasData always returns true, so this check is a no-op + if !cn.MaybeHasData() { + return nil + } + + // Use WithReader to access the reader and process push notifications + // This is critical for maintnotifications to work properly + // NOTE: almost no timeouts are set for this read, so it should not block + // longer than necessary, 10us should be plenty of time to read if there are any push notifications + // on the socket. + return cn.WithReader(ctx, 10*time.Microsecond, func(rd *proto.Reader) error { + // Create handler context with client, connection pool, and connection information + handlerCtx := c.pushNotificationHandlerContext(cn) + return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd) + }) +} + +// processPendingPushNotificationWithReader processes all pending push notifications on a connection +// This method should be called by the client in WithReader before reading the reply +func (c *baseClient) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error { + // if we have the reader, we don't need to check for data on the socket, we are waiting + // for either a reply or a push notification, so we can block until we get a reply or reach the timeout + if c.opt.Protocol != 3 || c.pushProcessor == nil { + return nil + } + + // Create handler context with client, connection pool, and connection information + handlerCtx := c.pushNotificationHandlerContext(cn) + return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd) +} + +// pushNotificationHandlerContext creates a handler context for push notification processing +func (c *baseClient) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext { + return push.NotificationHandlerContext{ + Client: c, + ConnPool: c.connPool, + Conn: cn, // Wrap in adapter for easier interface access + } +} diff --git a/vendor/github.com/redis/go-redis/v9/ring.go b/vendor/github.com/redis/go-redis/v9/ring.go index 8a004b8c0e7..b60d3eab533 100644 --- a/vendor/github.com/redis/go-redis/v9/ring.go +++ b/vendor/github.com/redis/go-redis/v9/ring.go @@ -5,39 +5,36 @@ import ( "crypto/tls" "errors" "fmt" + "math/rand" "net" "strconv" "sync" "sync/atomic" "time" - "github.com/cespare/xxhash/v2" - "github.com/dgryski/go-rendezvous" //nolint - + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" "github.com/redis/go-redis/v9/internal/pool" - "github.com/redis/go-redis/v9/internal/rand" + "github.com/redis/go-redis/v9/internal/proto" ) var errRingShardsDown = errors.New("redis: all ring shards are down") +// defaultHeartbeatFn is the default function used to check the shard liveness +var defaultHeartbeatFn = func(ctx context.Context, client *Client) bool { + err := client.Ping(ctx).Err() + return err == nil || err == pool.ErrPoolTimeout +} + //------------------------------------------------------------------------------ type ConsistentHash interface { Get(string) string } -type rendezvousWrapper struct { - *rendezvous.Rendezvous -} - -func (w rendezvousWrapper) Get(key string) string { - return w.Lookup(key) -} - func newRendezvous(shards []string) ConsistentHash { - return rendezvousWrapper{rendezvous.New(shards, xxhash.Sum64String)} + return hashtag.NewRendezvousHash(shards) } //------------------------------------------------------------------------------ @@ -54,10 +51,14 @@ type RingOptions struct { // ClientName will execute the `CLIENT SETNAME ClientName` command for each conn. ClientName string - // Frequency of PING commands sent to check shards availability. + // Frequency of executing HeartbeatFn to check shards availability. // Shard is considered down after 3 subsequent failed checks. HeartbeatFrequency time.Duration + // A function used to check the shard liveness + // if not set, defaults to defaultHeartbeatFn + HeartbeatFn func(ctx context.Context, client *Client) bool + // NewConsistentHash returns a consistent hash that is used // to distribute keys across the shards. // @@ -73,13 +74,45 @@ type RingOptions struct { Protocol int Username string Password string - DB int + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + DB int MaxRetries int MinRetryBackoff time.Duration MaxRetryBackoff time.Duration - DialTimeout time.Duration + DialTimeout time.Duration + + // DialerRetries is the maximum number of retry attempts when dialing fails. + // + // default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // + // default: 100 milliseconds + DialerRetryTimeout time.Duration + + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool @@ -87,13 +120,28 @@ type RingOptions struct { // PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO). PoolFIFO bool - PoolSize int - PoolTimeout time.Duration - MinIdleConns int - MaxIdleConns int - MaxActiveConns int - ConnMaxIdleTime time.Duration - ConnMaxLifetime time.Duration + PoolSize int + PoolTimeout time.Duration + MinIdleConns int + MaxIdleConns int + MaxActiveConns int + ConnMaxIdleTime time.Duration + ConnMaxLifetime time.Duration + ConnMaxLifetimeJitter time.Duration + + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + WriteBufferSize int TLSConfig *tls.Config Limiter Limiter @@ -110,7 +158,11 @@ type RingOptions struct { // default: false DisableIdentity bool IdentitySuffix string - UnstableResp3 bool + + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. + UnstableResp3 bool } func (opt *RingOptions) init() { @@ -124,6 +176,10 @@ func (opt *RingOptions) init() { opt.HeartbeatFrequency = 500 * time.Millisecond } + if opt.HeartbeatFn == nil { + opt.HeartbeatFn = defaultHeartbeatFn + } + if opt.NewConsistentHash == nil { opt.NewConsistentHash = newRendezvous } @@ -146,6 +202,13 @@ func (opt *RingOptions) init() { case 0: opt.MaxRetryBackoff = 512 * time.Millisecond } + + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } } func (opt *RingOptions) clientOptions() *Options { @@ -154,26 +217,35 @@ func (opt *RingOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, - DB: opt.DB, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, + DB: opt.DB, MaxRetries: -1, DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, ContextTimeoutEnabled: opt.ContextTimeoutEnabled, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + MaxActiveConns: opt.MaxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, TLSConfig: opt.TLSConfig, Limiter: opt.Limiter, @@ -405,7 +477,12 @@ func (c *ringSharding) GetByName(shardName string) (*ringShard, error) { c.mu.RLock() defer c.mu.RUnlock() - return c.shards.m[shardName], nil + shard, ok := c.shards.m[shardName] + if !ok { + return nil, errors.New("redis: the shard is not in the ring") + } + + return shard, nil } func (c *ringSharding) Random() (*ringShard, error) { @@ -424,8 +501,7 @@ func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) { // note: `c.List()` return a shadow copy of `[]*ringShard`. for _, shard := range c.List() { - err := shard.Client.Ping(ctx).Err() - isUp := err == nil || err == pool.ErrPoolTimeout + isUp := c.opt.HeartbeatFn(ctx, shard.Client) if shard.Vote(isUp) { internal.Logger.Printf(ctx, "ring shard state changed: %s", shard) rebalance = true @@ -522,6 +598,8 @@ type Ring struct { heartbeatCancelFn context.CancelFunc } +// NewRing returns a Redis Ring client to the Redis Server specified by RingOptions. +// Passing nil RingOptions will cause a panic. func NewRing(opt *RingOptions) *Ring { if opt == nil { panic("redis: NewRing nil options") @@ -558,20 +636,14 @@ func (c *Ring) SetAddrs(addrs map[string]string) { c.sharding.SetAddrs(addrs) } -// Do create a Cmd from the args and processes the cmd. -func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *Ring) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) return err } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *RingOptions that were used to create the client. +// Any alteration of the returned *RingOptions may result in undefined behaviour. func (c *Ring) Options() *RingOptions { return c.opt } @@ -703,7 +775,10 @@ func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) { } func (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) { - pos := cmdFirstKeyPos(cmd) + // TODO: populate cmdsInfoCache lazily (via cmdsInfoCache.Get) so that + // the warm-cache branch in cmdFirstKeyPosWithInfo is reachable for Ring, + // mirroring how ClusterClient.cmdInfo works. For now pass nil + pos := cmdFirstKeyPosWithInfo(cmd, nil) if pos == 0 { return c.sharding.Random() } @@ -726,7 +801,7 @@ func (c *Ring) process(ctx context.Context, cmd Cmder) error { } lastErr = shard.Client.Process(ctx, cmd) - if lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) { + if lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) || cmd.NoRetry() { return lastErr } } @@ -771,7 +846,7 @@ func (c *Ring) generalProcessPipeline( cmdsMap := make(map[string][]Cmder) for _, cmd := range cmds { - hash := cmd.stringArg(cmdFirstKeyPos(cmd)) + hash := cmd.stringArg(cmdFirstKeyPosWithInfo(cmd, nil)) if hash != "" { hash = c.sharding.Hash(hash) } @@ -779,6 +854,8 @@ func (c *Ring) generalProcessPipeline( } var wg sync.WaitGroup + errs := make(chan error, len(cmdsMap)) + for hash, cmds := range cmdsMap { wg.Add(1) go func(hash string, cmds []Cmder) { @@ -791,16 +868,24 @@ func (c *Ring) generalProcessPipeline( return } + hook := shard.Client.processPipelineHook if tx { cmds = wrapMultiExec(ctx, cmds) - _ = shard.Client.processTxPipelineHook(ctx, cmds) - } else { - _ = shard.Client.processPipelineHook(ctx, cmds) + hook = shard.Client.processTxPipelineHook + } + + if err = hook(ctx, cmds); err != nil { + errs <- err } }(hash, cmds) } wg.Wait() + close(errs) + + if err := <-errs; err != nil { + return err + } return cmdsFirstErr(cmds) } diff --git a/vendor/github.com/redis/go-redis/v9/script.go b/vendor/github.com/redis/go-redis/v9/script.go index 626ab03bb56..92d508f9a80 100644 --- a/vendor/github.com/redis/go-redis/v9/script.go +++ b/vendor/github.com/redis/go-redis/v9/script.go @@ -4,7 +4,9 @@ import ( "context" "crypto/sha1" "encoding/hex" + "errors" "io" + "sync" ) type Scripter interface { @@ -23,28 +25,69 @@ var ( ) type Script struct { - src, hash string + src string + mu sync.RWMutex + hash string + serverSHA bool // if true: do not compute SHA-1 in Go; load digest from Redis (SCRIPT LOAD) } func NewScript(src string) *Script { h := sha1.New() _, _ = io.WriteString(h, src) + + return &Script{ + src: src, + hash: hex.EncodeToString(h.Sum(nil)), + serverSHA: false, + } +} + +// NewScriptServerSHA creates a Script that avoids computing SHA-1 in Go. +// The digest is obtained from Redis via SCRIPT LOAD (server-side hashing), +// then EVALSHA/EVALSHA_RO is used. +func NewScriptServerSHA(src string) *Script { return &Script{ - src: src, - hash: hex.EncodeToString(h.Sum(nil)), + src: src, + serverSHA: true, } } func (s *Script) Hash() string { + s.mu.RLock() + defer s.mu.RUnlock() return s.hash } func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd { - return c.ScriptLoad(ctx, s.src) + cmd := c.ScriptLoad(ctx, s.src) + if err := cmd.Err(); err == nil { + s.mu.Lock() + s.hash = cmd.Val() + s.mu.Unlock() + } + return cmd } func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd { - return c.ScriptExists(ctx, s.hash) + s.mu.RLock() + hash := s.hash + serverSHA := s.serverSHA + s.mu.RUnlock() + if hash == "" && serverSHA { + // For server-side scripts, obtain digest from Redis first. + // If hash is empty, it means SCRIPT LOAD was not called yet, so we check existence of empty hash which will return false. + // This avoids unnecessary SCRIPT LOAD just to check existence. + if err := s.ensureHash(ctx, c); err != nil { + return c.ScriptExists(ctx, "") + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + } + if hash == "" { + return c.ScriptExists(ctx, "") + } + return c.ScriptExists(ctx, hash) } func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { @@ -55,19 +98,101 @@ func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ... return c.EvalRO(ctx, s.src, keys, args...) } +// ensureHash ensures that s.hash is populated by using SCRIPT LOAD. +// It never calls SHA-1 in Go; Redis computes and returns the digest. +func (s *Script) ensureHash(ctx context.Context, c Scripter) error { + // Fast path: read lock, return if hash is already set. + s.mu.RLock() + if s.hash != "" { + s.mu.RUnlock() + return nil + } + s.mu.RUnlock() + + // Slow path: acquire write lock and load. + s.mu.Lock() + if s.hash != "" { + s.mu.Unlock() + return nil + } + cmd := c.ScriptLoad(ctx, s.src) + if err := cmd.Err(); err != nil { + s.mu.Unlock() + return err + } + s.hash = cmd.Val() + s.mu.Unlock() + return nil +} + func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { - return c.EvalSha(ctx, s.hash, keys, args...) + // Default behavior: use client-side SHA-1 computed in NewScript. + if !s.serverSHA { + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + return c.EvalSha(ctx, hash, keys, args...) + } + + // Server-side SHA via SCRIPT LOAD + EVALSHA. + if err := s.ensureHash(ctx, c); err != nil { + return s.Eval(ctx, c, keys, args...) + } + + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + + r := c.EvalSha(ctx, hash, keys, args...) + if HasErrorPrefix(r.Err(), "NOSCRIPT") { + // Script cache was flushed; reload and retry once. + if err := s.ensureHash(ctx, c); err != nil { + return s.Eval(ctx, c, keys, args...) + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + return c.EvalSha(ctx, hash, keys, args...) + } + + return r } func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { - return c.EvalShaRO(ctx, s.hash, keys, args...) + if !s.serverSHA { + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + return c.EvalShaRO(ctx, hash, keys, args...) + } + + if err := s.ensureHash(ctx, c); err != nil { + return s.EvalRO(ctx, c, keys, args...) + } + + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + + r := c.EvalShaRO(ctx, hash, keys, args...) + if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if err := s.ensureHash(ctx, c); err != nil { + return s.EvalRO(ctx, c, keys, args...) + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + return c.EvalShaRO(ctx, hash, keys, args...) + } + + return r } // Run optimistically uses EVALSHA to run the script. If script does not exist // it is retried using EVAL. func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { r := s.EvalSha(ctx, c, keys, args...) - if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if errors.Is(r.Err(), ErrNoScript) { return s.Eval(ctx, c, keys, args...) } return r @@ -77,7 +202,7 @@ func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...int // it is retried using EVAL_RO. func (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { r := s.EvalShaRO(ctx, c, keys, args...) - if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if errors.Is(r.Err(), ErrNoScript) { return s.EvalRO(ctx, c, keys, args...) } return r diff --git a/vendor/github.com/redis/go-redis/v9/scripting_commands.go b/vendor/github.com/redis/go-redis/v9/scripting_commands.go index af9c3397bfe..3310b9d0ae0 100644 --- a/vendor/github.com/redis/go-redis/v9/scripting_commands.go +++ b/vendor/github.com/redis/go-redis/v9/scripting_commands.go @@ -60,6 +60,11 @@ func (c cmdable) eval(ctx context.Context, name, payload string, keys []string, cmd.SetFirstKeyPos(3) } _ = c(ctx, cmd) + if err := cmd.Err(); err != nil { + if HasErrorPrefix(err, "NOSCRIPT") { + cmd.SetErr(ErrNoScript) + } + } return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/search_builders.go b/vendor/github.com/redis/go-redis/v9/search_builders.go new file mode 100644 index 00000000000..a6c6718c343 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/search_builders.go @@ -0,0 +1,858 @@ +package redis + +import ( + "context" + "fmt" +) + +// ---------------------- +// Search Module Builders +// ---------------------- + +// SearchBuilder provides a fluent API for FT.SEARCH +// (see original FTSearchOptions for all options). +// EXPERIMENTAL: this API is subject to change, use with caution. +type SearchBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSearchOptions +} + +// NewSearchBuilder creates a new SearchBuilder for FT.SEARCH commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSearchBuilder(ctx context.Context, index, query string) *SearchBuilder { + b := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}} + return b +} + +// WithScores includes WITHSCORES. +func (b *SearchBuilder) WithScores() *SearchBuilder { + b.options.WithScores = true + return b +} + +// NoContent includes NOCONTENT. +func (b *SearchBuilder) NoContent() *SearchBuilder { b.options.NoContent = true; return b } + +// Verbatim includes VERBATIM. +func (b *SearchBuilder) Verbatim() *SearchBuilder { b.options.Verbatim = true; return b } + +// NoStopWords includes NOSTOPWORDS. +func (b *SearchBuilder) NoStopWords() *SearchBuilder { b.options.NoStopWords = true; return b } + +// WithPayloads includes WITHPAYLOADS. +func (b *SearchBuilder) WithPayloads() *SearchBuilder { + b.options.WithPayloads = true + return b +} + +// WithSortKeys includes WITHSORTKEYS. +func (b *SearchBuilder) WithSortKeys() *SearchBuilder { + b.options.WithSortKeys = true + return b +} + +// Filter adds a FILTER clause: FILTER . +func (b *SearchBuilder) Filter(field string, min, max interface{}) *SearchBuilder { + b.options.Filters = append(b.options.Filters, FTSearchFilter{ + FieldName: field, + Min: min, + Max: max, + }) + return b +} + +// GeoFilter adds a GEOFILTER clause: GEOFILTER . +func (b *SearchBuilder) GeoFilter(field string, lon, lat, radius float64, unit string) *SearchBuilder { + b.options.GeoFilter = append(b.options.GeoFilter, FTSearchGeoFilter{ + FieldName: field, + Longitude: lon, + Latitude: lat, + Radius: radius, + Unit: unit, + }) + return b +} + +// InKeys restricts the search to the given keys. +func (b *SearchBuilder) InKeys(keys ...interface{}) *SearchBuilder { + b.options.InKeys = append(b.options.InKeys, keys...) + return b +} + +// InFields restricts the search to the given fields. +func (b *SearchBuilder) InFields(fields ...interface{}) *SearchBuilder { + b.options.InFields = append(b.options.InFields, fields...) + return b +} + +// ReturnFields adds simple RETURN ... +func (b *SearchBuilder) ReturnFields(fields ...string) *SearchBuilder { + for _, f := range fields { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: f}) + } + return b +} + +// ReturnAs adds RETURN AS . +func (b *SearchBuilder) ReturnAs(field, alias string) *SearchBuilder { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: field, As: alias}) + return b +} + +// Slop adds SLOP . +func (b *SearchBuilder) Slop(slop int) *SearchBuilder { + b.options.Slop = slop + return b +} + +// Timeout adds TIMEOUT . +func (b *SearchBuilder) Timeout(timeout int) *SearchBuilder { + b.options.Timeout = timeout + return b +} + +// InOrder includes INORDER. +func (b *SearchBuilder) InOrder() *SearchBuilder { + b.options.InOrder = true + return b +} + +// Language sets LANGUAGE . +func (b *SearchBuilder) Language(lang string) *SearchBuilder { + b.options.Language = lang + return b +} + +// Expander sets EXPANDER . +func (b *SearchBuilder) Expander(expander string) *SearchBuilder { + b.options.Expander = expander + return b +} + +// Scorer sets SCORER . +func (b *SearchBuilder) Scorer(scorer string) *SearchBuilder { + b.options.Scorer = scorer + return b +} + +// ExplainScore includes EXPLAINSCORE. +func (b *SearchBuilder) ExplainScore() *SearchBuilder { + b.options.ExplainScore = true + return b +} + +// Payload sets PAYLOAD . +func (b *SearchBuilder) Payload(payload string) *SearchBuilder { + b.options.Payload = payload + return b +} + +// SortBy adds SORTBY ASC|DESC. +func (b *SearchBuilder) SortBy(field string, asc bool) *SearchBuilder { + b.options.SortBy = append(b.options.SortBy, FTSearchSortBy{ + FieldName: field, + Asc: asc, + Desc: !asc, + }) + return b +} + +// WithSortByCount includes WITHCOUNT (when used with SortBy). +func (b *SearchBuilder) WithSortByCount() *SearchBuilder { + b.options.SortByWithCount = true + return b +} + +// Param adds a single PARAMS . +func (b *SearchBuilder) Param(key string, value interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, 1) + } + b.options.Params[key] = value + return b +} + +// ParamsMap adds multiple PARAMS at once. +func (b *SearchBuilder) ParamsMap(p map[string]interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *SearchBuilder) Dialect(version int) *SearchBuilder { + b.options.DialectVersion = version + return b +} + +// Limit sets OFFSET and COUNT. CountOnly uses LIMIT 0 0. +func (b *SearchBuilder) Limit(offset, count int) *SearchBuilder { + b.options.LimitOffset = offset + b.options.Limit = count + return b +} +func (b *SearchBuilder) CountOnly() *SearchBuilder { b.options.CountOnly = true; return b } + +// Run executes FT.SEARCH and returns a typed result. +func (b *SearchBuilder) Run() (FTSearchResult, error) { + cmd := b.c.FTSearchWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// AggregateBuilder for FT.AGGREGATE +// ---------------------- + +type AggregateBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTAggregateOptions + err error +} + +// NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) *AggregateBuilder { + return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} +} + +// setErr records the first error produced while building the pipeline. +// Subsequent errors are ignored; the first error is returned from Run. +func (b *AggregateBuilder) setErr(err error) { + if b.err == nil { + b.err = err + } +} + +// Verbatim includes VERBATIM. +func (b *AggregateBuilder) Verbatim() *AggregateBuilder { b.options.Verbatim = true; return b } + +// AddScores includes ADDSCORES. +func (b *AggregateBuilder) AddScores() *AggregateBuilder { b.options.AddScores = true; return b } + +// Scorer sets SCORER . +func (b *AggregateBuilder) Scorer(s string) *AggregateBuilder { + b.options.Scorer = s + return b +} + +// LoadAll includes LOAD * (mutually exclusive with Load). +func (b *AggregateBuilder) LoadAll() *AggregateBuilder { + b.options.LoadAll = true + return b +} + +// Load adds a LOAD [AS alias] step. +// You can call it multiple times; each call becomes a separate LOAD clause +// at its position in the pipeline. +func (b *AggregateBuilder) Load(field string, alias ...string) *AggregateBuilder { + l := &FTAggregateLoad{Field: field} + if len(alias) > 0 { + l.As = alias[0] + } + b.options.Steps = append(b.options.Steps, FTAggregateStep{Load: l}) + return b +} + +// Timeout sets TIMEOUT . +func (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder { + b.options.Timeout = ms + return b +} + +// Apply adds an APPLY [AS alias] step. +func (b *AggregateBuilder) Apply(field string, alias ...string) *AggregateBuilder { + a := &FTAggregateApply{Field: field} + if len(alias) > 0 { + a.As = alias[0] + } + b.options.Steps = append(b.options.Steps, FTAggregateStep{Apply: a}) + return b +} + +// GroupBy adds a new GROUPBY step. +func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder { + b.options.Steps = append(b.options.Steps, FTAggregateStep{ + GroupBy: &FTAggregateGroupBy{Fields: fields}, + }) + return b +} + +// Reduce adds a REDUCE [<#args> ] clause to the last step, +// which must be a GROUPBY. If it is not, Run will return an error. +func (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder { + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].GroupBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: Reduce must follow a GroupBy step")) + return b + } + g := b.options.Steps[n-1].GroupBy + g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args}) + return b +} + +// ReduceAs does the same but also sets an alias: REDUCE … AS . +// The last step must be a GROUPBY; otherwise Run will return an error. +func (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder { + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].GroupBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: ReduceAs must follow a GroupBy step")) + return b + } + g := b.options.Steps[n-1].GroupBy + g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args, As: alias}) + return b +} + +// SortBy adds SORTBY ASC|DESC. Consecutive SortBy calls (with no +// other step in between) are merged into a single SORTBY clause so fields +// act as tiebreakers. A SortBy call after a non-SortBy step starts a new +// SORTBY step. +// +// Note: this is a semantics change from earlier experimental versions of +// the builder, where SortBy always accumulated into a single SORTBY clause +// regardless of position in the pipeline. +func (b *AggregateBuilder) SortBy(field string, asc bool) *AggregateBuilder { + sb := FTAggregateSortBy{FieldName: field, Asc: asc, Desc: !asc} + if n := len(b.options.Steps); n > 0 && b.options.Steps[n-1].SortBy != nil { + b.options.Steps[n-1].SortBy.Fields = append(b.options.Steps[n-1].SortBy.Fields, sb) + return b + } + b.options.Steps = append(b.options.Steps, FTAggregateStep{ + SortBy: &FTAggregateSortByStep{Fields: []FTAggregateSortBy{sb}}, + }) + return b +} + +// SortByMax sets MAX on the last SORTBY step. The last step must be a +// SORTBY; otherwise Run will return an error. +func (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder { + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].SortBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: SortByMax must follow a SortBy step")) + return b + } + b.options.Steps[n-1].SortBy.Max = max + return b +} + +// Filter sets FILTER . +func (b *AggregateBuilder) Filter(expr string) *AggregateBuilder { + b.options.Filter = expr + return b +} + +// WithCursor enables WITHCURSOR [COUNT ] [MAXIDLE ]. +func (b *AggregateBuilder) WithCursor(count, maxIdle int) *AggregateBuilder { + b.options.WithCursor = true + if b.options.WithCursorOptions == nil { + b.options.WithCursorOptions = &FTAggregateWithCursor{} + } + b.options.WithCursorOptions.Count = count + b.options.WithCursorOptions.MaxIdle = maxIdle + return b +} + +// Params adds PARAMS pairs. +func (b *AggregateBuilder) Params(p map[string]interface{}) *AggregateBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *AggregateBuilder) Dialect(version int) *AggregateBuilder { + b.options.DialectVersion = version + return b +} + +// Run executes FT.AGGREGATE and returns a typed result. If the builder +// recorded a validation error while constructing the pipeline (for example, +// calling SortByMax when the last step is not a SortBy), that error is +// returned without issuing the command. +func (b *AggregateBuilder) Run() (*FTAggregateResult, error) { + if b.err != nil { + return nil, b.err + } + cmd := b.c.FTAggregateWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// CreateIndexBuilder for FT.CREATE +// ---------------------- +// CreateIndexBuilder is builder for FT.CREATE +// EXPERIMENTAL: this API is subject to change, use with caution. +type CreateIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTCreateOptions + schema []*FieldSchema +} + +// NewCreateIndexBuilder creates a new CreateIndexBuilder for FT.CREATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewCreateIndexBuilder(ctx context.Context, index string) *CreateIndexBuilder { + return &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}} +} + +// OnHash sets ON HASH. +func (b *CreateIndexBuilder) OnHash() *CreateIndexBuilder { b.options.OnHash = true; return b } + +// OnJSON sets ON JSON. +func (b *CreateIndexBuilder) OnJSON() *CreateIndexBuilder { b.options.OnJSON = true; return b } + +// Prefix sets PREFIX. +func (b *CreateIndexBuilder) Prefix(prefixes ...interface{}) *CreateIndexBuilder { + b.options.Prefix = prefixes + return b +} + +// Filter sets FILTER. +func (b *CreateIndexBuilder) Filter(filter string) *CreateIndexBuilder { + b.options.Filter = filter + return b +} + +// DefaultLanguage sets LANGUAGE. +func (b *CreateIndexBuilder) DefaultLanguage(lang string) *CreateIndexBuilder { + b.options.DefaultLanguage = lang + return b +} + +// LanguageField sets LANGUAGE_FIELD. +func (b *CreateIndexBuilder) LanguageField(field string) *CreateIndexBuilder { + b.options.LanguageField = field + return b +} + +// Score sets SCORE. +func (b *CreateIndexBuilder) Score(score float64) *CreateIndexBuilder { + b.options.Score = score + return b +} + +// ScoreField sets SCORE_FIELD. +func (b *CreateIndexBuilder) ScoreField(field string) *CreateIndexBuilder { + b.options.ScoreField = field + return b +} + +// PayloadField sets PAYLOAD_FIELD. +func (b *CreateIndexBuilder) PayloadField(field string) *CreateIndexBuilder { + b.options.PayloadField = field + return b +} + +// NoOffsets includes NOOFFSETS. +func (b *CreateIndexBuilder) NoOffsets() *CreateIndexBuilder { b.options.NoOffsets = true; return b } + +// Temporary sets TEMPORARY seconds. +func (b *CreateIndexBuilder) Temporary(sec int) *CreateIndexBuilder { + b.options.Temporary = sec + return b +} + +// NoHL includes NOHL. +func (b *CreateIndexBuilder) NoHL() *CreateIndexBuilder { b.options.NoHL = true; return b } + +// NoFields includes NOFIELDS. +func (b *CreateIndexBuilder) NoFields() *CreateIndexBuilder { b.options.NoFields = true; return b } + +// NoFreqs includes NOFREQS. +func (b *CreateIndexBuilder) NoFreqs() *CreateIndexBuilder { b.options.NoFreqs = true; return b } + +// StopWords sets STOPWORDS. +func (b *CreateIndexBuilder) StopWords(words ...interface{}) *CreateIndexBuilder { + b.options.StopWords = words + return b +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *CreateIndexBuilder) SkipInitialScan() *CreateIndexBuilder { + b.options.SkipInitialScan = true + return b +} + +// Schema adds a FieldSchema. +func (b *CreateIndexBuilder) Schema(field *FieldSchema) *CreateIndexBuilder { + b.schema = append(b.schema, field) + return b +} + +// Run executes FT.CREATE and returns the status. +func (b *CreateIndexBuilder) Run() (string, error) { + cmd := b.c.FTCreate(b.ctx, b.index, b.options, b.schema...) + return cmd.Result() +} + +// ---------------------- +// DropIndexBuilder for FT.DROPINDEX +// ---------------------- +// DropIndexBuilder is a builder for FT.DROPINDEX +// EXPERIMENTAL: this API is subject to change, use with caution. +type DropIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTDropIndexOptions +} + +// NewDropIndexBuilder creates a new DropIndexBuilder for FT.DROPINDEX commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewDropIndexBuilder(ctx context.Context, index string) *DropIndexBuilder { + return &DropIndexBuilder{c: c, ctx: ctx, index: index} +} + +// DeleteRuncs includes DD. +func (b *DropIndexBuilder) DeleteDocs() *DropIndexBuilder { b.options.DeleteDocs = true; return b } + +// Run executes FT.DROPINDEX. +func (b *DropIndexBuilder) Run() (string, error) { + cmd := b.c.FTDropIndexWithArgs(b.ctx, b.index, b.options) + return cmd.Result() +} + +// ---------------------- +// AliasBuilder for FT.ALIAS* commands +// ---------------------- +// AliasBuilder is builder for FT.ALIAS* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type AliasBuilder struct { + c *Client + ctx context.Context + alias string + index string + action string // add|del|update +} + +// NewAliasBuilder creates a new AliasBuilder for FT.ALIAS* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewAliasBuilder(ctx context.Context, alias string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias} +} + +// Action sets the action for the alias builder. +func (b *AliasBuilder) Action(action string) *AliasBuilder { + b.action = action + return b +} + +// Add sets the action to "add" and requires an index. +func (b *AliasBuilder) Add(index string) *AliasBuilder { + b.action = "add" + b.index = index + return b +} + +// Del sets the action to "del". +func (b *AliasBuilder) Del() *AliasBuilder { + b.action = "del" + return b +} + +// Update sets the action to "update" and requires an index. +func (b *AliasBuilder) Update(index string) *AliasBuilder { + b.action = "update" + b.index = index + return b +} + +// Run executes the configured alias command. +func (b *AliasBuilder) Run() (string, error) { + switch b.action { + case "add": + cmd := b.c.FTAliasAdd(b.ctx, b.index, b.alias) + return cmd.Result() + case "del": + cmd := b.c.FTAliasDel(b.ctx, b.alias) + return cmd.Result() + case "update": + cmd := b.c.FTAliasUpdate(b.ctx, b.index, b.alias) + return cmd.Result() + } + return "", nil +} + +// ---------------------- +// ExplainBuilder for FT.EXPLAIN +// ---------------------- +// ExplainBuilder is builder for FT.EXPLAIN +// EXPERIMENTAL: this API is subject to change, use with caution. +type ExplainBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTExplainOptions +} + +// NewExplainBuilder creates a new ExplainBuilder for FT.EXPLAIN commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewExplainBuilder(ctx context.Context, index, query string) *ExplainBuilder { + return &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}} +} + +// Dialect sets dialect for EXPLAINCLI. +func (b *ExplainBuilder) Dialect(d string) *ExplainBuilder { b.options.Dialect = d; return b } + +// Run executes FT.EXPLAIN and returns the plan. +func (b *ExplainBuilder) Run() (string, error) { + cmd := b.c.FTExplainWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// InfoBuilder for FT.INFO +// ---------------------- + +type FTInfoBuilder struct { + c *Client + ctx context.Context + index string +} + +// NewSearchInfoBuilder creates a new FTInfoBuilder for FT.INFO commands. +func (c *Client) NewSearchInfoBuilder(ctx context.Context, index string) *FTInfoBuilder { + return &FTInfoBuilder{c: c, ctx: ctx, index: index} +} + +// Run executes FT.INFO and returns detailed info. +func (b *FTInfoBuilder) Run() (FTInfoResult, error) { + cmd := b.c.FTInfo(b.ctx, b.index) + return cmd.Result() +} + +// ---------------------- +// SpellCheckBuilder for FT.SPELLCHECK +// ---------------------- +// SpellCheckBuilder is builder for FT.SPELLCHECK +// EXPERIMENTAL: this API is subject to change, use with caution. +type SpellCheckBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSpellCheckOptions +} + +// NewSpellCheckBuilder creates a new SpellCheckBuilder for FT.SPELLCHECK commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSpellCheckBuilder(ctx context.Context, index, query string) *SpellCheckBuilder { + return &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}} +} + +// Distance sets MAXDISTANCE. +func (b *SpellCheckBuilder) Distance(d int) *SpellCheckBuilder { b.options.Distance = d; return b } + +// Terms sets INCLUDE or EXCLUDE terms. +func (b *SpellCheckBuilder) Terms(include bool, dictionary string, terms ...interface{}) *SpellCheckBuilder { + if b.options.Terms == nil { + b.options.Terms = &FTSpellCheckTerms{} + } + if include { + b.options.Terms.Inclusion = "INCLUDE" + } else { + b.options.Terms.Inclusion = "EXCLUDE" + } + b.options.Terms.Dictionary = dictionary + b.options.Terms.Terms = terms + return b +} + +// Dialect sets dialect version. +func (b *SpellCheckBuilder) Dialect(d int) *SpellCheckBuilder { b.options.Dialect = d; return b } + +// Run executes FT.SPELLCHECK and returns suggestions. +func (b *SpellCheckBuilder) Run() ([]SpellCheckResult, error) { + cmd := b.c.FTSpellCheckWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// DictBuilder for FT.DICT* commands +// ---------------------- +// DictBuilder is builder for FT.DICT* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type DictBuilder struct { + c *Client + ctx context.Context + dict string + terms []interface{} + action string // add|del|dump +} + +// NewDictBuilder creates a new DictBuilder for FT.DICT* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewDictBuilder(ctx context.Context, dict string) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict} +} + +// Action sets the action for the dictionary builder. +func (b *DictBuilder) Action(action string) *DictBuilder { + b.action = action + return b +} + +// Add sets the action to "add" and requires terms. +func (b *DictBuilder) Add(terms ...interface{}) *DictBuilder { + b.action = "add" + b.terms = terms + return b +} + +// Del sets the action to "del" and requires terms. +func (b *DictBuilder) Del(terms ...interface{}) *DictBuilder { + b.action = "del" + b.terms = terms + return b +} + +// Dump sets the action to "dump". +func (b *DictBuilder) Dump() *DictBuilder { + b.action = "dump" + return b +} + +// Run executes the configured dictionary command. +func (b *DictBuilder) Run() (interface{}, error) { + switch b.action { + case "add": + cmd := b.c.FTDictAdd(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "del": + cmd := b.c.FTDictDel(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "dump": + cmd := b.c.FTDictDump(b.ctx, b.dict) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// TagValsBuilder for FT.TAGVALS +// ---------------------- +// TagValsBuilder is builder for FT.TAGVALS +// EXPERIMENTAL: this API is subject to change, use with caution. +type TagValsBuilder struct { + c *Client + ctx context.Context + index string + field string +} + +// NewTagValsBuilder creates a new TagValsBuilder for FT.TAGVALS commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewTagValsBuilder(ctx context.Context, index, field string) *TagValsBuilder { + return &TagValsBuilder{c: c, ctx: ctx, index: index, field: field} +} + +// Run executes FT.TAGVALS and returns tag values. +func (b *TagValsBuilder) Run() ([]string, error) { + cmd := b.c.FTTagVals(b.ctx, b.index, b.field) + return cmd.Result() +} + +// ---------------------- +// CursorBuilder for FT.CURSOR* +// ---------------------- +// CursorBuilder is builder for FT.CURSOR* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type CursorBuilder struct { + c *Client + ctx context.Context + index string + cursorId int64 + count int + action string // read|del +} + +// NewCursorBuilder creates a new CursorBuilder for FT.CURSOR* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewCursorBuilder(ctx context.Context, index string, cursorId int64) *CursorBuilder { + return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId} +} + +// Action sets the action for the cursor builder. +func (b *CursorBuilder) Action(action string) *CursorBuilder { + b.action = action + return b +} + +// Read sets the action to "read". +func (b *CursorBuilder) Read() *CursorBuilder { + b.action = "read" + return b +} + +// Del sets the action to "del". +func (b *CursorBuilder) Del() *CursorBuilder { + b.action = "del" + return b +} + +// Count for READ. +func (b *CursorBuilder) Count(count int) *CursorBuilder { b.count = count; return b } + +// Run executes the cursor command. +func (b *CursorBuilder) Run() (interface{}, error) { + switch b.action { + case "read": + cmd := b.c.FTCursorRead(b.ctx, b.index, int(b.cursorId), b.count) + return cmd.Result() + case "del": + cmd := b.c.FTCursorDel(b.ctx, b.index, int(b.cursorId)) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// SynUpdateBuilder for FT.SYNUPDATE +// ---------------------- +// SyncUpdateBuilder is builder for FT.SYNCUPDATE +// EXPERIMENTAL: this API is subject to change, use with caution. +type SynUpdateBuilder struct { + c *Client + ctx context.Context + index string + groupId interface{} + options *FTSynUpdateOptions + terms []interface{} +} + +// NewSynUpdateBuilder creates a new SynUpdateBuilder for FT.SYNUPDATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSynUpdateBuilder(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { + return &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}} +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *SynUpdateBuilder) SkipInitialScan() *SynUpdateBuilder { + b.options.SkipInitialScan = true + return b +} + +// Terms adds synonyms to the group. +func (b *SynUpdateBuilder) Terms(terms ...interface{}) *SynUpdateBuilder { b.terms = terms; return b } + +// Run executes FT.SYNUPDATE. +func (b *SynUpdateBuilder) Run() (string, error) { + cmd := b.c.FTSynUpdateWithArgs(b.ctx, b.index, b.groupId, b.options, b.terms) + return cmd.Result() +} diff --git a/vendor/github.com/redis/go-redis/v9/search_commands.go b/vendor/github.com/redis/go-redis/v9/search_commands.go index b31baaa7609..b13aa5bef11 100644 --- a/vendor/github.com/redis/go-redis/v9/search_commands.go +++ b/vendor/github.com/redis/go-redis/v9/search_commands.go @@ -3,7 +3,10 @@ package redis import ( "context" "fmt" + "maps" + "slices" "strconv" + "strings" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/proto" @@ -29,6 +32,8 @@ type SearchCmdable interface { FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd FTExplain(ctx context.Context, index string, query string) *StringCmd FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd + FTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd + FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd FTInfo(ctx context.Context, index string) *FTInfoCmd FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd @@ -80,8 +85,9 @@ type FieldSchema struct { } type FTVectorArgs struct { - FlatOptions *FTFlatOptions - HNSWOptions *FTHNSWOptions + FlatOptions *FTFlatOptions + HNSWOptions *FTHNSWOptions + VamanaOptions *FTVamanaOptions } type FTFlatOptions struct { @@ -103,6 +109,19 @@ type FTHNSWOptions struct { Epsilon float64 } +type FTVamanaOptions struct { + Type string + Dim int + DistanceMetric string + Compression string + ConstructionWindowSize int + GraphMaxDegree int + SearchWindowSize int + Epsilon float64 + TrainingThreshold int + ReduceDim int +} + type FTDropIndexOptions struct { DeleteDocs bool } @@ -240,22 +259,42 @@ type FTAggregateWithCursor struct { MaxIdle int } +// FTAggregateSortByStep represents a SORTBY operation with optional MAX. +// Used inside FTAggregateStep to place SORTBY at an arbitrary position in +// the aggregation pipeline. +type FTAggregateSortByStep struct { + Fields []FTAggregateSortBy + Max int // 0 means no MAX +} + +// FTAggregateStep represents a single operation in the aggregation pipeline. +// LOAD, APPLY, SORTBY and GROUPBY can all appear multiple times in any order. +// Exactly one of the fields should be set per step. +type FTAggregateStep struct { + Load *FTAggregateLoad + Apply *FTAggregateApply + GroupBy *FTAggregateGroupBy + SortBy *FTAggregateSortByStep +} + type FTAggregateOptions struct { - Verbatim bool - LoadAll bool - Load []FTAggregateLoad - Timeout int - GroupBy []FTAggregateGroupBy - SortBy []FTAggregateSortBy - SortByMax int + Verbatim bool + LoadAll bool + Timeout int // Scorer is used to set scoring function, if not set passed, a default will be used. // The default scorer depends on the Redis version: // - `BM25` for Redis >= 8 // - `TFIDF` for Redis < 8 Scorer string // AddScores is available in Redis CE 8 - AddScores bool - Apply []FTAggregateApply + AddScores bool + + // Steps is the ordered sequence of aggregation pipeline operations. + // It can contain LOAD, APPLY, GROUPBY and SORTBY in any order, multiple times. + // Steps cannot be combined with the deprecated Load, Apply, GroupBy, SortBy + // and SortByMax fields: doing so returns an error. + Steps []FTAggregateStep + LimitOffset int Limit int Filter string @@ -264,6 +303,17 @@ type FTAggregateOptions struct { Params map[string]interface{} // Dialect 1,3 and 4 are deprecated since redis 8.0 DialectVersion int + + // Deprecated: Use Steps instead. + Load []FTAggregateLoad + // Deprecated: Use Steps instead. + GroupBy []FTAggregateGroupBy + // Deprecated: Use Steps instead. + SortBy []FTAggregateSortBy + // Deprecated: Use Steps instead. + SortByMax int + // Deprecated: Use Steps instead. + Apply []FTAggregateApply } type FTSearchFilter struct { @@ -330,6 +380,100 @@ type FTSearchOptions struct { DialectVersion int } +// FTHybridCombineMethod represents the fusion method for combining search and vector results +type FTHybridCombineMethod string + +const ( + FTHybridCombineRRF FTHybridCombineMethod = "RRF" + FTHybridCombineLinear FTHybridCombineMethod = "LINEAR" + FTHybridCombineFunction FTHybridCombineMethod = "FUNCTION" +) + +// FTHybridSearchExpression represents a search expression in hybrid search +type FTHybridSearchExpression struct { + Query string + Scorer string + ScorerParams []interface{} + YieldScoreAs string +} + +type FTHybridVectorMethod = string + +const ( + KNN FTHybridCombineMethod = "KNN" + RANGE FTHybridCombineMethod = "RANGE" +) + +// FTHybridVectorExpression represents a vector expression in hybrid search +type FTHybridVectorExpression struct { + VectorField string + VectorData Vector + // VectorParamName optionally specifies the parameter name used to pass the + // vector data via the PARAMS mechanism. + // Vector data is always passed via PARAMS because inline vector blobs are no + // longer supported by Redis. When left empty, the library generates a unique + // parameter name automatically (e.g. "__vector_param_0") without mutating + // FTHybridOptions.Params and without colliding with any explicit names. + // The vector blob is passed as: VSIM @field $VectorParamName ... PARAMS ... VectorParamName + VectorParamName string + Method FTHybridVectorMethod + MethodParams []interface{} + Filter string + YieldScoreAs string +} + +// FTHybridCombineOptions represents options for result fusion +type FTHybridCombineOptions struct { + Method FTHybridCombineMethod + Count int + Window int // For RRF + Constant float64 // For RRF + Alpha float64 // For LINEAR + Beta float64 // For LINEAR + YieldScoreAs string +} + +// FTHybridGroupBy represents GROUP BY functionality +type FTHybridGroupBy struct { + Count int + Fields []string + ReduceFunc string + ReduceCount int + ReduceParams []interface{} +} + +// FTHybridApply represents APPLY functionality +type FTHybridApply struct { + Expression string + AsField string +} + +// FTHybridWithCursor represents cursor configuration for hybrid search +type FTHybridWithCursor struct { + Count int // Number of results to return per cursor read + MaxIdle int // Maximum idle time in milliseconds before cursor is automatically deleted +} + +// FTHybridOptions hold options that can be passed to the FT.HYBRID command +type FTHybridOptions struct { + CountExpressions int // Number of search/vector expressions + SearchExpressions []FTHybridSearchExpression // Multiple search expressions + VectorExpressions []FTHybridVectorExpression // Multiple vector expressions + Combine *FTHybridCombineOptions // Fusion step options + Load []string // Projected fields + GroupBy *FTHybridGroupBy // Aggregation grouping + Apply []FTHybridApply // Field transformations + SortBy []FTSearchSortBy // Reuse from FTSearch + Filter string // Post-filter expression + LimitOffset int // Result limiting + Limit int + Params map[string]interface{} // Parameter substitution + ExplainScore bool // Include score explanations + Timeout int // Runtime timeout + WithCursor bool // Enable cursor support for large result sets + WithCursorOptions *FTHybridWithCursor // Cursor configuration options +} + type FTSynDumpResult struct { Term string Synonyms []string @@ -340,9 +484,12 @@ type FTSynDumpCmd struct { val []FTSynDumpResult } +// FTAggregateResult represents the result of an aggregate operation +// NOTE: For RESP3 Total is not reliable (before Redis 8.8) type FTAggregateResult struct { - Total int - Rows []AggregateRow + Total int + Rows []AggregateRow + Warnings []string } type AggregateRow struct { @@ -409,6 +556,14 @@ type FTAttribute struct { PhoneticMatcher string CaseSensitive bool WithSuffixtrie bool + + // Vector specific attributes + Algorithm string + DataType string + Dim int + DistanceMetric string + M int + EFConstruction int } type CursorStats struct { @@ -464,8 +619,9 @@ type SpellCheckSuggestion struct { } type FTSearchResult struct { - Total int - Docs []Document + Total int + Docs []Document + Warnings []string } type Document struct { @@ -474,6 +630,7 @@ type Document struct { Payload *string SortKey *string Fields map[string]string + Error error } type AggregateQuery []interface{} @@ -498,9 +655,112 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M return cmd } -func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { +// validateFTAggregateOptions validates mutually exclusive combinations of +// FTAggregateOptions fields before any command arguments are constructed. +func validateFTAggregateOptions(options *FTAggregateOptions) error { + if len(options.Steps) > 0 { + if options.Load != nil || options.Apply != nil || options.GroupBy != nil || + options.SortBy != nil || options.SortByMax != 0 { + return fmt.Errorf("FT.AGGREGATE: Steps cannot be combined with the deprecated Load, Apply, GroupBy, SortBy and SortByMax fields") + } + if options.LoadAll { + for _, step := range options.Steps { + if step.Load != nil { + return fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + } + } + } + if options.LoadAll && options.Load != nil { + return fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + return nil +} + +// appendFTAggregateStep appends the Redis command arguments for a single +// aggregation pipeline step. Each step must set exactly one of Load, Apply, +// GroupBy or SortBy. +func appendFTAggregateStep(args []interface{}, step FTAggregateStep) ([]interface{}, error) { + set := 0 + if step.Load != nil { + set++ + } + if step.Apply != nil { + set++ + } + if step.GroupBy != nil { + set++ + } + if step.SortBy != nil { + set++ + } + if set != 1 { + return args, fmt.Errorf("FT.AGGREGATE: each step must set exactly one of Load, Apply, GroupBy, SortBy (got %d)", set) + } + + switch { + case step.Load != nil: + args = append(args, "LOAD") + countIdx := len(args) + args = append(args, 0) + count := 0 + args = append(args, step.Load.Field) + count++ + if step.Load.As != "" { + args = append(args, "AS", step.Load.As) + count += 2 + } + args[countIdx] = count + case step.Apply != nil: + args = append(args, "APPLY", step.Apply.Field) + if step.Apply.As != "" { + args = append(args, "AS", step.Apply.As) + } + case step.GroupBy != nil: + args = append(args, "GROUPBY", len(step.GroupBy.Fields)) + args = append(args, step.GroupBy.Fields...) + for _, reducer := range step.GroupBy.Reduce { + args = append(args, "REDUCE", reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } + case step.SortBy != nil: + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range step.SortBy.Fields { + if sortBy.Asc && sortBy.Desc { + return args, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + if step.SortBy.Max > 0 { + args = append(args, "MAX", step.SortBy.Max) + } + } + return args, nil +} + +func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) { queryArgs := []interface{}{query} if options != nil { + if err := validateFTAggregateOptions(options); err != nil { + return nil, err + } if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } @@ -513,13 +773,10 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "ADDSCORES") } - if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") - } if options.LoadAll { queryArgs = append(queryArgs, "LOAD", "*") } - if options.Load != nil { + if len(options.Steps) == 0 && options.Load != nil { queryArgs = append(queryArgs, "LOAD", len(options.Load)) index, count := len(queryArgs)-1, 0 for _, load := range options.Load { @@ -537,53 +794,63 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) } - for _, apply := range options.Apply { - queryArgs = append(queryArgs, "APPLY", apply.Field) - if apply.As != "" { - queryArgs = append(queryArgs, "AS", apply.As) + if len(options.Steps) > 0 { + for _, step := range options.Steps { + var err error + queryArgs, err = appendFTAggregateStep(queryArgs, step) + if err != nil { + return nil, err + } + } + } else { + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } } - } - if options.GroupBy != nil { - for _, groupBy := range options.GroupBy { - queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) - queryArgs = append(queryArgs, groupBy.Fields...) - - for _, reducer := range groupBy.Reduce { - queryArgs = append(queryArgs, "REDUCE") - queryArgs = append(queryArgs, reducer.Reducer.String()) - if reducer.Args != nil { - queryArgs = append(queryArgs, len(reducer.Args)) - queryArgs = append(queryArgs, reducer.Args...) - } else { - queryArgs = append(queryArgs, 0) - } - if reducer.As != "" { - queryArgs = append(queryArgs, "AS", reducer.As) + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) + queryArgs = append(queryArgs, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + queryArgs = append(queryArgs, "REDUCE") + queryArgs = append(queryArgs, reducer.Reducer.String()) + if reducer.Args != nil { + queryArgs = append(queryArgs, len(reducer.Args)) + queryArgs = append(queryArgs, reducer.Args...) + } else { + queryArgs = append(queryArgs, 0) + } + if reducer.As != "" { + queryArgs = append(queryArgs, "AS", reducer.As) + } } } } - } - if options.SortBy != nil { - queryArgs = append(queryArgs, "SORTBY") - sortByOptions := []interface{}{} - for _, sortBy := range options.SortBy { - sortByOptions = append(sortByOptions, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") - } - if sortBy.Asc { - sortByOptions = append(sortByOptions, "ASC") - } - if sortBy.Desc { - sortByOptions = append(sortByOptions, "DESC") + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } } + queryArgs = append(queryArgs, len(sortByOptions)) + queryArgs = append(queryArgs, sortByOptions...) + } + if options.SortByMax > 0 { + queryArgs = append(queryArgs, "MAX", options.SortByMax) } - queryArgs = append(queryArgs, len(sortByOptions)) - queryArgs = append(queryArgs, sortByOptions...) - } - if options.SortByMax > 0 { - queryArgs = append(queryArgs, "MAX", options.SortByMax) } if options.LimitOffset >= 0 && options.Limit > 0 { queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) @@ -615,7 +882,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { @@ -657,8 +924,9 @@ func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd { return &AggregateCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeAggregate, }, } } @@ -688,15 +956,158 @@ func (cmd *AggregateCmd) String() string { } func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadSlice() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val, err = ProcessAggregateResult(data) + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + // Parse the raw response into structured result + if mapVal, ok := cmd.rawVal.(map[interface{}]interface{}); ok { + cmd.val, err = parseFTAggregateMapRESP3(mapVal) + } else { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + return err + } + + // RESP2 format or error response - use ReadReply to handle errors properly + data, err := rd.ReadReply() if err != nil { return err } - return nil + cmd.rawVal = data // Store raw value for debugging + if dataSlice, ok := data.([]interface{}); ok { + cmd.val, err = ProcessAggregateResult(dataSlice) + return err + } + return fmt.Errorf("unexpected response type: %T", data) +} + +// parseFTAggregateMapRESP3 parses the RESP3 format response from FT.AGGREGATE. +// It takes a map[interface{}]interface{} which is the raw response from ReadReply(). +// RESP3 format: +// +// %5 +// $10 attributes => *0 +// $13 total_results => :N +// $6 format => $6 STRING +// $7 results => *N (array of maps with extra_attributes, values) +// $7 warning => *N (array of strings) +func parseFTAggregateMapRESP3(data map[interface{}]interface{}) (*FTAggregateResult, error) { + result := &FTAggregateResult{ + Rows: make([]AggregateRow, 0), + } + + for k, v := range data { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "total_results": + result.Total = internal.ToInteger(v) + case "results": + if resultsData, ok := v.([]interface{}); ok { + rows, err := parseFTAggregateResultsMapRESP3(resultsData) + if err != nil { + return nil, err + } + result.Rows = rows + } + case "warning": + if warningsData, ok := v.([]interface{}); ok { + result.Warnings = make([]string, 0, len(warningsData)) + for _, w := range warningsData { + if ws, ok := w.(string); ok { + result.Warnings = append(result.Warnings, ws) + } + } + } + // Ignore "attributes", "format", and other fields as per the spec + } + } + + return result, nil +} + +// parseFTAggregateResultsMapRESP3 parses the results array from RESP3 FT.AGGREGATE response. +func parseFTAggregateResultsMapRESP3(resultsData []interface{}) ([]AggregateRow, error) { + rows := make([]AggregateRow, 0, len(resultsData)) + for _, item := range resultsData { + if itemMap, ok := item.(map[interface{}]interface{}); ok { + row, err := parseFTAggregateRowMapRESP3(itemMap) + if err != nil { + return nil, err + } + rows = append(rows, row) + } + } + return rows, nil +} + +// parseFTAggregateRowMapRESP3 parses a single row from RESP3 FT.AGGREGATE response. +func parseFTAggregateRowMapRESP3(itemMap map[interface{}]interface{}) (AggregateRow, error) { + row := AggregateRow{ + Fields: make(map[string]interface{}), + } + + for k, v := range itemMap { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "extra_attributes": + if extraAttrs, ok := v.(map[interface{}]interface{}); ok { + for ek, ev := range extraAttrs { + if ekStr, ok := ek.(string); ok { + row.Fields[ekStr] = ev + } + } + } + // Ignore "values" and other fields as per the spec + } + } + + return row, nil +} + +func (cmd *AggregateCmd) Clone() Cmder { + var val *FTAggregateResult + if cmd.val != nil { + val = &FTAggregateResult{ + Total: cmd.val.Total, + } + if cmd.val.Rows != nil { + val.Rows = make([]AggregateRow, len(cmd.val.Rows)) + for i, row := range cmd.val.Rows { + val.Rows[i] = AggregateRow{} + if row.Fields != nil { + val.Rows[i].Fields = make(map[string]interface{}, len(row.Fields)) + for k, v := range row.Fields { + val.Rows[i].Fields[k] = v + } + } + } + } + if cmd.val.Warnings != nil { + val.Warnings = make([]string, len(cmd.val.Warnings)) + copy(val.Warnings, cmd.val.Warnings) + } + } + return &AggregateCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } } // FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result. @@ -707,6 +1118,11 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd { args := []interface{}{"FT.AGGREGATE", index, query} if options != nil { + if err := validateFTAggregateOptions(options); err != nil { + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } if options.Verbatim { args = append(args, "VERBATIM") } @@ -716,13 +1132,10 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.AddScores { args = append(args, "ADDSCORES") } - if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") - } if options.LoadAll { args = append(args, "LOAD", "*") } - if options.Load != nil { + if len(options.Steps) == 0 && options.Load != nil { args = append(args, "LOAD", len(options.Load)) index, count := len(args)-1, 0 for _, load := range options.Load { @@ -738,52 +1151,66 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) } - for _, apply := range options.Apply { - args = append(args, "APPLY", apply.Field) - if apply.As != "" { - args = append(args, "AS", apply.As) - } - } - if options.GroupBy != nil { - for _, groupBy := range options.GroupBy { - args = append(args, "GROUPBY", len(groupBy.Fields)) - args = append(args, groupBy.Fields...) - - for _, reducer := range groupBy.Reduce { - args = append(args, "REDUCE") - args = append(args, reducer.Reducer.String()) - if reducer.Args != nil { - args = append(args, len(reducer.Args)) - args = append(args, reducer.Args...) - } else { - args = append(args, 0) - } - if reducer.As != "" { - args = append(args, "AS", reducer.As) - } + if len(options.Steps) > 0 { + for _, step := range options.Steps { + var err error + args, err = appendFTAggregateStep(args, step) + if err != nil { + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(err) + return cmd } } - } - if options.SortBy != nil { - args = append(args, "SORTBY") - sortByOptions := []interface{}{} - for _, sortBy := range options.SortBy { - sortByOptions = append(sortByOptions, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } else { + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) } - if sortBy.Asc { - sortByOptions = append(sortByOptions, "ASC") + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + args = append(args, "GROUPBY", len(groupBy.Fields)) + args = append(args, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + args = append(args, "REDUCE") + args = append(args, reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } } - if sortBy.Desc { - sortByOptions = append(sortByOptions, "DESC") + } + if options.SortBy != nil { + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")) + return cmd + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + } + if options.SortByMax > 0 { + args = append(args, "MAX", options.SortByMax) } - args = append(args, len(sortByOptions)) - args = append(args, sortByOptions...) - } - if options.SortByMax > 0 { - args = append(args, "MAX", options.SortByMax) } if options.LimitOffset >= 0 && options.Limit > 0 { args = append(args, "LIMIT", options.LimitOffset, options.Limit) @@ -918,7 +1345,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, "ON", "JSON") } if options.OnHash && options.OnJSON { - panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: ON HASH and ON JSON are mutually exclusive")) + return cmd } if options.Prefix != nil { args = append(args, "PREFIX", len(options.Prefix)) @@ -969,12 +1398,16 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp } } if schema == nil { - panic("FT.CREATE: SCHEMA is required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA is required")) + return cmd } args = append(args, "SCHEMA") for _, schema := range schema { if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid { - panic("FT.CREATE: SCHEMA FieldName and FieldType are required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldName and FieldType are required")) + return cmd } args = append(args, schema.FieldName) if schema.As != "" { @@ -983,15 +1416,32 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, schema.FieldType.String()) if schema.VectorArgs != nil { if schema.FieldType != SearchFieldTypeVector { - panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs")) + return cmd + } + // Check mutual exclusivity of vector options + optionCount := 0 + if schema.VectorArgs.FlatOptions != nil { + optionCount++ } - if schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil { - panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive") + if schema.VectorArgs.HNSWOptions != nil { + optionCount++ + } + if schema.VectorArgs.VamanaOptions != nil { + optionCount++ + } + if optionCount != 1 { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + return cmd } if schema.VectorArgs.FlatOptions != nil { args = append(args, "FLAT") if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT")) + return cmd } flatArgs := []interface{}{ "TYPE", schema.VectorArgs.FlatOptions.Type, @@ -1010,7 +1460,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.VectorArgs.HNSWOptions != nil { args = append(args, "HNSW") if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW")) + return cmd } hnswArgs := []interface{}{ "TYPE", schema.VectorArgs.HNSWOptions.Type, @@ -1035,10 +1487,48 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, len(hnswArgs)) args = append(args, hnswArgs...) } + if schema.VectorArgs.VamanaOptions != nil { + args = append(args, "SVS-VAMANA") + if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + return cmd + } + vamanaArgs := []interface{}{ + "TYPE", schema.VectorArgs.VamanaOptions.Type, + "DIM", schema.VectorArgs.VamanaOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.VamanaOptions.DistanceMetric, + } + if schema.VectorArgs.VamanaOptions.Compression != "" { + vamanaArgs = append(vamanaArgs, "COMPRESSION", schema.VectorArgs.VamanaOptions.Compression) + } + if schema.VectorArgs.VamanaOptions.ConstructionWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "CONSTRUCTION_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.ConstructionWindowSize) + } + if schema.VectorArgs.VamanaOptions.GraphMaxDegree > 0 { + vamanaArgs = append(vamanaArgs, "GRAPH_MAX_DEGREE", schema.VectorArgs.VamanaOptions.GraphMaxDegree) + } + if schema.VectorArgs.VamanaOptions.SearchWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "SEARCH_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.SearchWindowSize) + } + if schema.VectorArgs.VamanaOptions.Epsilon > 0 { + vamanaArgs = append(vamanaArgs, "EPSILON", schema.VectorArgs.VamanaOptions.Epsilon) + } + if schema.VectorArgs.VamanaOptions.TrainingThreshold > 0 { + vamanaArgs = append(vamanaArgs, "TRAINING_THRESHOLD", schema.VectorArgs.VamanaOptions.TrainingThreshold) + } + if schema.VectorArgs.VamanaOptions.ReduceDim > 0 { + vamanaArgs = append(vamanaArgs, "REDUCE", schema.VectorArgs.VamanaOptions.ReduceDim) + } + args = append(args, len(vamanaArgs)) + args = append(args, vamanaArgs...) + } } if schema.GeoShapeFieldType != "" { if schema.FieldType != SearchFieldTypeGeoShape { - panic("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType")) + return cmd } args = append(args, schema.GeoShapeFieldType) } @@ -1196,100 +1686,325 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri // FTExplainCli - Returns the execution plan for a complex query. [Not Implemented] // For more information, see https://redis.io/commands/ft.explaincli/ func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { - panic("not implemented") + return fmt.Errorf("FTExplainCli is not implemented") +} + +// parseFTAttributeFromMap parses an FTAttribute from a RESP3 map format +func parseFTAttributeFromMap(attrMap map[interface{}]interface{}) FTAttribute { + att := FTAttribute{} + for k, v := range attrMap { + key := internal.ToLower(internal.ToString(k)) + switch key { + case "attribute": + att.Attribute = internal.ToString(v) + case "identifier": + att.Identifier = internal.ToString(v) + case "type": + att.Type = internal.ToString(v) + case "weight": + att.Weight = internal.ToFloat(v) + case "phonetic": + att.PhoneticMatcher = internal.ToString(v) + case "algorithm": + att.Algorithm = internal.ToString(v) + case "data_type": + att.DataType = internal.ToString(v) + case "dim": + att.Dim = internal.ToInteger(v) + case "distance_metric": + att.DistanceMetric = internal.ToString(v) + case "m": + att.M = internal.ToInteger(v) + case "ef_construction": + att.EFConstruction = internal.ToInteger(v) + case "flags": + // flags is an array of strings like ["SORTABLE", "NOSTEM"] + if flags, ok := v.([]interface{}); ok { + for _, flag := range flags { + flagStr := internal.ToLower(internal.ToString(flag)) + switch flagStr { + case "nostem": + att.NoStem = true + case "sortable": + att.Sortable = true + case "noindex": + att.NoIndex = true + case "unf": + att.UNF = true + case "case_sensitive": + att.CaseSensitive = true + case "withsuffixtrie": + att.WithSuffixtrie = true + } + } + } + } + } + return att +} + +// getMapStringKey extracts a string value from a map with interface{} keys +func getMapStringKey(m map[interface{}]interface{}, key string) interface{} { + if v, ok := m[key]; ok { + return v + } + return nil +} + +// parseIndexErrorsRESP3 parses Index Errors from RESP3 map format +func parseIndexErrorsRESP3(m map[interface{}]interface{}) IndexErrors { + return IndexErrors{ + IndexingFailures: internal.ToInteger(getMapStringKey(m, "indexing failures")), + LastIndexingError: internal.ToString(getMapStringKey(m, "last indexing error")), + LastIndexingErrorKey: internal.ToString(getMapStringKey(m, "last indexing error key")), + } +} + +// parseCursorStatsRESP3 parses cursor_stats from RESP3 map format +func parseCursorStatsRESP3(m map[interface{}]interface{}) CursorStats { + return CursorStats{ + GlobalIdle: internal.ToInteger(getMapStringKey(m, "global_idle")), + GlobalTotal: internal.ToInteger(getMapStringKey(m, "global_total")), + IndexCapacity: internal.ToInteger(getMapStringKey(m, "index_capacity")), + IndexTotal: internal.ToInteger(getMapStringKey(m, "index_total")), + } +} + +// parseGCStatsRESP3 parses gc_stats from RESP3 map format +func parseGCStatsRESP3(m map[interface{}]interface{}) GCStats { + // Handle average_cycle_time_ms which can be a float64 (including NaN) or string + avgCycleTime := "" + if v := getMapStringKey(m, "average_cycle_time_ms"); v != nil { + switch val := v.(type) { + case string: + // Normalize to lowercase for consistency with RESP2 + avgCycleTime = strings.ToLower(val) + case float64: + avgCycleTime = internal.FormatFloat(val) + } + } + + return GCStats{ + BytesCollected: ftInfoNumInt(getMapStringKey(m, "bytes_collected")), + TotalMsRun: ftInfoNumInt(getMapStringKey(m, "total_ms_run")), + TotalCycles: ftInfoNumInt(getMapStringKey(m, "total_cycles")), + AverageCycleTimeMs: avgCycleTime, + LastRunTimeMs: ftInfoNumInt(getMapStringKey(m, "last_run_time_ms")), + GCNumericTreesMissed: ftInfoNumInt(getMapStringKey(m, "gc_numeric_trees_missed")), + GCBlocksDenied: ftInfoNumInt(getMapStringKey(m, "gc_blocks_denied")), + } +} + +// parseIndexDefinitionRESP3 parses index_definition from RESP3 map format +func parseIndexDefinitionRESP3(m map[interface{}]interface{}) IndexDefinition { + def := IndexDefinition{ + KeyType: internal.ToString(getMapStringKey(m, "key_type")), + DefaultScore: internal.ToFloat(getMapStringKey(m, "default_score")), + } + if prefixes, ok := getMapStringKey(m, "prefixes").([]interface{}); ok { + def.Prefixes = internal.ToStringSlice(prefixes) + } + return def +} + +// parseDialectStatsRESP3 parses dialect_stats from RESP3 map format +func parseDialectStatsRESP3(m map[interface{}]interface{}) map[string]int { + result := make(map[string]int) + for k, v := range m { + if kStr, ok := k.(string); ok { + result[kStr] = internal.ToInteger(v) + } + } + return result +} + +// ftInfoNumString stringifies a value that RediSearch emits via REPLY_KVNUM +// (RedisModule_ReplyWithDouble): a bulk string in RESP2 but a native double +// in RESP3. Used for FTInfoResult fields whose public type is string. +// Special float values (NaN, +Inf, -Inf) are normalized to lowercase to match +// the RESP2 wire format. +func ftInfoNumString(val interface{}) string { + switch v := val.(type) { + case string: + return v + case float64: + return internal.FormatFloat(v) + case float32: + return internal.FormatFloat(float64(v)) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + return "" + } +} + +// ftInfoNumInt converts a value that RediSearch emits via REPLY_KVNUM to int. +// In RESP2 the value is a bulk string; in RESP3 it is a native double, even +// for logically-integer fields (counters, byte sizes). This helper exists so +// the internal.ToInteger helper can remain strict about float-to-int coercion +// while still letting the RediSearch parsers read those values correctly. +func ftInfoNumInt(val interface{}) int { + switch v := val.(type) { + case float64: + return int(v) + case float32: + return int(v) + default: + return internal.ToInteger(v) + } } func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { var ftInfo FTInfoResult - // Manually parse each field from the map + + // Parse Index Errors - handle both RESP2 (array) and RESP3 (map) formats if indexErrors, ok := data["Index Errors"].([]interface{}); ok { + // RESP2 format: array with key-value pairs ftInfo.IndexErrors = IndexErrors{ IndexingFailures: internal.ToInteger(indexErrors[1]), LastIndexingError: internal.ToString(indexErrors[3]), LastIndexingErrorKey: internal.ToString(indexErrors[5]), } + } else if indexErrors, ok := data["Index Errors"].(map[interface{}]interface{}); ok { + // RESP3 format: map + ftInfo.IndexErrors = parseIndexErrorsRESP3(indexErrors) } if attributes, ok := data["attributes"].([]interface{}); ok { for _, attr := range attributes { - if attrMap, ok := attr.([]interface{}); ok { - att := FTAttribute{} - for i := 0; i < len(attrMap); i++ { - if internal.ToLower(internal.ToString(attrMap[i])) == "attribute" { - att.Attribute = internal.ToString(attrMap[i+1]) + att := FTAttribute{} + // Handle RESP2 format: attribute is []interface{} + if attrSlice, ok := attr.([]interface{}); ok { + attrLen := len(attrSlice) + for i := 0; i < attrLen; i++ { + if internal.ToLower(internal.ToString(attrSlice[i])) == "attribute" && i+1 < attrLen { + att.Attribute = internal.ToString(attrSlice[i+1]) + i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "identifier" { - att.Identifier = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "identifier" && i+1 < attrLen { + att.Identifier = internal.ToString(attrSlice[i+1]) + i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "type" { - att.Type = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "type" && i+1 < attrLen { + att.Type = internal.ToString(attrSlice[i+1]) + i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "weight" { - att.Weight = internal.ToFloat(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "weight" && i+1 < attrLen { + att.Weight = internal.ToFloat(attrSlice[i+1]) + i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "nostem" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "nostem" { att.NoStem = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "sortable" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "sortable" { att.Sortable = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "noindex" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "noindex" { att.NoIndex = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "unf" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "unf" { att.UNF = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "phonetic" { - att.PhoneticMatcher = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "phonetic" && i+1 < attrLen { + att.PhoneticMatcher = internal.ToString(attrSlice[i+1]) continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "case_sensitive" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "case_sensitive" { att.CaseSensitive = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "withsuffixtrie" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "withsuffixtrie" { att.WithSuffixtrie = true continue } + // vector specific attributes + if internal.ToLower(internal.ToString(attrSlice[i])) == "algorithm" && i+1 < attrLen { + att.Algorithm = internal.ToString(attrSlice[i+1]) + i++ + continue + } + if internal.ToLower(internal.ToString(attrSlice[i])) == "data_type" && i+1 < attrLen { + att.DataType = internal.ToString(attrSlice[i+1]) + i++ + continue + } + if internal.ToLower(internal.ToString(attrSlice[i])) == "dim" && i+1 < attrLen { + att.Dim = internal.ToInteger(attrSlice[i+1]) + i++ + continue + } + if internal.ToLower(internal.ToString(attrSlice[i])) == "distance_metric" && i+1 < attrLen { + att.DistanceMetric = internal.ToString(attrSlice[i+1]) + i++ + continue + } + if internal.ToLower(internal.ToString(attrSlice[i])) == "m" && i+1 < attrLen { + att.M = internal.ToInteger(attrSlice[i+1]) + i++ + continue + } + if internal.ToLower(internal.ToString(attrSlice[i])) == "ef_construction" && i+1 < attrLen { + att.EFConstruction = internal.ToInteger(attrSlice[i+1]) + i++ + continue + } } ftInfo.Attributes = append(ftInfo.Attributes, att) + } else if attrMap, ok := attr.(map[interface{}]interface{}); ok { + // Handle RESP3 format: attribute is map[interface{}]interface{} + att = parseFTAttributeFromMap(attrMap) + ftInfo.Attributes = append(ftInfo.Attributes, att) } } } - ftInfo.BytesPerRecordAvg = internal.ToString(data["bytes_per_record_avg"]) + ftInfo.BytesPerRecordAvg = ftInfoNumString(data["bytes_per_record_avg"]) ftInfo.Cleaning = internal.ToInteger(data["cleaning"]) + // Parse cursor_stats - handle both RESP2 (array) and RESP3 (map) formats if cursorStats, ok := data["cursor_stats"].([]interface{}); ok { + // RESP2 format ftInfo.CursorStats = CursorStats{ GlobalIdle: internal.ToInteger(cursorStats[1]), GlobalTotal: internal.ToInteger(cursorStats[3]), IndexCapacity: internal.ToInteger(cursorStats[5]), IndexTotal: internal.ToInteger(cursorStats[7]), } + } else if cursorStats, ok := data["cursor_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.CursorStats = parseCursorStatsRESP3(cursorStats) } + // Parse dialect_stats - handle both RESP2 (array) and RESP3 (map) formats if dialectStats, ok := data["dialect_stats"].([]interface{}); ok { + // RESP2 format ftInfo.DialectStats = make(map[string]int) for i := 0; i < len(dialectStats); i += 2 { ftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1]) } + } else if dialectStats, ok := data["dialect_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.DialectStats = parseDialectStatsRESP3(dialectStats) } ftInfo.DocTableSizeMB = internal.ToFloat(data["doc_table_size_mb"]) + // Parse field statistics - handle both RESP2 and RESP3 formats if fieldStats, ok := data["field statistics"].([]interface{}); ok { for _, stat := range fieldStats { if statMap, ok := stat.([]interface{}); ok { + // RESP2 format ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{ Identifier: internal.ToString(statMap[1]), Attribute: internal.ToString(statMap[3]), @@ -1299,11 +2014,23 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { LastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]), }, }) + } else if statMap, ok := stat.(map[interface{}]interface{}); ok { + // RESP3 format + fs := FieldStatistic{ + Identifier: internal.ToString(getMapStringKey(statMap, "identifier")), + Attribute: internal.ToString(getMapStringKey(statMap, "attribute")), + } + if indexErrors, ok := getMapStringKey(statMap, "Index Errors").(map[interface{}]interface{}); ok { + fs.IndexErrors = parseIndexErrorsRESP3(indexErrors) + } + ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, fs) } } } + // Parse gc_stats - handle both RESP2 (array) and RESP3 (map) formats if gcStats, ok := data["gc_stats"].([]interface{}); ok { + // RESP2 format ftInfo.GCStats = GCStats{} for i := 0; i < len(gcStats); i += 2 { if internal.ToLower(internal.ToString(gcStats[i])) == "bytes_collected" { @@ -1335,21 +2062,31 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { continue } } + } else if gcStats, ok := data["gc_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.GCStats = parseGCStatsRESP3(gcStats) } ftInfo.GeoshapesSzMB = internal.ToFloat(data["geoshapes_sz_mb"]) ftInfo.HashIndexingFailures = internal.ToInteger(data["hash_indexing_failures"]) + // Parse index_definition - handle both RESP2 (array) and RESP3 (map) formats if indexDef, ok := data["index_definition"].([]interface{}); ok { + // RESP2 format ftInfo.IndexDefinition = IndexDefinition{ KeyType: internal.ToString(indexDef[1]), Prefixes: internal.ToStringSlice(indexDef[3]), DefaultScore: internal.ToFloat(indexDef[5]), } + } else if indexDef, ok := data["index_definition"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.IndexDefinition = parseIndexDefinitionRESP3(indexDef) } ftInfo.IndexName = internal.ToString(data["index_name"]) - ftInfo.IndexOptions = internal.ToStringSlice(data["index_options"].([]interface{})) + if indexOptions, ok := data["index_options"].([]interface{}); ok { + ftInfo.IndexOptions = internal.ToStringSlice(indexOptions) + } ftInfo.Indexing = internal.ToInteger(data["indexing"]) ftInfo.InvertedSzMB = internal.ToFloat(data["inverted_sz_mb"]) ftInfo.KeyTableSizeMB = internal.ToFloat(data["key_table_size_mb"]) @@ -1358,16 +2095,16 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { ftInfo.NumRecords = internal.ToInteger(data["num_records"]) ftInfo.NumTerms = internal.ToInteger(data["num_terms"]) ftInfo.NumberOfUses = internal.ToInteger(data["number_of_uses"]) - ftInfo.OffsetBitsPerRecordAvg = internal.ToString(data["offset_bits_per_record_avg"]) + ftInfo.OffsetBitsPerRecordAvg = ftInfoNumString(data["offset_bits_per_record_avg"]) ftInfo.OffsetVectorsSzMB = internal.ToFloat(data["offset_vectors_sz_mb"]) - ftInfo.OffsetsPerTermAvg = internal.ToString(data["offsets_per_term_avg"]) + ftInfo.OffsetsPerTermAvg = ftInfoNumString(data["offsets_per_term_avg"]) ftInfo.PercentIndexed = internal.ToFloat(data["percent_indexed"]) - ftInfo.RecordsPerDocAvg = internal.ToString(data["records_per_doc_avg"]) + ftInfo.RecordsPerDocAvg = ftInfoNumString(data["records_per_doc_avg"]) ftInfo.SortableValuesSizeMB = internal.ToFloat(data["sortable_values_size_mb"]) ftInfo.TagOverheadSzMB = internal.ToFloat(data["tag_overhead_sz_mb"]) ftInfo.TextOverheadSzMB = internal.ToFloat(data["text_overhead_sz_mb"]) ftInfo.TotalIndexMemorySzMB = internal.ToFloat(data["total_index_memory_sz_mb"]) - ftInfo.TotalIndexingTime = internal.ToInteger(data["total_indexing_time"]) + ftInfo.TotalIndexingTime = ftInfoNumInt(data["total_indexing_time"]) ftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data["total_inverted_index_blocks"]) ftInfo.VectorIndexSzMB = internal.ToFloat(data["vector_index_sz_mb"]) @@ -1382,8 +2119,9 @@ type FTInfoCmd struct { func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd { return &FTInfoCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFTInfo, }, } } @@ -1412,6 +2150,37 @@ func (cmd *FTInfoCmd) RawResult() (interface{}, error) { return cmd.rawVal, cmd.err } func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { + readType, err := rd.PeekReplyType() + if err != nil { + return err + } + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Convert map[interface{}]interface{} to map[string]interface{} + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + data := make(map[string]interface{}, len(rawMap)) + for k, v := range rawMap { + if kStr, ok := k.(string); ok { + data[kStr] = v + } + } + + cmd.val, err = parseFTInfo(data) + return err + } + + // RESP2 format - read as map n, err := rd.ReadMapLen() if err != nil { return err @@ -1438,11 +2207,62 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { data[k] = v } cmd.val, err = parseFTInfo(data) - if err != nil { - return err + return err +} + +func (cmd *FTInfoCmd) Clone() Cmder { + val := FTInfoResult{ + IndexErrors: cmd.val.IndexErrors, + BytesPerRecordAvg: cmd.val.BytesPerRecordAvg, + Cleaning: cmd.val.Cleaning, + CursorStats: cmd.val.CursorStats, + DocTableSizeMB: cmd.val.DocTableSizeMB, + GCStats: cmd.val.GCStats, + GeoshapesSzMB: cmd.val.GeoshapesSzMB, + HashIndexingFailures: cmd.val.HashIndexingFailures, + IndexDefinition: cmd.val.IndexDefinition, + IndexName: cmd.val.IndexName, + Indexing: cmd.val.Indexing, + InvertedSzMB: cmd.val.InvertedSzMB, + KeyTableSizeMB: cmd.val.KeyTableSizeMB, + MaxDocID: cmd.val.MaxDocID, + NumDocs: cmd.val.NumDocs, + NumRecords: cmd.val.NumRecords, + NumTerms: cmd.val.NumTerms, + NumberOfUses: cmd.val.NumberOfUses, + OffsetBitsPerRecordAvg: cmd.val.OffsetBitsPerRecordAvg, + OffsetVectorsSzMB: cmd.val.OffsetVectorsSzMB, + OffsetsPerTermAvg: cmd.val.OffsetsPerTermAvg, + PercentIndexed: cmd.val.PercentIndexed, + RecordsPerDocAvg: cmd.val.RecordsPerDocAvg, + SortableValuesSizeMB: cmd.val.SortableValuesSizeMB, + TagOverheadSzMB: cmd.val.TagOverheadSzMB, + TextOverheadSzMB: cmd.val.TextOverheadSzMB, + TotalIndexMemorySzMB: cmd.val.TotalIndexMemorySzMB, + TotalIndexingTime: cmd.val.TotalIndexingTime, + TotalInvertedIndexBlocks: cmd.val.TotalInvertedIndexBlocks, + VectorIndexSzMB: cmd.val.VectorIndexSzMB, + } + // Clone slices and maps + if cmd.val.Attributes != nil { + val.Attributes = slices.Clone(cmd.val.Attributes) + } + if cmd.val.DialectStats != nil { + val.DialectStats = maps.Clone(cmd.val.DialectStats) + } + if cmd.val.FieldStatistics != nil { + val.FieldStatistics = slices.Clone(cmd.val.FieldStatistics) + } + if cmd.val.IndexOptions != nil { + val.IndexOptions = slices.Clone(cmd.val.IndexOptions) + } + if cmd.val.IndexDefinition.Prefixes != nil { + val.IndexDefinition.Prefixes = slices.Clone(cmd.val.IndexDefinition.Prefixes) + } + return &FTInfoCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, } - - return nil } // FTInfo - Retrieves information about an index. @@ -1501,8 +2321,9 @@ type FTSpellCheckCmd struct { func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd { return &FTSpellCheckCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFTSpellCheck, }, } } @@ -1532,15 +2353,117 @@ func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) { } func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadSlice() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val, err = parseFTSpellCheck(data) + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Parse the raw response into structured result + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + cmd.val, err = parseFTSpellCheckRESP3(rawMap) + return err + } + + // RESP2 format + data, err := rd.ReadSlice() if err != nil { return err } - return nil + cmd.val, err = parseFTSpellCheck(data) + return err +} + +// parseFTSpellCheckRESP3 parses the RESP3 format response from FT.SPELLCHECK. +// RESP3 format: +// +// map{ +// "results": map{ +// "misspelled_term": [ +// map{"suggestion": score}, +// ... +// ], +// ... +// } +// } +func parseFTSpellCheckRESP3(data map[interface{}]interface{}) ([]SpellCheckResult, error) { + results := make([]SpellCheckResult, 0) + + resultsData, ok := data["results"] + if !ok { + return results, nil + } + + resultsMap, ok := resultsData.(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("invalid results format: expected map, got %T", resultsData) + } + + for termKey, suggestionsData := range resultsMap { + term, ok := termKey.(string) + if !ok { + continue + } + + suggestionsArray, ok := suggestionsData.([]interface{}) + if !ok { + continue + } + + suggestions := make([]SpellCheckSuggestion, 0, len(suggestionsArray)) + for _, suggestionData := range suggestionsArray { + suggestionMap, ok := suggestionData.(map[interface{}]interface{}) + if !ok { + continue + } + + for suggKey, scoreVal := range suggestionMap { + suggestion, ok := suggKey.(string) + if !ok { + continue + } + + var score float64 + switch v := scoreVal.(type) { + case float64: + score = v + case int64: + score = float64(v) + case string: + var err error + score, err = strconv.ParseFloat(v, 64) + if err != nil { + continue + } + default: + continue + } + + suggestions = append(suggestions, SpellCheckSuggestion{ + Score: score, + Suggestion: suggestion, + }) + } + } + + results = append(results, SpellCheckResult{ + Term: term, + Suggestions: suggestions, + }) + } + + return results, nil } func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) { @@ -1598,6 +2521,25 @@ func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) { return results, nil } +func (cmd *FTSpellCheckCmd) Clone() Cmder { + var val []SpellCheckResult + if cmd.val != nil { + val = make([]SpellCheckResult, len(cmd.val)) + for i, result := range cmd.val { + val[i] = SpellCheckResult{ + Term: result.Term, + } + if result.Suggestions != nil { + val[i].Suggestions = slices.Clone(result.Suggestions) + } + } + } + return &FTSpellCheckCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) { if len(data) < 1 { return FTSearchResult{}, fmt.Errorf("unexpected search result format") @@ -1654,7 +2596,13 @@ func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, with if i < len(data) { fields, ok := data[i].([]interface{}) if !ok { - return FTSearchResult{}, fmt.Errorf("invalid document fields format") + if data[i] == proto.Nil || data[i] == nil { + doc.Error = proto.Nil + doc.Fields = map[string]string{} + fields = []interface{}{} + } else { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } } for j := 0; j < len(fields); j += 2 { @@ -1688,8 +2636,9 @@ type FTSearchCmd struct { func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd { return &FTSearchCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFTSearch, }, options: options, } @@ -1720,17 +2669,530 @@ func (cmd *FTSearchCmd) RawResult() (interface{}, error) { } func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { + readType, err := rd.PeekReplyType() + if err != nil { + return err + } + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + // Parse the raw response into structured result + if mapVal, ok := cmd.rawVal.(map[interface{}]interface{}); ok { + cmd.val, err = parseFTSearchMapRESP3(mapVal) + } else { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + return err + } + + // RESP2 format or error response - use ReadReply to handle errors properly + data, err := rd.ReadReply() + if err != nil { + return err + } + if dataSlice, ok := data.([]interface{}); ok { + cmd.val, err = parseFTSearch(dataSlice, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + return err + } + return fmt.Errorf("unexpected response type: %T", data) +} + +// parseFTSearchMapRESP3 parses the RESP3 format response from FT.SEARCH. +// It takes a map[interface{}]interface{} which is the raw response from ReadReply(). +// RESP3 format: +// +// %5 +// $10 attributes => *0 +// $13 total_results => :N +// $6 format => $6 STRING +// $7 results => *N (array of maps with id, score, extra_attributes, values) +// $7 warning => *N (array of strings) +func parseFTSearchMapRESP3(data map[interface{}]interface{}) (FTSearchResult, error) { + var result FTSearchResult + result.Docs = make([]Document, 0) + + for k, v := range data { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "total_results": + result.Total = internal.ToInteger(v) + case "results": + if resultsData, ok := v.([]interface{}); ok { + docs, err := parseFTSearchResultsMapRESP3(resultsData) + if err != nil { + return FTSearchResult{}, err + } + result.Docs = docs + } + case "warning": + if warningsData, ok := v.([]interface{}); ok { + result.Warnings = make([]string, 0, len(warningsData)) + for _, w := range warningsData { + if ws, ok := w.(string); ok { + result.Warnings = append(result.Warnings, ws) + } + } + } + // Ignore "attributes", "format", and other fields as per the spec + } + } + + return result, nil +} + +// parseFTSearchResultsMapRESP3 parses the results array from RESP3 FT.SEARCH response. +func parseFTSearchResultsMapRESP3(resultsData []interface{}) ([]Document, error) { + docs := make([]Document, 0, len(resultsData)) + for _, item := range resultsData { + if itemMap, ok := item.(map[interface{}]interface{}); ok { + doc, err := parseFTSearchDocumentMapRESP3(itemMap) + if err != nil { + return nil, err + } + docs = append(docs, doc) + } + } + return docs, nil +} + +// parseFTSearchDocumentMapRESP3 parses a single document from RESP3 FT.SEARCH response. +func parseFTSearchDocumentMapRESP3(itemMap map[interface{}]interface{}) (Document, error) { + doc := Document{ + Fields: make(map[string]string), + } + + for k, v := range itemMap { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "id": + if id, ok := v.(string); ok { + doc.ID = id + } + case "score": + if score, ok := v.(float64); ok { + doc.Score = &score + } + case "payload": + if payload, ok := v.(string); ok { + doc.Payload = &payload + } + case "sortkey": + if sortKey, ok := v.(string); ok { + doc.SortKey = &sortKey + } + case "extra_attributes": + if extraAttrs, ok := v.(map[interface{}]interface{}); ok { + for ek, ev := range extraAttrs { + if ekStr, ok := ek.(string); ok { + if evStr, ok := ev.(string); ok { + doc.Fields[ekStr] = evStr + } + } + } + } + // Ignore "values" and other fields as per the spec + } + } + + return doc, nil +} + +func (cmd *FTSearchCmd) Clone() Cmder { + val := FTSearchResult{ + Total: cmd.val.Total, + } + if cmd.val.Docs != nil { + val.Docs = make([]Document, len(cmd.val.Docs)) + for i, doc := range cmd.val.Docs { + val.Docs[i] = Document{ + ID: doc.ID, + Score: doc.Score, + Payload: doc.Payload, + SortKey: doc.SortKey, + } + if doc.Fields != nil { + val.Docs[i].Fields = make(map[string]string, len(doc.Fields)) + for k, v := range doc.Fields { + val.Docs[i].Fields[k] = v + } + } + } + } + if cmd.val.Warnings != nil { + val.Warnings = make([]string, len(cmd.val.Warnings)) + copy(val.Warnings, cmd.val.Warnings) + } + var options *FTSearchOptions + if cmd.options != nil { + options = &FTSearchOptions{ + NoContent: cmd.options.NoContent, + Verbatim: cmd.options.Verbatim, + NoStopWords: cmd.options.NoStopWords, + WithScores: cmd.options.WithScores, + WithPayloads: cmd.options.WithPayloads, + WithSortKeys: cmd.options.WithSortKeys, + Slop: cmd.options.Slop, + Timeout: cmd.options.Timeout, + InOrder: cmd.options.InOrder, + Language: cmd.options.Language, + Expander: cmd.options.Expander, + Scorer: cmd.options.Scorer, + ExplainScore: cmd.options.ExplainScore, + Payload: cmd.options.Payload, + SortByWithCount: cmd.options.SortByWithCount, + LimitOffset: cmd.options.LimitOffset, + Limit: cmd.options.Limit, + CountOnly: cmd.options.CountOnly, + DialectVersion: cmd.options.DialectVersion, + } + // Clone slices and maps + if cmd.options.Filters != nil { + options.Filters = slices.Clone(cmd.options.Filters) + } + if cmd.options.GeoFilter != nil { + options.GeoFilter = slices.Clone(cmd.options.GeoFilter) + } + if cmd.options.InKeys != nil { + options.InKeys = slices.Clone(cmd.options.InKeys) + } + if cmd.options.InFields != nil { + options.InFields = slices.Clone(cmd.options.InFields) + } + if cmd.options.Return != nil { + options.Return = slices.Clone(cmd.options.Return) + } + if cmd.options.SortBy != nil { + options.SortBy = slices.Clone(cmd.options.SortBy) + } + if cmd.options.Params != nil { + options.Params = maps.Clone(cmd.options.Params) + } + } + return &FTSearchCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + options: options, + } +} + +// FTHybridResult represents the result of a hybrid search operation +type FTHybridResult struct { + TotalResults int + Results []map[string]interface{} + Warnings []string + ExecutionTime float64 +} + +// FTHybridCursorResult represents cursor result for hybrid search +type FTHybridCursorResult struct { + SearchCursorID int + VsimCursorID int +} + +type FTHybridCmd struct { + baseCmd + val FTHybridResult + cursorVal *FTHybridCursorResult + options *FTHybridOptions + withCursor bool +} + +func newFTHybridCmd(ctx context.Context, options *FTHybridOptions, args ...interface{}) *FTHybridCmd { + var withCursor bool + if options != nil && options.WithCursor { + withCursor = true + } + return &FTHybridCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + options: options, + withCursor: withCursor, + } +} + +func (cmd *FTHybridCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTHybridCmd) SetVal(val FTHybridResult) { + cmd.val = val +} + +func (cmd *FTHybridCmd) Result() (FTHybridResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTHybridCmd) CursorResult() (*FTHybridCursorResult, error) { + return cmd.cursorVal, cmd.err +} + +func (cmd *FTHybridCmd) Val() FTHybridResult { + return cmd.val +} + +func (cmd *FTHybridCmd) CursorVal() *FTHybridCursorResult { + return cmd.cursorVal +} + +func (cmd *FTHybridCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *FTHybridCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} + +func parseFTHybrid(data []interface{}, withCursor bool) (FTHybridResult, *FTHybridCursorResult, error) { + // Convert to map + resultMap := make(map[string]interface{}) + for i := 0; i < len(data); i += 2 { + if i+1 < len(data) { + key, ok := data[i].(string) + if !ok { + return FTHybridResult{}, nil, fmt.Errorf("invalid key type at index %d", i) + } + resultMap[key] = data[i+1] + } + } + + // Handle cursor result + if withCursor { + searchCursorID, ok1 := resultMap["SEARCH"].(int64) + vsimCursorID, ok2 := resultMap["VSIM"].(int64) + if !ok1 || !ok2 { + return FTHybridResult{}, nil, fmt.Errorf("invalid cursor result format") + } + return FTHybridResult{}, &FTHybridCursorResult{ + SearchCursorID: int(searchCursorID), + VsimCursorID: int(vsimCursorID), + }, nil + } + + // Parse regular result + totalResults, ok := resultMap["total_results"].(int64) + if !ok { + return FTHybridResult{}, nil, fmt.Errorf("invalid total_results format") + } + + resultsData, ok := resultMap["results"].([]interface{}) + if !ok { + return FTHybridResult{}, nil, fmt.Errorf("invalid results format") + } + + // Parse each result item + results := make([]map[string]interface{}, 0, len(resultsData)) + for _, item := range resultsData { + // Try parsing as map[string]interface{} first (RESP3 format) + if itemMap, ok := item.(map[string]interface{}); ok { + results = append(results, itemMap) + continue + } + + // Try parsing as map[interface{}]interface{} (alternative RESP3 format) + if rawMap, ok := item.(map[interface{}]interface{}); ok { + itemMap := make(map[string]interface{}) + for k, v := range rawMap { + if keyStr, ok := k.(string); ok { + itemMap[keyStr] = v + } + } + results = append(results, itemMap) + continue + } + + // Fall back to array format (RESP2 format - key-value pairs) + itemData, ok := item.([]interface{}) + if !ok { + return FTHybridResult{}, nil, fmt.Errorf("invalid result item format") + } + + itemMap := make(map[string]interface{}) + for i := 0; i < len(itemData); i += 2 { + if i+1 < len(itemData) { + key, ok := itemData[i].(string) + if !ok { + return FTHybridResult{}, nil, fmt.Errorf("invalid item key format") + } + itemMap[key] = itemData[i+1] + } + } + results = append(results, itemMap) + } + + // Parse warnings (optional field) + var warnings []string + if warningsData, ok := resultMap["warnings"].([]interface{}); ok { + warnings = make([]string, 0, len(warningsData)) + for _, w := range warningsData { + if ws, ok := w.(string); ok { + warnings = append(warnings, ws) + } + } + } + + // Parse execution time (optional field) + var executionTime float64 + if execTimeVal, exists := resultMap["execution_time"]; exists { + switch v := execTimeVal.(type) { + case string: + var err error + executionTime, err = strconv.ParseFloat(v, 64) + if err != nil { + return FTHybridResult{}, nil, fmt.Errorf("invalid execution_time format: %v", err) + } + case float64: + executionTime = v + case int64: + executionTime = float64(v) + } + } + + return FTHybridResult{ + TotalResults: int(totalResults), + Results: results, + Warnings: warnings, + ExecutionTime: executionTime, + }, nil, nil +} + +func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { return err } - cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + + result, cursorResult, err := parseFTHybrid(data, cmd.withCursor) if err != nil { return err } + + if cmd.withCursor { + cmd.cursorVal = cursorResult + } else { + cmd.val = result + } return nil } +func (cmd *FTHybridCmd) Clone() Cmder { + val := FTHybridResult{ + TotalResults: cmd.val.TotalResults, + ExecutionTime: cmd.val.ExecutionTime, + } + if cmd.val.Results != nil { + val.Results = make([]map[string]interface{}, len(cmd.val.Results)) + for i, result := range cmd.val.Results { + val.Results[i] = make(map[string]interface{}, len(result)) + for k, v := range result { + val.Results[i][k] = v + } + } + } + if cmd.val.Warnings != nil { + val.Warnings = slices.Clone(cmd.val.Warnings) + } + + var cursorVal *FTHybridCursorResult + if cmd.cursorVal != nil { + cursorVal = &FTHybridCursorResult{ + SearchCursorID: cmd.cursorVal.SearchCursorID, + VsimCursorID: cmd.cursorVal.VsimCursorID, + } + } + + var options *FTHybridOptions + if cmd.options != nil { + options = &FTHybridOptions{ + CountExpressions: cmd.options.CountExpressions, + Load: cmd.options.Load, + Filter: cmd.options.Filter, + LimitOffset: cmd.options.LimitOffset, + Limit: cmd.options.Limit, + ExplainScore: cmd.options.ExplainScore, + Timeout: cmd.options.Timeout, + WithCursor: cmd.options.WithCursor, + } + // Clone slices and maps + if cmd.options.SearchExpressions != nil { + options.SearchExpressions = make([]FTHybridSearchExpression, len(cmd.options.SearchExpressions)) + copy(options.SearchExpressions, cmd.options.SearchExpressions) + } + if cmd.options.VectorExpressions != nil { + options.VectorExpressions = make([]FTHybridVectorExpression, len(cmd.options.VectorExpressions)) + copy(options.VectorExpressions, cmd.options.VectorExpressions) + } + if cmd.options.Combine != nil { + options.Combine = &FTHybridCombineOptions{ + Method: cmd.options.Combine.Method, + Count: cmd.options.Combine.Count, + Window: cmd.options.Combine.Window, + Constant: cmd.options.Combine.Constant, + Alpha: cmd.options.Combine.Alpha, + Beta: cmd.options.Combine.Beta, + YieldScoreAs: cmd.options.Combine.YieldScoreAs, + } + } + if cmd.options.GroupBy != nil { + options.GroupBy = &FTHybridGroupBy{ + Count: cmd.options.GroupBy.Count, + ReduceFunc: cmd.options.GroupBy.ReduceFunc, + ReduceCount: cmd.options.GroupBy.ReduceCount, + } + if cmd.options.GroupBy.Fields != nil { + options.GroupBy.Fields = make([]string, len(cmd.options.GroupBy.Fields)) + copy(options.GroupBy.Fields, cmd.options.GroupBy.Fields) + } + if cmd.options.GroupBy.ReduceParams != nil { + options.GroupBy.ReduceParams = make([]interface{}, len(cmd.options.GroupBy.ReduceParams)) + copy(options.GroupBy.ReduceParams, cmd.options.GroupBy.ReduceParams) + } + } + if cmd.options.Apply != nil { + options.Apply = make([]FTHybridApply, len(cmd.options.Apply)) + copy(options.Apply, cmd.options.Apply) + } + if cmd.options.SortBy != nil { + options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy)) + copy(options.SortBy, cmd.options.SortBy) + } + if cmd.options.Params != nil { + options.Params = make(map[string]interface{}, len(cmd.options.Params)) + for k, v := range cmd.options.Params { + options.Params[k] = v + } + } + if cmd.options.WithCursorOptions != nil { + options.WithCursorOptions = &FTHybridWithCursor{ + MaxIdle: cmd.options.WithCursorOptions.MaxIdle, + Count: cmd.options.WithCursorOptions.Count, + } + } + } + + return &FTHybridCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + cursorVal: cursorVal, + options: options, + withCursor: cmd.withCursor, + } +} + // FTSearch - Executes a search query on an index. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. // For more information, please refer to the Redis documentation about [FT.SEARCH]. @@ -1751,7 +3213,7 @@ type SearchQuery []interface{} // For more information, please refer to the Redis documentation about [FT.SEARCH]. // // [FT.SEARCH]: (https://redis.io/commands/ft.search/) -func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { +func FTSearchQuery(query string, options *FTSearchOptions) (SearchQuery, error) { queryArgs := []interface{}{query} if options != nil { if options.NoContent { @@ -1831,7 +3293,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { for _, sortBy := range options.SortBy { queryArgs = append(queryArgs, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + return nil, fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive") } if sortBy.Asc { queryArgs = append(queryArgs, "ASC") @@ -1859,7 +3321,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } // FTSearchWithArgs - Executes a search query on an index with additional options. @@ -1948,7 +3410,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin for _, sortBy := range options.SortBy { args = append(args, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + cmd := newFTSearchCmd(ctx, options, args...) + cmd.SetErr(fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive")) + return cmd } if sortBy.Asc { args = append(args, "ASC") @@ -1988,8 +3452,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd { return &FTSynDumpCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeFTSynDump, }, } } @@ -2019,6 +3484,30 @@ func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) { } func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { + readType, err := rd.PeekReplyType() + if err != nil { + return err + } + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Parse the raw response into structured result + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + cmd.val, err = parseFTSynDumpRESP3(rawMap) + return err + } + + // RESP2 format termSynonymPairs, err := rd.ReadSlice() if err != nil { return err @@ -2055,6 +3544,64 @@ func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { return nil } +// parseFTSynDumpRESP3 parses the RESP3 format response from FT.SYNDUMP. +// RESP3 format: +// +// map{ +// "term1": ["synonym_group_id1", ...], +// "term2": ["synonym_group_id2", ...], +// ... +// } +func parseFTSynDumpRESP3(data map[interface{}]interface{}) ([]FTSynDumpResult, error) { + results := make([]FTSynDumpResult, 0, len(data)) + + for termKey, synonymsData := range data { + term, ok := termKey.(string) + if !ok { + continue + } + + synonymsArray, ok := synonymsData.([]interface{}) + if !ok { + continue + } + + synonymList := make([]string, 0, len(synonymsArray)) + for _, syn := range synonymsArray { + if synonym, ok := syn.(string); ok { + synonymList = append(synonymList, synonym) + } + } + + results = append(results, FTSynDumpResult{ + Term: term, + Synonyms: synonymList, + }) + } + + return results, nil +} + +func (cmd *FTSynDumpCmd) Clone() Cmder { + var val []FTSynDumpResult + if cmd.val != nil { + val = make([]FTSynDumpResult, len(cmd.val)) + for i, result := range cmd.val { + val[i] = FTSynDumpResult{ + Term: result.Term, + } + if result.Synonyms != nil { + val[i].Synonyms = make([]string, len(result.Synonyms)) + copy(val[i].Synonyms, result.Synonyms) + } + } + } + return &FTSynDumpCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // FTSynDump - Dumps the contents of a synonym group. // The 'index' parameter specifies the index to dump. // For more information, please refer to the Redis documentation: @@ -2101,3 +3648,289 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str _ = c(ctx, cmd) return cmd } + +// FTHybrid - Executes a hybrid search combining full-text search and vector similarity +// The 'index' parameter specifies the index to search, 'searchExpr' is the search query, +// 'vectorField' is the name of the vector field, and 'vectorData' is the vector to search with. +// FTHybrid is still experimental, the command behaviour and signature may change +func (c cmdable) FTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd { + options := &FTHybridOptions{ + CountExpressions: 2, + SearchExpressions: []FTHybridSearchExpression{ + {Query: searchExpr}, + }, + VectorExpressions: []FTHybridVectorExpression{ + {VectorField: vectorField, VectorData: vectorData}, + }, + } + return c.FTHybridWithArgs(ctx, index, options) +} + +func hybridVectorBlob(v Vector) (interface{}, error) { + if v == nil { + return nil, fmt.Errorf("FT.HYBRID: vector data is required") + } + + switch vector := v.(type) { + case *VectorFP32: + return hybridVectorBytes(vector.Val) + case *VectorFloat16: + return hybridVectorBytes(vector.Val) + case *VectorBFloat16: + return hybridVectorBytes(vector.Val) + case *VectorFloat64: + return hybridVectorBytes(vector.Val) + case *VectorInt8: + return hybridVectorBytes(vector.Val) + case *VectorUint8: + return hybridVectorBytes(vector.Val) + case *VectorValues, *VectorRef: + return nil, fmt.Errorf("FT.HYBRID: unsupported vector type %T", v) + default: + values := v.Value() + if len(values) < 2 { + return nil, fmt.Errorf("FT.HYBRID: vector Value must contain a blob at index 1") + } + return values[1], nil + } +} + +func hybridVectorBytes(blob []byte) ([]byte, error) { + if len(blob) == 0 { + return nil, fmt.Errorf("FT.HYBRID: vector blob is required") + } + return blob, nil +} + +// generateVectorParamName returns a parameter name that is not already present +// in params. It is used to pass vector data via the PARAMS mechanism when the +// caller does not provide a VectorParamName, since inline vector blobs are no +// longer supported by Redis. +func generateVectorParamName(params map[string]interface{}) string { + for i := 0; ; i++ { + name := fmt.Sprintf("__vector_param_%d", i) + if _, ok := params[name]; !ok { + return name + } + } +} + +// FTHybridWithArgs - Executes a hybrid search with advanced options +// FTHybridWithArgs is still experimental, the command behaviour and signature may change +// +// Vector data is always sent through the PARAMS mechanism, because inline vector +// blobs are no longer supported by Redis. For every vector expression whose +// VectorParamName is empty, a unique name is generated (e.g. "__vector_param_0") +// and the corresponding blob is passed via PARAMS. +// +// options.Params is never mutated: the command is built from a local copy that +// combines the caller-provided params with any generated vector parameters. This +// makes it safe to reuse the same *FTHybridOptions across multiple calls. Generated +// names are also reserved against all explicit VectorParamName values, so they never +// collide with explicit names (even those following the "__vector_param_N" pattern). +func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd { + args := []interface{}{"FT.HYBRID", index} + + if options != nil { + // Add search expressions + for _, searchExpr := range options.SearchExpressions { + args = append(args, "SEARCH", searchExpr.Query) + + if searchExpr.Scorer != "" { + args = append(args, "SCORER", searchExpr.Scorer) + if len(searchExpr.ScorerParams) > 0 { + args = append(args, searchExpr.ScorerParams...) + } + } + + if searchExpr.YieldScoreAs != "" { + args = append(args, "YIELD_SCORE_AS", searchExpr.YieldScoreAs) + } + } + + // Vector data is always passed via the PARAMS mechanism (inline vector blobs + // are no longer supported by Redis). When vectors are present, build a local + // copy of the caller-provided params so options.Params is never mutated, and + // pre-reserve any explicit VectorParamName values so generated names never + // collide with them. + params := options.Params + if len(options.VectorExpressions) > 0 { + params = make(map[string]interface{}, len(options.Params)+len(options.VectorExpressions)) + for k, v := range options.Params { + params[k] = v + } + for _, vectorExpr := range options.VectorExpressions { + if vectorExpr.VectorParamName != "" { + params[vectorExpr.VectorParamName] = nil + } + } + } + // Add vector expressions + for _, vectorExpr := range options.VectorExpressions { + args = append(args, "VSIM", "@"+vectorExpr.VectorField) + + vectorBlob, err := hybridVectorBlob(vectorExpr.VectorData) + if err != nil { + cmd := newFTHybridCmd(ctx, options, args...) + cmd.SetErr(err) + return cmd + } + + // When VectorParamName is not provided, generate a unique name. Generated + // names are tracked only in the local params map, never written back to + // options.Params. + paramName := vectorExpr.VectorParamName + if paramName == "" { + paramName = generateVectorParamName(params) + } + args = append(args, "$"+paramName) + params[paramName] = vectorBlob + + if vectorExpr.Method != "" { + args = append(args, vectorExpr.Method) + if len(vectorExpr.MethodParams) > 0 { + // MethodParams should be key-value pairs, count them + args = append(args, len(vectorExpr.MethodParams)) + args = append(args, vectorExpr.MethodParams...) + } + } + + if vectorExpr.Filter != "" { + args = append(args, "FILTER", vectorExpr.Filter) + } + + if vectorExpr.YieldScoreAs != "" { + args = append(args, "YIELD_SCORE_AS", vectorExpr.YieldScoreAs) + } + } + + // Add combine/fusion options + if options.Combine != nil { + // Build combine parameters + combineParams := []interface{}{} + + switch options.Combine.Method { + case FTHybridCombineRRF: + if options.Combine.Window > 0 { + combineParams = append(combineParams, "WINDOW", options.Combine.Window) + } + if options.Combine.Constant > 0 { + combineParams = append(combineParams, "CONSTANT", options.Combine.Constant) + } + case FTHybridCombineLinear: + if options.Combine.Alpha > 0 { + combineParams = append(combineParams, "ALPHA", options.Combine.Alpha) + } + if options.Combine.Beta > 0 { + combineParams = append(combineParams, "BETA", options.Combine.Beta) + } + } + + if options.Combine.YieldScoreAs != "" { + combineParams = append(combineParams, "YIELD_SCORE_AS", options.Combine.YieldScoreAs) + } + + // Add COMBINE with method and parameter count + args = append(args, "COMBINE", string(options.Combine.Method)) + if len(combineParams) > 0 { + args = append(args, len(combineParams)) + args = append(args, combineParams...) + } + } + + // Add LOAD (projected fields) + if len(options.Load) > 0 { + args = append(args, "LOAD", len(options.Load)) + for _, field := range options.Load { + args = append(args, field) + } + } + + // Add GROUPBY + if options.GroupBy != nil { + args = append(args, "GROUPBY", options.GroupBy.Count) + for _, field := range options.GroupBy.Fields { + args = append(args, field) + } + if options.GroupBy.ReduceFunc != "" { + args = append(args, "REDUCE", options.GroupBy.ReduceFunc, options.GroupBy.ReduceCount) + args = append(args, options.GroupBy.ReduceParams...) + } + } + + // Add APPLY transformations + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Expression, "AS", apply.AsField) + } + + // Add SORTBY + if len(options.SortBy) > 0 { + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + cmd := newFTHybridCmd(ctx, options, args...) + cmd.SetErr(fmt.Errorf("FT.HYBRID: ASC and DESC are mutually exclusive")) + return cmd + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, "SORTBY", len(sortByOptions)) + args = append(args, sortByOptions...) + } + + // Add FILTER (post-filter) + if options.Filter != "" { + args = append(args, "FILTER", options.Filter) + } + + // Add LIMIT + if options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) + } + + // Add PARAMS + // Emit from the local params map, which contains the caller-provided params + // plus any generated vector parameter names. options.Params is left untouched. + if len(params) > 0 { + args = append(args, "PARAMS", len(params)*2) + for key, value := range params { + // PARAMS entries are passed without a '$' prefix; they are referenced in + // the query and clauses using "$". + args = append(args, key, value) + } + } + + // Add EXPLAINSCORE + if options.ExplainScore { + args = append(args, "EXPLAINSCORE") + } + + // Add TIMEOUT + if options.Timeout > 0 { + args = append(args, "TIMEOUT", options.Timeout) + } + + // Add WITHCURSOR support + if options.WithCursor { + args = append(args, "WITHCURSOR") + if options.WithCursorOptions != nil { + if options.WithCursorOptions.Count > 0 { + args = append(args, "COUNT", options.WithCursorOptions.Count) + } + if options.WithCursorOptions.MaxIdle > 0 { + args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle) + } + } + } + } + + cmd := newFTHybridCmd(ctx, options, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/sentinel.go b/vendor/github.com/redis/go-redis/v9/sentinel.go index 43fbcd24431..055b3101f2f 100644 --- a/vendor/github.com/redis/go-redis/v9/sentinel.go +++ b/vendor/github.com/redis/go-redis/v9/sentinel.go @@ -5,16 +5,20 @@ import ( "crypto/tls" "errors" "fmt" + "math/rand" "net" "net/url" + "slices" "strconv" "strings" "sync" "time" + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/pool" - "github.com/redis/go-redis/v9/internal/rand" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/redis/go-redis/v9/push" ) //------------------------------------------------------------------------------ @@ -60,26 +64,80 @@ type FailoverOptions struct { Protocol int Username string Password string - DB int + + // Push notifications are always enabled for RESP3 connections + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + DB int MaxRetries int MinRetryBackoff time.Duration MaxRetryBackoff time.Duration - DialTimeout time.Duration + DialTimeout time.Duration + + // DialerRetries is the maximum number of retry attempts when dialing fails. + // + // default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // + // default: 100 milliseconds + DialerRetryTimeout time.Duration + + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + WriteBufferSize int + PoolFIFO bool - PoolSize int - PoolTimeout time.Duration - MinIdleConns int - MaxIdleConns int - MaxActiveConns int - ConnMaxIdleTime time.Duration - ConnMaxLifetime time.Duration + PoolSize int + + // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines. + // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize. + MaxConcurrentDials int + + PoolTimeout time.Duration + MinIdleConns int + MaxIdleConns int + MaxActiveConns int + ConnMaxIdleTime time.Duration + ConnMaxLifetime time.Duration + ConnMaxLifetimeJitter time.Duration TLSConfig *tls.Config @@ -96,7 +154,29 @@ type FailoverOptions struct { DisableIdentity bool IdentitySuffix string - UnstableResp3 bool + + // FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing. + // When a node is marked as failing, it will be avoided for this duration. + // Only applies to failover cluster clients. Default is 15 seconds. + FailingTimeoutSeconds int + + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. + UnstableResp3 bool + + // PushNotificationProcessor is the processor for handling push notifications. + // If nil, a default processor will be created for RESP3 connections. + PushNotificationProcessor push.NotificationProcessor + + // MaintNotificationsConfig is not supported for FailoverClients at the moment + // MaintNotificationsConfig provides custom configuration for maintnotifications upgrades. + // When MaintNotificationsConfig.Mode is not "disabled", the client will handle + // upgrade notifications gracefully and manage connection/pool state transitions + // seamlessly. Requires Protocol: 3 (RESP3) for push notifications. + // If nil, maintnotifications upgrades are disabled. + // (however if Mode is nil, it defaults to "auto" - enable if server supports it) + //MaintNotificationsConfig *maintnotifications.Config } func (opt *FailoverOptions) clientOptions() *Options { @@ -107,36 +187,53 @@ func (opt *FailoverOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - DB: opt.DB, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, + DB: opt.DB, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, MaxRetries: opt.MaxRetries, MinRetryBackoff: opt.MinRetryBackoff, MaxRetryBackoff: opt.MaxRetryBackoff, - DialTimeout: opt.DialTimeout, - ReadTimeout: opt.ReadTimeout, - WriteTimeout: opt.WriteTimeout, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, + + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + ContextTimeoutEnabled: opt.ContextTimeoutEnabled, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + MaxActiveConns: opt.MaxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, TLSConfig: opt.TLSConfig, DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, + PushNotificationProcessor: opt.PushNotificationProcessor, + + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeDisabled, + }, } } @@ -156,27 +253,42 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { MinRetryBackoff: opt.MinRetryBackoff, MaxRetryBackoff: opt.MaxRetryBackoff, - DialTimeout: opt.DialTimeout, - ReadTimeout: opt.ReadTimeout, - WriteTimeout: opt.WriteTimeout, + // The sentinel client uses a 4KiB read/write buffer size. + ReadBufferSize: 4096, + WriteBufferSize: 4096, + + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + ContextTimeoutEnabled: opt.ContextTimeoutEnabled, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + MaxActiveConns: opt.MaxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter, TLSConfig: opt.TLSConfig, DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, + PushNotificationProcessor: opt.PushNotificationProcessor, + + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeDisabled, + }, } } @@ -187,38 +299,55 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, MaxRedirects: opt.MaxRetries, + ReadOnly: opt.ReplicaOnly, RouteByLatency: opt.RouteByLatency, RouteRandomly: opt.RouteRandomly, MinRetryBackoff: opt.MinRetryBackoff, MaxRetryBackoff: opt.MaxRetryBackoff, - DialTimeout: opt.DialTimeout, - ReadTimeout: opt.ReadTimeout, - WriteTimeout: opt.WriteTimeout, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, + + DialTimeout: opt.DialTimeout, + DialerRetries: opt.DialerRetries, + DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + ContextTimeoutEnabled: opt.ContextTimeoutEnabled, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - MaxActiveConns: opt.MaxActiveConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + MaxConcurrentDials: opt.MaxConcurrentDials, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + MaxActiveConns: opt.MaxActiveConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, TLSConfig: opt.TLSConfig, - DisableIdentity: opt.DisableIdentity, - DisableIndentity: opt.DisableIndentity, + DisableIdentity: opt.DisableIdentity, + DisableIndentity: opt.DisableIndentity, + IdentitySuffix: opt.IdentitySuffix, + FailingTimeoutSeconds: opt.FailingTimeoutSeconds, + PushNotificationProcessor: opt.PushNotificationProcessor, - IdentitySuffix: opt.IdentitySuffix, + MaintNotificationsConfig: &maintnotifications.Config{ + Mode: maintnotifications.ModeDisabled, + }, } } @@ -247,6 +376,7 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { // URL attributes (scheme, host, userinfo, resp.), query parameters using these // names will be treated as unknown parameters // - unknown parameter names will result in an error +// - use "skip_verify=true" to ignore TLS certificate validation // // Example: // @@ -317,15 +447,21 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, o.MinRetryBackoff = q.duration("min_retry_backoff") o.MaxRetryBackoff = q.duration("max_retry_backoff") o.DialTimeout = q.duration("dial_timeout") + o.DialerRetries = q.int("dialer_retries") + o.DialerRetryTimeout = q.duration("dialer_retry_timeout") o.ReadTimeout = q.duration("read_timeout") o.WriteTimeout = q.duration("write_timeout") o.ContextTimeoutEnabled = q.bool("context_timeout_enabled") o.PoolFIFO = q.bool("pool_fifo") o.PoolSize = q.int("pool_size") + o.MaxConcurrentDials = q.int("max_concurrent_dials") o.MinIdleConns = q.int("min_idle_conns") o.MaxIdleConns = q.int("max_idle_conns") o.MaxActiveConns = q.int("max_active_conns") o.ConnMaxLifetime = q.duration("conn_max_lifetime") + if q.has("conn_max_lifetime_jitter") { + o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime) + } o.ConnMaxIdleTime = q.duration("conn_max_idle_time") o.PoolTimeout = q.duration("pool_timeout") o.DisableIdentity = q.bool("disableIdentity") @@ -354,6 +490,10 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p)) } + if o.TLSConfig != nil && q.has("skip_verify") { + o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify") + } + // any parameters left? if r := q.remaining(); len(r) > 0 { return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", ")) @@ -365,6 +505,7 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, // NewFailoverClient returns a Redis client that uses Redis Sentinel // for automatic failover. It's safe for concurrent use by multiple // goroutines. +// Passing nil FailoverOptions will cause a panic. func NewFailoverClient(failoverOpt *FailoverOptions) *Client { if failoverOpt == nil { panic("redis: NewFailoverClient nil options") @@ -393,24 +534,42 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client { opt.Dialer = masterReplicaDialer(failover) opt.init() - var connPool *pool.ConnPool - rdb := &Client{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } rdb.init() - connPool = newConnPool(opt, rdb.dialHook) - rdb.connPool = connPool - rdb.onClose = rdb.wrappedOnClose(failover.Close) + // Initialize push notification processor using shared helper + // Use void processor by default for RESP2 connections + rdb.pushProcessor = initializePushProcessor(opt) + + // Generate unique pool names for metrics + uniqueID := generateUniqueID() + mainPoolName := opt.Addr + "_" + uniqueID + pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub" + + var err error + rdb.connPool, err = newConnPool(opt, rdb.dialHook, mainPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create connection pool: %w", err)) + } + rdb.pubSubPool, err = newPubSubPool(opt, rdb.dialHook, pubsubPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err)) + } + + rdb.onClose.register(onCloseHookIDSentinelFailover, failover.Close) failover.mu.Lock() failover.onFailover = func(ctx context.Context, addr string) { - _ = connPool.Filter(func(cn *pool.Conn) bool { - return cn.RemoteAddr().String() != addr - }) + if connPool, ok := rdb.connPool.(*pool.ConnPool); ok { + _ = connPool.Filter(func(cn *pool.Conn) bool { + return cn.RemoteAddr().String() != addr + }) + } } failover.mu.Unlock() @@ -457,6 +616,8 @@ type SentinelClient struct { *baseClient } +// NewSentinelClient returns a Redis Sentinel client. +// Passing nil Options will cause a panic. func NewSentinelClient(opt *Options) *SentinelClient { if opt == nil { panic("redis: NewSentinelClient nil options") @@ -464,19 +625,51 @@ func NewSentinelClient(opt *Options) *SentinelClient { opt.init() c := &SentinelClient{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } + // Initialize push notification processor using shared helper + // Use void processor for Sentinel clients + c.pushProcessor = NewVoidPushNotificationProcessor() + c.initHooks(hooks{ dial: c.baseClient.dial, process: c.baseClient.process, }) - c.connPool = newConnPool(opt, c.dialHook) + + // Generate unique pool names for metrics + uniqueID := generateUniqueID() + mainPoolName := opt.Addr + "_" + uniqueID + pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub" + + var err error + c.connPool, err = newConnPool(opt, c.dialHook, mainPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create connection pool: %w", err)) + } + c.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName) + if err != nil { + panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err)) + } return c } +// GetPushNotificationHandler returns the handler for a specific push notification name. +// Returns nil if no handler is registered for the given name. +func (c *SentinelClient) GetPushNotificationHandler(pushNotificationName string) push.NotificationHandler { + return c.pushProcessor.GetHandler(pushNotificationName) +} + +// RegisterPushNotificationHandler registers a handler for a specific push notification name. +// Returns an error if a handler is already registered for this push notification name. +// If protected is true, the handler cannot be unregistered. +func (c *SentinelClient) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error { + return c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected) +} + func (c *SentinelClient) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) @@ -486,13 +679,31 @@ func (c *SentinelClient) Process(ctx context.Context, cmd Cmder) error { func (c *SentinelClient) pubSub() *PubSub { pubsub := &PubSub{ opt: c.opt, - - newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) { - return c.newConn(ctx) + newConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) { + cn, err := c.pubSubPool.NewConn(ctx, c.opt.Network, addr, channels) + if err != nil { + return nil, err + } + // will return nil if already initialized + err = c.initConn(ctx, cn) + if err != nil { + _ = cn.Close() + return nil, err + } + // Track connection in PubSubPool + c.pubSubPool.TrackConn(cn) + return cn, nil }, - closeConn: c.connPool.CloseConn, + closeConn: func(cn *pool.Conn) error { + // Untrack connection from PubSubPool + c.pubSubPool.UntrackConn(cn) + _ = cn.Close() + return nil + }, + pushProcessor: c.pushProcessor, } pubsub.init() + return pubsub } @@ -627,10 +838,10 @@ type sentinelFailover struct { onFailover func(ctx context.Context, addr string) onUpdate func(ctx context.Context) - mu sync.RWMutex - _masterAddr string - sentinel *SentinelClient - pubsub *PubSub + mu sync.RWMutex + masterAddr string + sentinel *SentinelClient + pubsub *PubSub } func (c *sentinelFailover) Close() error { @@ -686,7 +897,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { if sentinel != nil { addr, err := c.getMasterAddr(ctx, sentinel) if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if isContextError(ctx.Err()) { return "", err } // Continue on other errors @@ -704,7 +915,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { addr, err := c.getMasterAddr(ctx, c.sentinel) if err != nil { _ = c.closeSentinel() - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if isContextError(ctx.Err()) { return "", err } // Continue on other errors @@ -715,6 +926,11 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { } } + // short circuit if no sentinels configured + if len(c.sentinelAddrs) == 0 { + return "", errors.New("redis: no sentinels configured") + } + var ( masterAddr string wg sync.WaitGroup @@ -738,6 +954,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { errCh <- err return } + once.Do(func() { masterAddr = net.JoinHostPort(addrVal[0], addrVal[1]) // Push working sentinel to the top @@ -746,6 +963,10 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { internal.Logger.Printf(ctx, "sentinel: selected addr=%s masterAddr=%s", addr, masterAddr) cancel() }) + + if sentinelCli != c.sentinel { + _ = sentinelCli.Close() + } }(i, sentinelAddr) } @@ -769,7 +990,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo if sentinel != nil { addrs, err := c.getReplicaAddrs(ctx, sentinel) if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if isContextError(ctx.Err()) { return nil, err } // Continue on other errors @@ -787,7 +1008,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo addrs, err := c.getReplicaAddrs(ctx, c.sentinel) if err != nil { _ = c.closeSentinel() - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if isContextError(ctx.Err()) { return nil, err } // Continue on other errors @@ -795,8 +1016,16 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo c.opt.MasterName, err) } else if len(addrs) > 0 { return addrs, nil + } else if !useDisconnected { + // No error and no replicas — valid steady state for master-only setups. + // Preserve the sentinel connection for master discovery and failover + // pub/sub monitoring. Only return early when useDisconnected is false; + // when true, fall through to the discovery loop which passes + // useDisconnected to parseReplicaAddrs (getReplicaAddrs hardcodes false). + return []string{}, nil } else { - // No error and no replicas. + // useDisconnected=true: close sentinel so the discovery loop can call + // setSentinel if it finds disconnected replicas. _ = c.closeSentinel() } } @@ -809,7 +1038,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo replicas, err := sentinel.Replicas(ctx, c.opt.MasterName).Result() if err != nil { _ = sentinel.Close() - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if isContextError(ctx.Err()) { return nil, err } internal.Logger.Printf(ctx, "sentinel: Replicas master=%q failed: %s", @@ -829,9 +1058,9 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo } if sentinelReachable { - return []string{}, nil + return nil, nil } - return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable") + return nil, errors.New("redis: all sentinels specified in configuration are unreachable") } func (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) (string, error) { @@ -878,7 +1107,7 @@ func parseReplicaAddrs(addrs []map[string]string, keepDisconnected bool) []strin func (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) { c.mu.RLock() - currentAddr := c._masterAddr //nolint:ifshort + currentAddr := c.masterAddr //nolint:ifshort c.mu.RUnlock() if addr == currentAddr { @@ -888,10 +1117,10 @@ func (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) { c.mu.Lock() defer c.mu.Unlock() - if addr == c._masterAddr { + if addr == c.masterAddr { return } - c._masterAddr = addr + c.masterAddr = addr internal.Logger.Printf(ctx, "sentinel: new master=%q addr=%q", c.opt.MasterName, addr) @@ -928,7 +1157,7 @@ func (c *sentinelFailover) discoverSentinels(ctx context.Context) { } if ip != "" && port != "" { sentinelAddr := net.JoinHostPort(ip, port) - if !contains(c.sentinelAddrs, sentinelAddr) { + if !slices.Contains(c.sentinelAddrs, sentinelAddr) { internal.Logger.Printf(ctx, "sentinel: discovered new sentinel=%q for master=%q", sentinelAddr, c.opt.MasterName) c.sentinelAddrs = append(c.sentinelAddrs, sentinelAddr) @@ -962,19 +1191,11 @@ func (c *sentinelFailover) listen(pubsub *PubSub) { } } -func contains(slice []string, str string) bool { - for _, s := range slice { - if s == str { - return true - } - } - return false -} - //------------------------------------------------------------------------------ // NewFailoverClusterClient returns a client that supports routing read-only commands // to a replica node. +// Passing nil FailoverOptions will cause a panic. func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient { if failoverOpt == nil { panic("redis: NewFailoverClusterClient nil options") diff --git a/vendor/github.com/redis/go-redis/v9/set_commands.go b/vendor/github.com/redis/go-redis/v9/set_commands.go index cef8ad6d8b6..2a465728b70 100644 --- a/vendor/github.com/redis/go-redis/v9/set_commands.go +++ b/vendor/github.com/redis/go-redis/v9/set_commands.go @@ -1,7 +1,13 @@ package redis -import "context" +import ( + "context" + "github.com/redis/go-redis/v9/internal/hashtag" +) + +// SetCmdable is an interface for Redis set commands. +// Sets are unordered collections of unique strings. type SetCmdable interface { SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd SCard(ctx context.Context, key string) *IntCmd @@ -25,8 +31,12 @@ type SetCmdable interface { SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd } -//------------------------------------------------------------------------------ - +// Returns the number of elements that were added to the set, not including all +// the elements already present in the set. +// +// For more information about the command please refer to [SADD]. +// +// [SADD]: (https://redis.io/docs/latest/commands/sadd/) func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "sadd" @@ -37,12 +47,25 @@ func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) * return cmd } +// Returns the set cardinality (number of elements) of the set stored at key. +// Returns 0 if key does not exist. +// +// For more information about the command please refer to [SCARD]. +// +// [SCARD]: (https://redis.io/docs/latest/commands/scard/) func (c cmdable) SCard(ctx context.Context, key string) *IntCmd { cmd := NewIntCmd(ctx, "scard", key) _ = c(ctx, cmd) return cmd } +// Returns the members of the set resulting from the difference between the first set +// and all the successive sets. +// Keys that do not exist are considered to be empty sets. +// +// For more information about the command please refer to [SDIFF]. +// +// [SDIFF]: (https://redis.io/docs/latest/commands/sdiff/) func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sdiff" @@ -54,6 +77,13 @@ func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd { return cmd } +// Stores the members of the set resulting from the difference between the first set +// and all the successive sets into destination. +// If destination already exists, it is overwritten. +// +// For more information about the command please refer to [SDIFFSTORE]. +// +// [SDIFFSTORE]: (https://redis.io/docs/latest/commands/sdiffstore/) func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sdiffstore" @@ -66,6 +96,13 @@ func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...str return cmd } +// Returns the members of the set resulting from the intersection of all the given sets. +// Keys that do not exist are considered to be empty sets. +// With one of the keys being an empty set, the resulting set is also empty. +// +// For more information about the command please refer to [SINTER]. +// +// [SINTER]: (https://redis.io/docs/latest/commands/sinter/) func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sinter" @@ -77,22 +114,38 @@ func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd { return cmd } +// Returns the cardinality of the set resulting from the intersection of all the given sets. +// Keys that do not exist are considered to be empty sets. +// With one of the keys being an empty set, the resulting set is also empty. +// +// The limit parameter sets an upper bound on the number of results returned. +// If limit is 0, no limit is applied. +// +// For more information about the command please refer to [SINTERCARD]. +// +// [SINTERCARD]: (https://redis.io/docs/latest/commands/sintercard/) func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd { - args := make([]interface{}, 4+len(keys)) + numKeys := len(keys) + args := make([]interface{}, 4+numKeys) args[0] = "sintercard" - numkeys := int64(0) + args[1] = numKeys for i, key := range keys { args[2+i] = key - numkeys++ } - args[1] = numkeys - args[2+numkeys] = "limit" - args[3+numkeys] = limit + args[2+numKeys] = "limit" + args[3+numKeys] = limit cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) return cmd } +// Stores the members of the set resulting from the intersection of all the given sets +// into destination. +// If destination already exists, it is overwritten. +// +// For more information about the command please refer to [SINTERSTORE]. +// +// [SINTERSTORE]: (https://redis.io/docs/latest/commands/sinterstore/) func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sinterstore" @@ -105,13 +158,26 @@ func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...st return cmd } +// Returns if member is a member of the set stored at key. +// Returns true if the element is a member of the set, false if it is not a member +// or if key does not exist. +// +// For more information about the command please refer to [SISMEMBER]. +// +// [SISMEMBER]: (https://redis.io/docs/latest/commands/sismember/) func (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd { cmd := NewBoolCmd(ctx, "sismember", key, member) _ = c(ctx, cmd) return cmd } -// SMIsMember Redis `SMISMEMBER key member [member ...]` command. +// Returns whether each member is a member of the set stored at key. +// For each member, returns true if the element is a member of the set, false if it is not +// a member or if key does not exist. +// +// For more information about the command please refer to [SMISMEMBER]. +// +// [SMISMEMBER]: (https://redis.io/docs/latest/commands/smismember/) func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "smismember" @@ -122,54 +188,100 @@ func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interfac return cmd } -// SMembers Redis `SMEMBERS key` command output as a slice. +// Returns all the members of the set value stored at key. +// Returns an empty slice if key does not exist. +// +// For more information about the command please refer to [SMEMBERS]. +// +// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/) func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "smembers", key) _ = c(ctx, cmd) return cmd } -// SMembersMap Redis `SMEMBERS key` command output as a map. +// Returns all the members of the set value stored at key as a map. +// Returns an empty map if key does not exist. +// +// For more information about the command please refer to [SMEMBERS]. +// +// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/) func (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd { cmd := NewStringStructMapCmd(ctx, "smembers", key) _ = c(ctx, cmd) return cmd } +// Moves member from the set at source to the set at destination. +// This operation is atomic. In every given moment the element will appear to be a member +// of source or destination for other clients. +// +// For more information about the command please refer to [SMOVE]. +// +// [SMOVE]: (https://redis.io/docs/latest/commands/smove/) func (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd { cmd := NewBoolCmd(ctx, "smove", source, destination, member) _ = c(ctx, cmd) return cmd } -// SPop Redis `SPOP key` command. +// Removes and returns one or more random members from the set value stored at key. +// This version returns a single random member. +// +// For more information about the command please refer to [SPOP]. +// +// [SPOP]: (https://redis.io/docs/latest/commands/spop/) func (c cmdable) SPop(ctx context.Context, key string) *StringCmd { cmd := NewStringCmd(ctx, "spop", key) _ = c(ctx, cmd) return cmd } -// SPopN Redis `SPOP key count` command. +// Removes and returns one or more random members from the set value stored at key. +// This version returns up to count random members. +// +// For more information about the command please refer to [SPOP]. +// +// [SPOP]: (https://redis.io/docs/latest/commands/spop/) func (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "spop", key, count) _ = c(ctx, cmd) return cmd } -// SRandMember Redis `SRANDMEMBER key` command. +// Returns a random member from the set value stored at key. +// This version returns a single random member without removing it. +// +// For more information about the command please refer to [SRANDMEMBER]. +// +// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/) func (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd { cmd := NewStringCmd(ctx, "srandmember", key) _ = c(ctx, cmd) return cmd } -// SRandMemberN Redis `SRANDMEMBER key count` command. +// Returns an array of random members from the set value stored at key. +// This version returns up to count random members without removing them. +// When called with a positive count, returns distinct elements. +// When called with a negative count, allows for repeated elements. +// +// For more information about the command please refer to [SRANDMEMBER]. +// +// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/) func (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "srandmember", key, count) _ = c(ctx, cmd) return cmd } +// Removes the specified members from the set stored at key. +// Specified members that are not a member of this set are ignored. +// If key does not exist, it is treated as an empty set and this command returns 0. +// +// For more information about the command please refer to [SREM]. +// +// [SREM]: (https://redis.io/docs/latest/commands/srem/) func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "srem" @@ -180,6 +292,12 @@ func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) * return cmd } +// Returns the members of the set resulting from the union of all the given sets. +// Keys that do not exist are considered to be empty sets. +// +// For more information about the command please refer to [SUNION]. +// +// [SUNION]: (https://redis.io/docs/latest/commands/sunion/) func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sunion" @@ -191,6 +309,13 @@ func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd { return cmd } +// Stores the members of the set resulting from the union of all the given sets +// into destination. +// If destination already exists, it is overwritten. +// +// For more information about the command please refer to [SUNIONSTORE]. +// +// [SUNIONSTORE]: (https://redis.io/docs/latest/commands/sunionstore/) func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sunionstore" @@ -203,6 +328,17 @@ func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...st return cmd } +// Incrementally iterates the set elements stored at key. +// This is a cursor-based iterator that allows scanning large sets efficiently. +// +// Parameters: +// - cursor: The cursor value for the iteration (use 0 to start a new scan) +// - match: Optional pattern to match elements (empty string means no pattern) +// - count: Optional hint about how many elements to return per iteration +// +// For more information about the command please refer to [SSCAN]. +// +// [SSCAN]: (https://redis.io/docs/latest/commands/sscan/) func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"sscan", key, cursor} if match != "" { @@ -212,6 +348,9 @@ func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/sortedset_commands.go b/vendor/github.com/redis/go-redis/v9/sortedset_commands.go index 67014027034..b171d7ac1d6 100644 --- a/vendor/github.com/redis/go-redis/v9/sortedset_commands.go +++ b/vendor/github.com/redis/go-redis/v9/sortedset_commands.go @@ -2,8 +2,11 @@ package redis import ( "context" + "errors" "strings" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type SortedSetCmdable interface { @@ -257,16 +260,15 @@ func (c cmdable) ZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd } func (c cmdable) ZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd { - args := make([]interface{}, 4+len(keys)) + numKeys := len(keys) + args := make([]interface{}, 4+numKeys) args[0] = "zintercard" - numkeys := int64(0) + args[1] = numKeys for i, key := range keys { args[2+i] = key - numkeys++ } - args[1] = numkeys - args[2+numkeys] = "limit" - args[3+numkeys] = limit + args[2+numKeys] = "limit" + args[3+numKeys] = limit cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -312,7 +314,9 @@ func (c cmdable) ZPopMax(ctx context.Context, key string, count ...int64) *ZSlic case 1: args = append(args, count[0]) default: - panic("too many arguments") + cmd := NewZSliceCmd(ctx) + cmd.SetErr(errors.New("too many arguments")) + return cmd } cmd := NewZSliceCmd(ctx, args...) @@ -332,7 +336,9 @@ func (c cmdable) ZPopMin(ctx context.Context, key string, count ...int64) *ZSlic case 1: args = append(args, count[0]) default: - panic("too many arguments") + cmd := NewZSliceCmd(ctx) + cmd.SetErr(errors.New("too many arguments")) + return cmd } cmd := NewZSliceCmd(ctx, args...) @@ -367,6 +373,17 @@ type ZRangeArgs struct { // } // cmd: "ZRange example-key (3 8 ByScore" (3 < score <= 8). // + // When the Rev option is also provided, should be the higher score value and + // should be the lower score value (i.e. reversed order): + // ZRangeArgs{ + // Key: "example-key", + // Start: 8, + // Stop: "(3", + // ByScore: true, + // Rev: true, + // } + // cmd: "ZRange example-key 8 (3 ByScore Rev" (8 >= score > 3, in reverse order). + // // For the ByLex option, it is similar to the deprecated(6.2.0+) ZRangeByLex command. // You can set the and options as follows: // ZRangeArgs{ @@ -377,6 +394,17 @@ type ZRangeArgs struct { // } // cmd: "ZRange example-key [abc (def ByLex" // + // When the Rev option is also provided, should be the lexicographically higher + // value and should be the lower value: + // ZRangeArgs{ + // Key: "example-key", + // Start: "(def", + // Stop: "[abc", + // ByLex: true, + // Rev: true, + // } + // cmd: "ZRange example-key (def [abc ByLex Rev" + // // For normal cases (ByScore==false && ByLex==false), and should be set to the index range (int). // You can read the documentation for more information: https://redis.io/commands/zrange Start interface{} @@ -394,12 +422,7 @@ type ZRangeArgs struct { } func (z ZRangeArgs) appendArgs(args []interface{}) []interface{} { - // For Rev+ByScore/ByLex, we need to adjust the position of and . - if z.Rev && (z.ByScore || z.ByLex) { - args = append(args, z.Key, z.Stop, z.Start) - } else { - args = append(args, z.Key, z.Start, z.Stop) - } + args = append(args, z.Key, z.Start, z.Stop) if z.ByScore { args = append(args, "byscore") @@ -473,10 +496,16 @@ func (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy, return cmd } +// ZRangeByScore returns members in a sorted set within a range of scores. +// +// Deprecated: Use ZRangeArgs with ByScore option instead as of Redis 6.2.0. func (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { return c.zRangeBy(ctx, "zrangebyscore", key, opt, false) } +// ZRangeByLex returns members in a sorted set within a lexicographical range. +// +// Deprecated: Use ZRangeArgs with ByLex option instead as of Redis 6.2.0. func (c cmdable) ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { return c.zRangeBy(ctx, "zrangebylex", key, opt, false) } @@ -553,6 +582,9 @@ func (c cmdable) ZRemRangeByLex(ctx context.Context, key, min, max string) *IntC return cmd } +// ZRevRange returns members in a sorted set within a range of indexes in reverse order. +// +// Deprecated: Use ZRangeArgs with Rev option instead as of Redis 6.2.0. func (c cmdable) ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd { cmd := NewStringSliceCmd(ctx, "zrevrange", key, start, stop) _ = c(ctx, cmd) @@ -582,10 +614,16 @@ func (c cmdable) zRevRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeB return cmd } +// ZRevRangeByScore returns members in a sorted set within a range of scores in reverse order. +// +// Deprecated: Use ZRangeArgs with Rev and ByScore options instead as of Redis 6.2.0. func (c cmdable) ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { return c.zRevRangeBy(ctx, "zrevrangebyscore", key, opt) } +// ZRevRangeByLex returns members in a sorted set within a lexicographical range in reverse order. +// +// Deprecated: Use ZRangeArgs with Rev and ByLex options instead as of Redis 6.2.0. func (c cmdable) ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { return c.zRevRangeBy(ctx, "zrevrangebylex", key, opt) } @@ -720,6 +758,9 @@ func (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } @@ -740,7 +781,7 @@ type ZWithKey struct { type ZStore struct { Keys []string Weights []float64 - // Can be SUM, MIN or MAX. + // Can be SUM, MIN, MAX or COUNT. Aggregate string } diff --git a/vendor/github.com/redis/go-redis/v9/stream_commands.go b/vendor/github.com/redis/go-redis/v9/stream_commands.go index 6d7b2292249..71191aec4b2 100644 --- a/vendor/github.com/redis/go-redis/v9/stream_commands.go +++ b/vendor/github.com/redis/go-redis/v9/stream_commands.go @@ -2,12 +2,18 @@ package redis import ( "context" + "strconv" + "strings" "time" + + "github.com/redis/go-redis/v9/internal/otel" ) type StreamCmdable interface { XAdd(ctx context.Context, a *XAddArgs) *StringCmd + XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd XDel(ctx context.Context, stream string, ids ...string) *IntCmd + XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd XLen(ctx context.Context, stream string) *IntCmd XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd @@ -23,20 +29,27 @@ type StreamCmdable interface { XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd + XNack(ctx context.Context, a *XNackArgs) *IntCmd XPending(ctx context.Context, stream, group string) *XPendingCmd XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd + XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd + XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd + XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd XTrimMinID(ctx context.Context, key string, minID string) *IntCmd XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd + XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd + XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd XInfoStream(ctx context.Context, key string) *XInfoStreamCmd XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd + XCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd } // XAddArgs accepts values in the following formats: @@ -46,41 +59,69 @@ type StreamCmdable interface { // // Note that map will not preserve the order of key-value pairs. // MaxLen/MaxLenApprox and MinID are in conflict, only one of them can be used. +// +// For idempotent production (at-most-once production): +// - ProducerID: A unique identifier for the producer (required for both IDMP and IDMPAUTO) +// - IdempotentID: A unique identifier for the message (used with IDMP) +// - IdempotentAuto: If true, Redis will auto-generate an idempotent ID based on message content (IDMPAUTO) +// +// ProducerID and IdempotentID are mutually exclusive with IdempotentAuto. +// When using idempotent production, ID must be "*" or empty. type XAddArgs struct { Stream string NoMkStream bool MaxLen int64 // MAXLEN N MinID string // Approx causes MaxLen and MinID to use "~" matcher (instead of "="). - Approx bool - Limit int64 - ID string - Values interface{} + Approx bool + Limit int64 + Mode string + ID string + Values interface{} + ProducerID string // Producer ID for idempotent production (IDMP or IDMPAUTO) + IdempotentID string // Idempotent ID for IDMP + IdempotentAuto bool // Use IDMPAUTO to auto-generate idempotent ID based on content } func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { - args := make([]interface{}, 0, 11) + args := make([]interface{}, 0, 15) args = append(args, "xadd", a.Stream) if a.NoMkStream { args = append(args, "nomkstream") } + + if a.Mode != "" { + args = append(args, a.Mode) + } + + if a.ProducerID != "" { + if a.IdempotentAuto { + // IDMPAUTO pid + args = append(args, "idmpauto", a.ProducerID) + } else if a.IdempotentID != "" { + // IDMP pid iid + args = append(args, "idmp", a.ProducerID, a.IdempotentID) + } + } + switch { case a.MaxLen > 0: if a.Approx { args = append(args, "maxlen", "~", a.MaxLen) } else { - args = append(args, "maxlen", a.MaxLen) + args = append(args, "maxlen", "=", a.MaxLen) } case a.MinID != "": if a.Approx { args = append(args, "minid", "~", a.MinID) } else { - args = append(args, "minid", a.MinID) + args = append(args, "minid", "=", a.MinID) } } if a.Limit > 0 { args = append(args, "limit", a.Limit) } + if a.ID != "" { args = append(args, a.ID) } else { @@ -93,6 +134,16 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { return cmd } +func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd { args := []interface{}{"xdel", stream} for _, id := range ids { @@ -103,6 +154,16 @@ func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd return cmd } +func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xdelex", stream, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd { cmd := NewIntCmd(ctx, "xlen", stream) _ = c(ctx, cmd) @@ -231,6 +292,7 @@ type XReadGroupArgs struct { Count int64 Block time.Duration NoAck bool + Claim time.Duration // Claim idle pending entries older than this duration } func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd { @@ -250,6 +312,10 @@ func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSlic args = append(args, "noack") keyPos++ } + if a.Claim > 0 { + args = append(args, "claim", int64(a.Claim/time.Millisecond)) + keyPos += 2 + } args = append(args, "streams") keyPos++ for _, s := range a.Streams { @@ -262,6 +328,26 @@ func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSlic } cmd.SetFirstKeyPos(keyPos) _ = c(ctx, cmd) + + // Record stream lag for each message (if command succeeded) + if cmd.Err() == nil { + streams := cmd.Val() + for _, stream := range streams { + for _, msg := range stream.Messages { + // Parse message ID to extract timestamp (format: "millisecondsTime-sequenceNumber") + if parts := strings.SplitN(msg.ID, "-", 2); len(parts) == 2 { + if timestampMs, err := strconv.ParseInt(parts[0], 10, 64); err == nil { + // Calculate lag (time since message was created) + messageTime := time.Unix(0, timestampMs*int64(time.Millisecond)) + lag := time.Since(messageTime) + // Record lag metric + otel.RecordStreamLag(ctx, lag, nil, stream.Stream, a.Group, a.Consumer) + } + } + } + } + } + return cmd } @@ -275,6 +361,71 @@ func (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) return cmd } +// XNACK modes. See [XNackArgs.Mode]. +const ( + XNackModeSilent = "SILENT" + XNackModeFail = "FAIL" + XNackModeFatal = "FATAL" +) + +// XNackArgs represents the arguments for the XNACK command (Redis >= 8.8). +// +// XNACK negatively acknowledges one or more messages in a consumer group's +// Pending Entries List (PEL), releasing them back to the group so they can be +// redelivered to another consumer via XREADGROUP. +type XNackArgs struct { + Stream string + Group string + + // Mode controls how the delivery counter is adjusted for each NACKed entry. + // Must be one of [XNackModeSilent], [XNackModeFail], or [XNackModeFatal]: + // - SILENT: the consumer is shutting down or experiencing internal errors + // unrelated to the message. The delivery counter is decremented by 1, + // undoing the increment that happened when the message was delivered. + // - FAIL: the consumer could not process the message (e.g. insufficient + // memory), but another consumer might succeed. The delivery counter is + // left unchanged. + // - FATAL: the message is invalid or suspected malicious. The delivery + // counter is set to MAXINT, which will immediately move the message to + // the Dead Letter Queue (DLQ) if one is configured for the group. + Mode string + + // IDs is the list of message IDs to NACK. All IDs must already be in the + // group's PEL (i.e. previously delivered via XREADGROUP), unless Force is set. + IDs []string + + // RetryCount sets the delivery counter to an explicit value, overriding the + // counter adjustment that would otherwise be applied by Mode. + // Leave nil to let Mode control the counter (the common case). + RetryCount *uint64 + + // Force allows NACKing message IDs that are not yet in the group's PEL, + // creating new unowned NACKed PEL entries for them directly. + // This is analogous to the FORCE flag in XCLAIM. + // Primarily used internally by Redis during AOF rewrite to reconstruct + // NACKed entries, but can also be used to manually inject entries. + Force bool +} + +// XNack executes the XNACK command. See [XNackArgs] for the full argument documentation. +// Requires Redis >= 8.8. +func (c cmdable) XNack(ctx context.Context, a *XNackArgs) *IntCmd { + args := make([]interface{}, 0, 9+len(a.IDs)) + args = append(args, "xnack", a.Stream, a.Group, a.Mode, "ids", len(a.IDs)) + for _, id := range a.IDs { + args = append(args, id) + } + if a.RetryCount != nil { + args = append(args, "retrycount", *a.RetryCount) + } + if a.Force { + args = append(args, "force") + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XPending(ctx context.Context, stream, group string) *XPendingCmd { cmd := NewXPendingCmd(ctx, "xpending", stream, group) _ = c(ctx, cmd) @@ -322,6 +473,13 @@ func (c cmdable) XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimC return cmd } +func (c cmdable) XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd { + args := xAutoClaimArgs(ctx, a) + cmd := NewXAutoClaimWithDeletedCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd { args := xAutoClaimArgs(ctx, a) args = append(args, "justid") @@ -375,6 +533,8 @@ func xClaimArgs(a *XClaimArgs) []interface{} { return args } +// TODO: refactor xTrim, xTrimMode and the wrappers over the functions + // xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default). // example: // @@ -390,6 +550,8 @@ func (c cmdable) xTrim( args = append(args, "xtrim", key, strategy) if approx { args = append(args, "~") + } else { + args = append(args, "=") } args = append(args, threshold) if limit > 0 { @@ -418,6 +580,44 @@ func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, return c.xTrim(ctx, key, "minid", true, minID, limit) } +func (c cmdable) xTrimMode( + ctx context.Context, key, strategy string, + approx bool, threshold interface{}, limit int64, + mode string, +) *IntCmd { + args := make([]interface{}, 0, 7) + args = append(args, "xtrim", key, strategy) + if approx { + args = append(args, "~") + } else { + args = append(args, "=") + } + args = append(args, threshold) + if limit > 0 { + args = append(args, "limit", limit) + } + args = append(args, mode) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode) +} + +func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode) +} + +func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode) +} + +func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode) +} + func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd { cmd := NewXInfoConsumersCmd(ctx, key, group) _ = c(ctx, cmd) @@ -448,3 +648,28 @@ func (c cmdable) XInfoStreamFull(ctx context.Context, key string, count int) *XI _ = c(ctx, cmd) return cmd } + +// XCfgSetArgs represents the arguments for the XCFGSET command. +// Duration is the duration, in seconds, that Redis keeps each idempotent ID. +// MaxSize is the maximum number of most recent idempotent IDs that Redis keeps for each producer ID. +type XCfgSetArgs struct { + Stream string + Duration int64 + MaxSize int64 +} + +// XCfgSet sets the idempotent production configuration for a stream. +// XCFGSET key [IDMP-DURATION duration] [IDMP-MAXSIZE maxsize] +func (c cmdable) XCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd { + args := make([]interface{}, 0, 6) + args = append(args, "xcfgset", a.Stream) + if a.Duration > 0 { + args = append(args, "idmp-duration", a.Duration) + } + if a.MaxSize > 0 { + args = append(args, "idmp-maxsize", a.MaxSize) + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/string_commands.go b/vendor/github.com/redis/go-redis/v9/string_commands.go index eff5880dcde..88a80844e20 100644 --- a/vendor/github.com/redis/go-redis/v9/string_commands.go +++ b/vendor/github.com/redis/go-redis/v9/string_commands.go @@ -2,6 +2,8 @@ package redis import ( "context" + "fmt" + "strings" "time" ) @@ -9,6 +11,8 @@ type StringCmdable interface { Append(ctx context.Context, key, value string) *IntCmd Decr(ctx context.Context, key string) *IntCmd DecrBy(ctx context.Context, key string, decrement int64) *IntCmd + DelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd + Digest(ctx context.Context, key string) *DigestCmd Get(ctx context.Context, key string) *StringCmd GetRange(ctx context.Context, key string, start, end int64) *StringCmd GetSet(ctx context.Context, key string, value interface{}) *StringCmd @@ -17,13 +21,24 @@ type StringCmdable interface { Incr(ctx context.Context, key string) *IntCmd IncrBy(ctx context.Context, key string, value int64) *IntCmd IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd + IncrEXInt(ctx context.Context, key string, args IncrEXIntArgs) *IncrEXIntCmd + IncrEXFloat(ctx context.Context, key string, args IncrEXFloatArgs) *IncrEXFloatCmd LCS(ctx context.Context, q *LCSQuery) *LCSCmd MGet(ctx context.Context, keys ...string) *SliceCmd MSet(ctx context.Context, values ...interface{}) *StatusCmd MSetNX(ctx context.Context, values ...interface{}) *BoolCmd + MSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd SetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd + SetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd + SetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd + SetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd + SetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd + SetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd + SetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd + SetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd + SetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd @@ -48,6 +63,76 @@ func (c cmdable) DecrBy(ctx context.Context, key string, decrement int64) *IntCm return cmd } +// DelExArgs provides arguments for the DelExArgs function. +type DelExArgs struct { + // Mode can be `IFEQ`, `IFNE`, `IFDEQ`, or `IFDNE`. + Mode string + + // MatchValue is used with IFEQ/IFNE modes for compare-and-delete operations. + // - IFEQ: only delete if current value equals MatchValue + // - IFNE: only delete if current value does not equal MatchValue + MatchValue interface{} + + // MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-delete. + // - IFDEQ: only delete if current value's digest equals MatchDigest + // - IFDNE: only delete if current value's digest does not equal MatchDigest + // + // The digest is a uint64 xxh3 hash value. + // + // For examples of client-side digest generation, see: + // example/digest-optimistic-locking/ + MatchDigest uint64 +} + +// DelExArgs Redis `DELEX key [IFEQ|IFNE|IFDEQ|IFDNE] match-value` command. +// Compare-and-delete with flexible conditions. +// +// Returns the number of keys that were removed (0 or 1). +// +// NOTE DelExArgs is still experimental +// it's signature and behaviour may change +func (c cmdable) DelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd { + args := []interface{}{"delex", key} + + if a.Mode != "" { + args = append(args, a.Mode) + + // Add match value/digest based on mode + switch a.Mode { + case "ifeq", "IFEQ", "ifne", "IFNE": + if a.MatchValue != nil { + args = append(args, a.MatchValue) + } + case "ifdeq", "IFDEQ", "ifdne", "IFDNE": + if a.MatchDigest != 0 { + args = append(args, fmt.Sprintf("%016x", a.MatchDigest)) + } + } + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// Digest returns the xxh3 hash (uint64) of the specified key's value. +// +// The digest is a 64-bit xxh3 hash that can be used for optimistic locking +// with SetIFDEQ, SetIFDNE, and DelExArgs commands. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/digest/ +// +// NOTE Digest is still experimental +// it's signature and behaviour may change +func (c cmdable) Digest(ctx context.Context, key string) *DigestCmd { + cmd := NewDigestCmd(ctx, "digest", key) + _ = c(ctx, cmd) + return cmd +} + // Get Redis `GET key` command. It returns redis.Nil error when key does not exist. func (c cmdable) Get(ctx context.Context, key string) *StringCmd { cmd := NewStringCmd(ctx, "get", key) @@ -61,6 +146,9 @@ func (c cmdable) GetRange(ctx context.Context, key string, start, end int64) *St return cmd } +// GetSet returns the old value stored at key and sets it to the new value. +// +// Deprecated: Use SetArgs with Get option instead as of Redis 6.2.0. func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *StringCmd { cmd := NewStringCmd(ctx, "getset", key, value) _ = c(ctx, cmd) @@ -112,6 +200,160 @@ func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *Fl return cmd } +// IncrEXIntArgs are the arguments to IncrEXInt (the BYINT variant of INCREX). +// +// If By is zero and HasBy is false, the server increments by 1. +// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is a +// valid bound. Expiration is shared with the SET command via ExpirationOption. +type IncrEXIntArgs struct { + By int64 + HasBy bool + + LBound, UBound int64 + HasLBound, HasUBound bool + + // Saturate clamps the result to LBOUND/UBOUND (or LLONG_MAX/MIN when no + // explicit bound is given) when the increment would exceed it. Without + // this flag, out-of-bounds operations are rejected: the key and TTL are + // left unchanged and the reply is [current_value, 0]. + Saturate bool + + // Expiration sets the TTL semantics: EX, PX, EXAT, PXAT, or PERSIST. + Expiration *ExpirationOption + + // ENX applies the expiration only when the key does not already have an + // expiration. Requires Expiration to set one of EX/PX/EXAT/PXAT. + ENX bool +} + +// IncrEXFloatArgs are the arguments to IncrEXFloat (the BYFLOAT variant of +// INCREX). BYFLOAT is always sent — even when By is zero — to keep the +// operation in float mode on the server side; omitting BYFLOAT would cause +// the server to treat the call as an integer increment by 1. +// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is +// a valid bound. +type IncrEXFloatArgs struct { + By float64 + + LBound, UBound float64 + HasLBound, HasUBound bool + + // Saturate clamps the result to LBOUND/UBOUND (or ±LDBL_MAX when no + // explicit bound is given) when the increment would exceed it. Without + // this flag, out-of-bounds operations are rejected: the key and TTL are + // left unchanged and the reply is [current_value, 0]. + Saturate bool + + Expiration *ExpirationOption + + ENX bool +} + +// IncrEXInt Redis `INCREX key [BYINT amount] [LBOUND value] [UBOUND value] +// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]` +// command. +// +// Atomically increments the integer value stored at key, optionally +// constraining the result to a range and applying expiration semantics. +// Returns the new value and the increment that was actually applied. When +// the increment would exceed LBOUND/UBOUND and SATURATE is not set, the key +// and TTL are left unchanged and the reply is [current_value, 0]. +// +// Available since Redis 8.8. +// For more information, see https://redis.io/commands/increx +func (c cmdable) IncrEXInt(ctx context.Context, key string, a IncrEXIntArgs) *IncrEXIntCmd { + args := make([]interface{}, 0, 14) + args = append(args, "increx", key) + if a.HasBy { + args = append(args, "byint", a.By) + } + if a.HasLBound { + args = append(args, "lbound", a.LBound) + } + if a.HasUBound { + args = append(args, "ubound", a.UBound) + } + if a.Saturate { + args = append(args, "saturate") + } + args = appendIncrEXTail(args, a.Expiration, a.ENX) + + cmd := NewIncrEXIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// IncrEXFloat Redis `INCREX key [BYFLOAT amount] [LBOUND value] [UBOUND value] +// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]` +// command. +// +// Available since Redis 8.8. +// For more information, see https://redis.io/commands/increx +func (c cmdable) IncrEXFloat(ctx context.Context, key string, a IncrEXFloatArgs) *IncrEXFloatCmd { + args := make([]interface{}, 0, 14) + args = append(args, "increx", key, "byfloat", a.By) + if a.HasLBound { + args = append(args, "lbound", a.LBound) + } + if a.HasUBound { + args = append(args, "ubound", a.UBound) + } + if a.Saturate { + args = append(args, "saturate") + } + args = appendIncrEXTail(args, a.Expiration, a.ENX) + + cmd := NewIncrEXFloatCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func appendIncrEXTail(args []interface{}, exp *ExpirationOption, enx bool) []interface{} { + if exp != nil { + switch exp.Mode { + case EX, PX, EXAT, PXAT: + args = append(args, strings.ToLower(string(exp.Mode)), exp.Value) + case PERSIST: + args = append(args, "persist") + } + } + if enx { + args = append(args, "enx") + } + return args +} + +type SetCondition string + +const ( + // NX only set the keys and their expiration if none exist + NX SetCondition = "NX" + // XX only set the keys and their expiration if all already exist + XX SetCondition = "XX" +) + +type ExpirationMode string + +const ( + // EX sets expiration in seconds + EX ExpirationMode = "EX" + // PX sets expiration in milliseconds + PX ExpirationMode = "PX" + // EXAT sets expiration as Unix timestamp in seconds + EXAT ExpirationMode = "EXAT" + // PXAT sets expiration as Unix timestamp in milliseconds + PXAT ExpirationMode = "PXAT" + // KEEPTTL keeps the existing TTL + KEEPTTL ExpirationMode = "KEEPTTL" + // PERSIST removes the existing TTL. Used by INCREX. + PERSIST ExpirationMode = "PERSIST" +) + +type ExpirationOption struct { + Mode ExpirationMode + Value int64 +} + func (c cmdable) LCS(ctx context.Context, q *LCSQuery) *LCSCmd { cmd := NewLCSCmd(ctx, q) _ = c(ctx, cmd) @@ -157,6 +399,49 @@ func (c cmdable) MSetNX(ctx context.Context, values ...interface{}) *BoolCmd { return cmd } +type MSetEXArgs struct { + Condition SetCondition + Expiration *ExpirationOption +} + +// MSetEX sets the given keys to their respective values. +// This command is an extension of the MSETNX that adds expiration and XX options. +// Available since Redis 8.4 +// Important: When this method is used with Cluster clients, all keys +// must be in the same hash slot, otherwise CROSSSLOT error will be returned. +// For more information, see https://redis.io/commands/msetex +func (c cmdable) MSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd { + expandedArgs := appendArgs([]interface{}{}, values) + numkeys := len(expandedArgs) / 2 + + cmdArgs := make([]interface{}, 0, 2+len(expandedArgs)+3) + cmdArgs = append(cmdArgs, "msetex", numkeys) + cmdArgs = append(cmdArgs, expandedArgs...) + + if args.Condition != "" { + cmdArgs = append(cmdArgs, string(args.Condition)) + } + + if args.Expiration != nil { + switch args.Expiration.Mode { + case EX: + cmdArgs = append(cmdArgs, "ex", args.Expiration.Value) + case PX: + cmdArgs = append(cmdArgs, "px", args.Expiration.Value) + case EXAT: + cmdArgs = append(cmdArgs, "exat", args.Expiration.Value) + case PXAT: + cmdArgs = append(cmdArgs, "pxat", args.Expiration.Value) + case KEEPTTL: + cmdArgs = append(cmdArgs, "keepttl") + } + } + + cmd := NewIntCmd(ctx, cmdArgs...) + _ = c(ctx, cmd) + return cmd +} + // Set Redis `SET key value [expiration]` command. // Use expiration for `SETEx`-like behavior. // @@ -185,9 +470,24 @@ func (c cmdable) Set(ctx context.Context, key string, value interface{}, expirat // SetArgs provides arguments for the SetArgs function. type SetArgs struct { - // Mode can be `NX` or `XX` or empty. + // Mode can be `NX`, `XX`, `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` or empty. Mode string + // MatchValue is used with IFEQ/IFNE modes for compare-and-set operations. + // - IFEQ: only set if current value equals MatchValue + // - IFNE: only set if current value does not equal MatchValue + MatchValue interface{} + + // MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-set. + // - IFDEQ: only set if current value's digest equals MatchDigest + // - IFDNE: only set if current value's digest does not equal MatchDigest + // + // The digest is a uint64 xxh3 hash value. + // + // For examples of client-side digest generation, see: + // example/digest-optimistic-locking/ + MatchDigest uint64 + // Zero `TTL` or `Expiration` means that the key has no expiration time. TTL time.Duration ExpireAt time.Time @@ -223,6 +523,18 @@ func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a S if a.Mode != "" { args = append(args, a.Mode) + + // Add match value/digest for CAS modes + switch a.Mode { + case "ifeq", "IFEQ", "ifne", "IFNE": + if a.MatchValue != nil { + args = append(args, a.MatchValue) + } + case "ifdeq", "IFDEQ", "ifdne", "IFDNE": + if a.MatchDigest != 0 { + args = append(args, fmt.Sprintf("%016x", a.MatchDigest)) + } + } } if a.Get { @@ -234,14 +546,16 @@ func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a S return cmd } -// SetEx Redis `SETEx key expiration value` command. +// SetEx sets the value and expiration of a key. +// +// Deprecated: Use Set with expiration instead as of Redis 2.6.12. func (c cmdable) SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd { cmd := NewStatusCmd(ctx, "setex", key, formatSec(ctx, expiration), value) _ = c(ctx, cmd) return cmd } -// SetNX Redis `SET key value [expiration] NX` command. +// SetNX sets the value of a key only if the key does not exist. // // Zero expiration means the key has no expiration time. // KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0, @@ -250,8 +564,7 @@ func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expir var cmd *BoolCmd switch expiration { case 0: - // Use old `SETNX` to support old Redis versions. - cmd = NewBoolCmd(ctx, "setnx", key, value) + cmd = NewBoolCmd(ctx, "set", key, value, "nx") case KeepTTL: cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "nx") default: @@ -290,6 +603,270 @@ func (c cmdable) SetXX(ctx context.Context, key string, value interface{}, expir return cmd } +// SetIFEQ Redis `SET key value [expiration] IFEQ match-value` command. +// Compare-and-set: only sets the value if the current value equals matchValue. +// +// Returns "OK" on success. +// Returns nil if the operation was aborted due to condition not matching. +// Zero expiration means the key has no expiration time. +// +// NOTE SetIFEQ is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifeq", matchValue) + + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFEQGet Redis `SET key value [expiration] IFEQ match-value GET` command. +// Compare-and-set with GET: only sets the value if the current value equals matchValue, +// and returns the previous value. +// +// Returns the previous value on success. +// Returns nil if the operation was aborted due to condition not matching. +// Zero expiration means the key has no expiration time. +// +// NOTE SetIFEQGet is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifeq", matchValue, "get") + + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFNE Redis `SET key value [expiration] IFNE match-value` command. +// Compare-and-set: only sets the value if the current value does not equal matchValue. +// +// Returns "OK" on success. +// Returns nil if the operation was aborted due to condition not matching. +// Zero expiration means the key has no expiration time. +// +// NOTE SetIFNE is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifne", matchValue) + + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFNEGet Redis `SET key value [expiration] IFNE match-value GET` command. +// Compare-and-set with GET: only sets the value if the current value does not equal matchValue, +// and returns the previous value. +// +// Returns the previous value on success. +// Returns nil if the operation was aborted due to condition not matching. +// Zero expiration means the key has no expiration time. +// +// NOTE SetIFNEGet is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifne", matchValue, "get") + + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFDEQ sets the value only if the current value's digest equals matchDigest. +// +// This is a compare-and-set operation using xxh3 digest for optimistic locking. +// The matchDigest parameter is a uint64 xxh3 hash value. +// +// Returns "OK" on success. +// Returns redis.Nil if the digest doesn't match (value was modified). +// Zero expiration means the key has no expiration time. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/set/ +// +// NOTE SetIFNEQ is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifdeq", fmt.Sprintf("%016x", matchDigest)) + + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFDEQGet sets the value only if the current value's digest equals matchDigest, +// and returns the previous value. +// +// This is a compare-and-set operation using xxh3 digest for optimistic locking. +// The matchDigest parameter is a uint64 xxh3 hash value. +// +// Returns the previous value on success. +// Returns redis.Nil if the digest doesn't match (value was modified). +// Zero expiration means the key has no expiration time. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/set/ +// +// NOTE SetIFNEQGet is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifdeq", fmt.Sprintf("%016x", matchDigest), "get") + + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFDNE sets the value only if the current value's digest does NOT equal matchDigest. +// +// This is a compare-and-set operation using xxh3 digest for optimistic locking. +// The matchDigest parameter is a uint64 xxh3 hash value. +// +// Returns "OK" on success (digest didn't match, value was set). +// Returns redis.Nil if the digest matches (value was not modified). +// Zero expiration means the key has no expiration time. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/set/ +// +// NOTE SetIFDNE is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifdne", fmt.Sprintf("%016x", matchDigest)) + + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// SetIFDNEGet sets the value only if the current value's digest does NOT equal matchDigest, +// and returns the previous value. +// +// This is a compare-and-set operation using xxh3 digest for optimistic locking. +// The matchDigest parameter is a uint64 xxh3 hash value. +// +// Returns the previous value on success (digest didn't match, value was set). +// Returns redis.Nil if the digest matches (value was not modified). +// Zero expiration means the key has no expiration time. +// +// For examples of client-side digest generation and usage patterns, see: +// example/digest-optimistic-locking/ +// +// Redis 8.4+. See https://redis.io/commands/set/ +// +// NOTE SetIFDNEGet is still experimental +// it's signature and behaviour may change +func (c cmdable) SetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd { + args := []interface{}{"set", key, value} + + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == KeepTTL { + args = append(args, "keepttl") + } + + args = append(args, "ifdne", fmt.Sprintf("%016x", matchDigest), "get") + + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd { cmd := NewIntCmd(ctx, "setrange", key, offset, value) _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/timeseries_commands.go b/vendor/github.com/redis/go-redis/v9/timeseries_commands.go index 82d8cdfcf57..db00db80f2a 100644 --- a/vendor/github.com/redis/go-redis/v9/timeseries_commands.go +++ b/vendor/github.com/redis/go-redis/v9/timeseries_commands.go @@ -2,9 +2,12 @@ package redis import ( "context" - "strconv" + "errors" + "fmt" + "strings" "github.com/redis/go-redis/v9/internal/proto" + "github.com/redis/go-redis/v9/internal/util" ) type TimeseriesCmdable interface { @@ -96,6 +99,8 @@ const ( VarP VarS Twa + CountNaN + CountAll ) func (a Aggregator) String() string { @@ -128,44 +133,97 @@ func (a Aggregator) String() string { return "VAR.S" case Twa: return "TWA" + case CountNaN: + return "COUNTNAN" + case CountAll: + return "COUNTALL" default: return "" } } +var ( + errTSMultiAggregationGroupBy = errors.New("redis: GROUPBY is not allowed when multiple aggregators are specified") + errTSAggregationConflict = errors.New("redis: setting both Aggregator and Aggregators is not allowed; use Aggregators instead because Aggregator is deprecated") +) + +func formatAggregationArgs(aggregator Aggregator, aggregators []Aggregator) (string, int, error) { + if aggregator != Invalid && len(aggregators) > 0 { + return "", 0, errTSAggregationConflict + } + if len(aggregators) == 0 { + if aggregator == Invalid { + return "", 0, nil + } + aggregationArg, err := formatAggregatorArg(aggregator) + if err != nil { + return "", 0, err + } + return aggregationArg, 1, nil + } + + parts := make([]string, len(aggregators)) + for i, agg := range aggregators { + if agg == Invalid { + return "", 0, fmt.Errorf("redis: invalid timeseries aggregator at index %d: Invalid (%d)", i, agg) + } + aggregationArg, err := formatAggregatorArg(agg) + if err != nil { + return "", 0, fmt.Errorf("redis: invalid timeseries aggregator at index %d: %d", i, agg) + } + parts[i] = aggregationArg + } + + return strings.Join(parts, ","), len(parts), nil +} + +func formatAggregatorArg(aggregator Aggregator) (string, error) { + aggregationArg := aggregator.String() + if aggregationArg == "" { + return "", fmt.Errorf("redis: invalid timeseries aggregator: %d", aggregator) + } + return aggregationArg, nil +} + type TSRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool } type TSRevRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool } type TSMRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - WithLabels bool - SelectedLabels []interface{} - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + WithLabels bool + SelectedLabels []interface{} + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool @@ -174,14 +232,16 @@ type TSMRangeOptions struct { } type TSMRevRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - WithLabels bool - SelectedLabels []interface{} - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + WithLabels bool + SelectedLabels []interface{} + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool @@ -477,7 +537,16 @@ func (c cmdable) TSGet(ctx context.Context, key string) *TSTimestampValueCmd { type TSTimestampValue struct { Timestamp int64 Value float64 + Values []float64 } + +func (tv TSTimestampValue) String() string { + if len(tv.Values) > 0 { + return fmt.Sprintf("{%d %v}", tv.Timestamp, tv.Values) + } + return fmt.Sprintf("{%d %v}", tv.Timestamp, tv.Value) +} + type TSTimestampValueCmd struct { baseCmd val TSTimestampValue @@ -486,8 +555,9 @@ type TSTimestampValueCmd struct { func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd { return &TSTimestampValueCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeTSTimestampValue, }, } } @@ -524,7 +594,7 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) { return err } cmd.val.Timestamp = timestamp - cmd.val.Value, err = strconv.ParseFloat(value, 64) + cmd.val.Value, err = util.ParseStringToFloat(value) if err != nil { return err } @@ -533,6 +603,18 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) { return nil } +func (cmd *TSTimestampValueCmd) Clone() Cmder { + val := cmd.val + if cmd.val.Values != nil { + val.Values = make([]float64, len(cmd.val.Values)) + copy(val.Values, cmd.val.Values) + } + return &TSTimestampValueCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // TSInfo - Returns information about a time-series key. // For more information - https://redis.io/commands/ts.info/ func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd { @@ -622,8 +704,14 @@ func (c cmdable) TSRevRangeWithArgs(ctx context.Context, key string, fromTimesta if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, _, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := newTSTimestampValueSliceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -678,8 +766,14 @@ func (c cmdable) TSRangeWithArgs(ctx context.Context, key string, fromTimestamp if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, _, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := newTSTimestampValueSliceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -704,8 +798,9 @@ type TSTimestampValueSliceCmd struct { func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd { return &TSTimestampValueSliceCmd{ baseCmd: baseCmd{ - ctx: ctx, - args: args, + ctx: ctx, + args: args, + cmdType: CmdTypeTSTimestampValueSlice, }, } } @@ -733,25 +828,62 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) { } cmd.val = make([]TSTimestampValue, n) for i := 0; i < n; i++ { - _, _ = rd.ReadArrayLen() - timestamp, err := rd.ReadInt() + itemLen, err := rd.ReadArrayLen() if err != nil { return err } - value, err := rd.ReadString() + + timestamp, err := rd.ReadInt() if err != nil { return err } cmd.val[i].Timestamp = timestamp - cmd.val[i].Value, err = strconv.ParseFloat(value, 64) - if err != nil { - return err + if itemLen == 2 { + value, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[i].Value, err = util.ParseStringToFloat(value) + if err != nil { + return err + } + continue + } + + cmd.val[i].Values = make([]float64, itemLen-1) + for j := 0; j < itemLen-1; j++ { + value, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[i].Values[j], err = util.ParseStringToFloat(value) + if err != nil { + return err + } } } return nil } +func (cmd *TSTimestampValueSliceCmd) Clone() Cmder { + var val []TSTimestampValue + if cmd.val != nil { + val = make([]TSTimestampValue, len(cmd.val)) + copy(val, cmd.val) + for i := range cmd.val { + if cmd.val[i].Values != nil { + val[i].Values = make([]float64, len(cmd.val[i].Values)) + copy(val[i].Values, cmd.val[i].Values) + } + } + } + return &TSTimestampValueSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + // TSMRange - Returns a range of samples from multiple time-series keys. // For more information - https://redis.io/commands/ts.mrange/ func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd { @@ -772,6 +904,7 @@ func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp in // For more information - https://redis.io/commands/ts.mrange/ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRangeOptions) *MapStringSliceInterfaceCmd { args := []interface{}{"TS.MRANGE", fromTimestamp, toTimestamp} + multiAggregationCount := 0 if options != nil { if options.Latest { args = append(args, "LATEST") @@ -801,8 +934,15 @@ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTime if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, count, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + multiAggregationCount = count + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -819,6 +959,11 @@ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTime args = append(args, f) } if options != nil { + if multiAggregationCount > 1 && (options.GroupByLabel != nil || options.Reducer != nil) { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(errTSMultiAggregationGroupBy) + return cmd + } if options.GroupByLabel != nil { args = append(args, "GROUPBY", options.GroupByLabel) } @@ -851,6 +996,7 @@ func (c cmdable) TSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp // For more information - https://redis.io/commands/ts.mrevrange/ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRevRangeOptions) *MapStringSliceInterfaceCmd { args := []interface{}{"TS.MREVRANGE", fromTimestamp, toTimestamp} + multiAggregationCount := 0 if options != nil { if options.Latest { args = append(args, "LATEST") @@ -880,8 +1026,15 @@ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toT if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, count, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + multiAggregationCount = count + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -898,6 +1051,11 @@ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toT args = append(args, f) } if options != nil { + if multiAggregationCount > 1 && (options.GroupByLabel != nil || options.Reducer != nil) { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(errTSMultiAggregationGroupBy) + return cmd + } if options.GroupByLabel != nil { args = append(args, "GROUPBY", options.GroupByLabel) } diff --git a/vendor/github.com/redis/go-redis/v9/tx.go b/vendor/github.com/redis/go-redis/v9/tx.go index 0daa222e352..b433b402414 100644 --- a/vendor/github.com/redis/go-redis/v9/tx.go +++ b/vendor/github.com/redis/go-redis/v9/tx.go @@ -11,7 +11,7 @@ import ( const TxFailedErr = proto.RedisError("redis: transaction failed") // Tx implements Redis transactions as described in -// http://redis.io/topics/transactions. It's NOT safe for concurrent use +// https://redis.io/docs/latest/develop/using-commands/transactions. It's NOT safe for concurrent use // by multiple goroutines, because Exec resets list of watched keys. // // If you don't need WATCH, use Pipeline instead. @@ -24,9 +24,11 @@ type Tx struct { func (c *Client) newTx() *Tx { tx := Tx{ baseClient: baseClient{ - opt: c.opt, - connPool: pool.NewStickyConnPool(c.connPool), - hooksMixin: c.hooksMixin.clone(), + opt: c.cloneOpt(), // Clone options under optLock to avoid race with initConn + connPool: pool.NewStickyConnPool(c.connPool), + hooksMixin: c.hooksMixin.clone(), + pushProcessor: c.pushProcessor, // Copy push processor from parent client + onClose: &onCloseHooks{}, }, } tx.init() diff --git a/vendor/github.com/redis/go-redis/v9/universal.go b/vendor/github.com/redis/go-redis/v9/universal.go index a1ce17bac36..b623460cba6 100644 --- a/vendor/github.com/redis/go-redis/v9/universal.go +++ b/vendor/github.com/redis/go-redis/v9/universal.go @@ -5,6 +5,10 @@ import ( "crypto/tls" "net" "time" + + "github.com/redis/go-redis/v9/auth" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/redis/go-redis/v9/push" ) // UniversalOptions information is required by UniversalClient to establish @@ -26,9 +30,27 @@ type UniversalOptions struct { Dialer func(ctx context.Context, network, addr string) (net.Conn, error) OnConnect func(ctx context.Context, cn *Conn) error - Protocol int - Username string - Password string + Protocol int + Username string + Password string + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + SentinelUsername string SentinelPassword string @@ -36,21 +58,52 @@ type UniversalOptions struct { MinRetryBackoff time.Duration MaxRetryBackoff time.Duration - DialTimeout time.Duration + DialTimeout time.Duration + + // DialerRetries is the maximum number of retry attempts when dialing fails. + // + // default: 5 + DialerRetries int + + // DialerRetryTimeout is the backoff duration between retry attempts. + // + // default: 100 milliseconds + DialerRetryTimeout time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 32KiB (32768 bytes) + WriteBufferSize int + // PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO). PoolFIFO bool - PoolSize int - PoolTimeout time.Duration - MinIdleConns int - MaxIdleConns int - MaxActiveConns int - ConnMaxIdleTime time.Duration - ConnMaxLifetime time.Duration + PoolSize int + + // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines. + // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize. + MaxConcurrentDials int + + PoolTimeout time.Duration + MinIdleConns int + MaxIdleConns int + MaxActiveConns int + ConnMaxIdleTime time.Duration + ConnMaxLifetime time.Duration + ConnMaxLifetimeJitter time.Duration TLSConfig *tls.Config @@ -78,10 +131,26 @@ type UniversalOptions struct { DisableIdentity bool IdentitySuffix string - UnstableResp3 bool + + // FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing. + // When a node is marked as failing, it will be avoided for this duration. + // Only applies to cluster clients. Default is 15 seconds. + FailingTimeoutSeconds int + + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. + UnstableResp3 bool + + // PushNotificationProcessor is the processor for handling push notifications. + // If nil, a default processor will be created for RESP3 connections. + PushNotificationProcessor push.NotificationProcessor // IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint). IsClusterMode bool + + // MaintNotificationsConfig provides configuration for maintnotifications upgrades. + MaintNotificationsConfig *maintnotifications.Config } // Cluster returns cluster options created from the universal options. @@ -96,9 +165,12 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { Dialer: o.Dialer, OnConnect: o.OnConnect, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, MaxRedirects: o.MaxRedirects, ReadOnly: o.ReadOnly, @@ -109,27 +181,37 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { MinRetryBackoff: o.MinRetryBackoff, MaxRetryBackoff: o.MaxRetryBackoff, - DialTimeout: o.DialTimeout, - ReadTimeout: o.ReadTimeout, - WriteTimeout: o.WriteTimeout, + DialTimeout: o.DialTimeout, + DialerRetries: o.DialerRetries, + DialerRetryTimeout: o.DialerRetryTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + ContextTimeoutEnabled: o.ContextTimeoutEnabled, - PoolFIFO: o.PoolFIFO, + ReadBufferSize: o.ReadBufferSize, + WriteBufferSize: o.WriteBufferSize, - PoolSize: o.PoolSize, - PoolTimeout: o.PoolTimeout, - MinIdleConns: o.MinIdleConns, - MaxIdleConns: o.MaxIdleConns, - MaxActiveConns: o.MaxActiveConns, - ConnMaxIdleTime: o.ConnMaxIdleTime, - ConnMaxLifetime: o.ConnMaxLifetime, + PoolFIFO: o.PoolFIFO, + PoolSize: o.PoolSize, + MaxConcurrentDials: o.MaxConcurrentDials, + PoolTimeout: o.PoolTimeout, + MinIdleConns: o.MinIdleConns, + MaxIdleConns: o.MaxIdleConns, + MaxActiveConns: o.MaxActiveConns, + ConnMaxIdleTime: o.ConnMaxIdleTime, + ConnMaxLifetime: o.ConnMaxLifetime, + ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter, TLSConfig: o.TLSConfig, - DisableIdentity: o.DisableIdentity, - DisableIndentity: o.DisableIndentity, - IdentitySuffix: o.IdentitySuffix, - UnstableResp3: o.UnstableResp3, + DisableIdentity: o.DisableIdentity, + DisableIndentity: o.DisableIndentity, + IdentitySuffix: o.IdentitySuffix, + FailingTimeoutSeconds: o.FailingTimeoutSeconds, + UnstableResp3: o.UnstableResp3, + PushNotificationProcessor: o.PushNotificationProcessor, + MaintNotificationsConfig: o.MaintNotificationsConfig, } } @@ -147,10 +229,14 @@ func (o *UniversalOptions) Failover() *FailoverOptions { Dialer: o.Dialer, OnConnect: o.OnConnect, - DB: o.DB, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + DB: o.DB, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, + SentinelUsername: o.SentinelUsername, SentinelPassword: o.SentinelPassword, @@ -161,28 +247,38 @@ func (o *UniversalOptions) Failover() *FailoverOptions { MinRetryBackoff: o.MinRetryBackoff, MaxRetryBackoff: o.MaxRetryBackoff, - DialTimeout: o.DialTimeout, - ReadTimeout: o.ReadTimeout, - WriteTimeout: o.WriteTimeout, + DialTimeout: o.DialTimeout, + DialerRetries: o.DialerRetries, + DialerRetryTimeout: o.DialerRetryTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + ContextTimeoutEnabled: o.ContextTimeoutEnabled, - PoolFIFO: o.PoolFIFO, - PoolSize: o.PoolSize, - PoolTimeout: o.PoolTimeout, - MinIdleConns: o.MinIdleConns, - MaxIdleConns: o.MaxIdleConns, - MaxActiveConns: o.MaxActiveConns, - ConnMaxIdleTime: o.ConnMaxIdleTime, - ConnMaxLifetime: o.ConnMaxLifetime, + ReadBufferSize: o.ReadBufferSize, + WriteBufferSize: o.WriteBufferSize, + + PoolFIFO: o.PoolFIFO, + PoolSize: o.PoolSize, + MaxConcurrentDials: o.MaxConcurrentDials, + PoolTimeout: o.PoolTimeout, + MinIdleConns: o.MinIdleConns, + MaxIdleConns: o.MaxIdleConns, + MaxActiveConns: o.MaxActiveConns, + ConnMaxIdleTime: o.ConnMaxIdleTime, + ConnMaxLifetime: o.ConnMaxLifetime, + ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter, TLSConfig: o.TLSConfig, ReplicaOnly: o.ReadOnly, - DisableIdentity: o.DisableIdentity, - DisableIndentity: o.DisableIndentity, - IdentitySuffix: o.IdentitySuffix, - UnstableResp3: o.UnstableResp3, + DisableIdentity: o.DisableIdentity, + DisableIndentity: o.DisableIndentity, + IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, + PushNotificationProcessor: o.PushNotificationProcessor, + // Note: MaintNotificationsConfig not supported for FailoverOptions } } @@ -199,35 +295,48 @@ func (o *UniversalOptions) Simple() *Options { Dialer: o.Dialer, OnConnect: o.OnConnect, - DB: o.DB, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + DB: o.DB, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, MaxRetries: o.MaxRetries, MinRetryBackoff: o.MinRetryBackoff, MaxRetryBackoff: o.MaxRetryBackoff, - DialTimeout: o.DialTimeout, - ReadTimeout: o.ReadTimeout, - WriteTimeout: o.WriteTimeout, + DialTimeout: o.DialTimeout, + DialerRetries: o.DialerRetries, + DialerRetryTimeout: o.DialerRetryTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + ContextTimeoutEnabled: o.ContextTimeoutEnabled, - PoolFIFO: o.PoolFIFO, - PoolSize: o.PoolSize, - PoolTimeout: o.PoolTimeout, - MinIdleConns: o.MinIdleConns, - MaxIdleConns: o.MaxIdleConns, - MaxActiveConns: o.MaxActiveConns, - ConnMaxIdleTime: o.ConnMaxIdleTime, - ConnMaxLifetime: o.ConnMaxLifetime, + ReadBufferSize: o.ReadBufferSize, + WriteBufferSize: o.WriteBufferSize, + + PoolFIFO: o.PoolFIFO, + PoolSize: o.PoolSize, + MaxConcurrentDials: o.MaxConcurrentDials, + PoolTimeout: o.PoolTimeout, + MinIdleConns: o.MinIdleConns, + MaxIdleConns: o.MaxIdleConns, + MaxActiveConns: o.MaxActiveConns, + ConnMaxIdleTime: o.ConnMaxIdleTime, + ConnMaxLifetime: o.ConnMaxLifetime, + ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter, TLSConfig: o.TLSConfig, - DisableIdentity: o.DisableIdentity, - DisableIndentity: o.DisableIndentity, - IdentitySuffix: o.IdentitySuffix, - UnstableResp3: o.UnstableResp3, + DisableIdentity: o.DisableIdentity, + DisableIndentity: o.DisableIndentity, + IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, + PushNotificationProcessor: o.PushNotificationProcessor, + MaintNotificationsConfig: o.MaintNotificationsConfig, } } @@ -266,6 +375,8 @@ var ( // 3. If the number of Addrs is two or more, or IsClusterMode option is specified, // a ClusterClient is returned. // 4. Otherwise, a single-node Client is returned. +// +// Passing nil UniversalOptions will cause a panic. func NewUniversalClient(opts *UniversalOptions) UniversalClient { if opts == nil { panic("redis: NewUniversalClient nil options") diff --git a/vendor/github.com/redis/go-redis/v9/vectorset_commands.go b/vendor/github.com/redis/go-redis/v9/vectorset_commands.go index 2bd9e221661..a88bb1cd985 100644 --- a/vendor/github.com/redis/go-redis/v9/vectorset_commands.go +++ b/vendor/github.com/redis/go-redis/v9/vectorset_commands.go @@ -15,8 +15,8 @@ type VectorSetCmdable interface { VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd VGetAttr(ctx context.Context, key, element string) *StringCmd VInfo(ctx context.Context, key string) *MapStringInterfaceCmd - VLinks(ctx context.Context, key, element string) *StringSliceCmd - VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd + VLinks(ctx context.Context, key, element string) *StringSliceSliceCmd + VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceSliceCmd VRandMember(ctx context.Context, key string) *StringCmd VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd VRem(ctx context.Context, key, element string) *BoolCmd @@ -26,6 +26,10 @@ type VectorSetCmdable interface { VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd + VSimWithArgsWithAttribs(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorAttribSliceCmd + VSimWithArgsWithScoresWithAttribs(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreAttribSliceCmd + VRange(ctx context.Context, key, start, end string, count int64) *StringSliceCmd + VIsMember(ctx context.Context, key, element string) *BoolCmd } type Vector interface { @@ -35,6 +39,11 @@ type Vector interface { const ( vectorFormatFP32 string = "FP32" vectorFormatValues string = "Values" + vectorFormatF16 string = "FLOAT16" + vectorFormatBF16 string = "BFLOAT16" + vectorFormatF64 string = "FLOAT64" + vectorFormatI8 string = "INT8" + vectorFormatU8 string = "UINT8" ) type VectorFP32 struct { @@ -47,6 +56,66 @@ func (v *VectorFP32) Value() []any { var _ Vector = (*VectorFP32)(nil) +// VectorFloat16 represents a FLOAT16-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorFloat16 struct { + Val []byte +} + +func (v *VectorFloat16) Value() []any { + return []any{vectorFormatF16, v.Val} +} + +var _ Vector = (*VectorFloat16)(nil) + +// VectorBFloat16 represents a BFLOAT16-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorBFloat16 struct { + Val []byte +} + +func (v *VectorBFloat16) Value() []any { + return []any{vectorFormatBF16, v.Val} +} + +var _ Vector = (*VectorBFloat16)(nil) + +// VectorFloat64 represents a FLOAT64-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorFloat64 struct { + Val []byte +} + +func (v *VectorFloat64) Value() []any { + return []any{vectorFormatF64, v.Val} +} + +var _ Vector = (*VectorFloat64)(nil) + +// VectorInt8 represents an INT8-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorInt8 struct { + Val []byte +} + +func (v *VectorInt8) Value() []any { + return []any{vectorFormatI8, v.Val} +} + +var _ Vector = (*VectorInt8)(nil) + +// VectorUint8 represents a UINT8-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorUint8 struct { + Val []byte +} + +func (v *VectorUint8) Value() []any { + return []any{vectorFormatU8, v.Val} +} + +var _ Vector = (*VectorUint8)(nil) + type VectorValues struct { Val []float64 } @@ -78,6 +147,17 @@ type VectorScore struct { Score float64 } +type VectorAttrib struct { + Name string + Attribs *string +} + +type VectorScoreAttrib struct { + Name string + Score float64 + Attribs *string +} + // `VADD key (FP32 | VALUES num) vector element` // note: the API is experimental and may be subject to change. func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { @@ -192,16 +272,16 @@ func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { // `VLINKS key element` // note: the API is experimental and may be subject to change. -func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "vlinks", key, element) +func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceSliceCmd { + cmd := NewStringSliceSliceCmd(ctx, "vlinks", key, element) _ = c(ctx, cmd) return cmd } // `VLINKS key element WITHSCORES` // note: the API is experimental and may be subject to change. -func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd { - cmd := NewVectorInfoSliceCmd(ctx, "vlinks", key, element, "withscores") +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceSliceCmd { + cmd := NewVectorScoreSliceSliceCmd(ctx, "vlinks", key, element, "withscores") _ = c(ctx, cmd) return cmd } @@ -287,8 +367,7 @@ type VSimArgs struct { FilterEF int64 Truth bool NoThread bool - // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. - // Epsilon float64 + Epsilon float64 } func (v VSimArgs) appendArgs(args []any) []any { @@ -310,13 +389,13 @@ func (v VSimArgs) appendArgs(args []any) []any { if v.NoThread { args = append(args, "nothread") } - // if v.Epsilon > 0 { - // args = append(args, "Epsilon", v.Epsilon) - // } + if v.Epsilon > 0 { + args = append(args, "epsilon", v.Epsilon) + } return args } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { @@ -331,7 +410,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr return cmd } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd { @@ -346,3 +425,56 @@ func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vec _ = c(ctx, cmd) return cmd } + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHATTRIBS] [COUNT num] [EPSILON delta] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// WITHATTRIBS is only available in Redis v8.2.0+ +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgsWithAttribs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorAttribSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withattribs") + args = simArgs.appendArgs(args) + cmd := NewVectorAttribSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [WITHATTRIBS] [COUNT num] [EPSILON delta] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// WITHATTRIBS is only available in Redis v8.2.0+ +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgsWithScoresWithAttribs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreAttribSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withscores", "withattribs") + args = simArgs.appendArgs(args) + cmd := NewVectorScoreAttribSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VRANGE key start end count` +// a negative count means to return all the elements in the vector set. +// note: the API is experimental and may be subject to change. +func (c cmdable) VRange(ctx context.Context, key, start, end string, count int64) *StringSliceCmd { + args := []any{"vrange", key, start, end, count} + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VISMEMBER key element` +// Check if an element exists in a vector set. +// note: the API is experimental and may be subject to change. +func (c cmdable) VIsMember(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vismember", key, element) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/version.go b/vendor/github.com/redis/go-redis/v9/version.go index cbed8bd8d28..d59381170c6 100644 --- a/vendor/github.com/redis/go-redis/v9/version.go +++ b/vendor/github.com/redis/go-redis/v9/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.10.0" + return "9.20.1" } diff --git a/vendor/github.com/weppos/publicsuffix-go/publicsuffix/rules.go b/vendor/github.com/weppos/publicsuffix-go/publicsuffix/rules.go index 6a45957dda1..3bb302368ee 100644 --- a/vendor/github.com/weppos/publicsuffix-go/publicsuffix/rules.go +++ b/vendor/github.com/weppos/publicsuffix-go/publicsuffix/rules.go @@ -3,13 +3,13 @@ package publicsuffix -const ListVersion = "PSL version 7ef638 (Mon Mar 2 12:22:01 2026)" +const ListVersion = "PSL version ee780b (Thu May 7 00:57:06 2026)" -func DefaultRules() [10154]Rule { +func DefaultRules() [10207]Rule { return r } -var r = [10154]Rule{ +var r = [10207]Rule{ {1, "ac", 1, false}, {1, "com.ac", 2, false}, {1, "edu.ac", 2, false}, @@ -7852,7 +7852,9 @@ var r = [10154]Rule{ {1, "transfer-webapp.cn-northwest-1.on.amazonwebservices.com.cn", 6, true}, {1, "eero.online", 2, true}, {1, "eero-stage.online", 2, true}, + {1, "opentunnel.xyz", 2, true}, {1, "antagonist.cloud", 2, true}, + {1, "claude.app", 2, true}, {1, "apigee.io", 2, true}, {1, "panel.dev", 2, true}, {1, "siiites.com", 2, true}, @@ -7889,6 +7891,9 @@ var r = [10154]Rule{ {1, "potager.org", 2, true}, {1, "sweetpepper.org", 2, true}, {1, "myasustor.com", 2, true}, + {2, "atlassian-3p.com", 3, true}, + {2, "atlassian-3p-us-gov-mod.com", 3, true}, + {2, "atlassian-isolated-3p.com", 3, true}, {1, "cdn.prod.atlassian-dev.net", 4, true}, {1, "myfritz.link", 2, true}, {1, "myfritz.net", 2, true}, @@ -8093,7 +8098,6 @@ var r = [10154]Rule{ {1, "static-access.net", 2, true}, {1, "craft.me", 2, true}, {1, "realm.cz", 2, true}, - {1, "on.crisp.email", 3, true}, {2, "cryptonomic.net", 3, true}, {1, "cfolks.pl", 2, true}, {1, "cyon.link", 2, true}, @@ -8131,6 +8135,9 @@ var r = [10154]Rule{ {1, "deno-staging.dev", 2, true}, {1, "deno.net", 2, true}, {1, "sandbox.deno.net", 3, true}, + {1, "deployagent.com", 2, true}, + {1, "piebox.site", 2, true}, + {1, "deployagent.space", 2, true}, {1, "dedyn.io", 2, true}, {1, "deta.app", 2, true}, {1, "deta.dev", 2, true}, @@ -8579,9 +8586,13 @@ var r = [10154]Rule{ {1, "us-4.evennode.com", 3, true}, {1, "relay.evervault.app", 3, true}, {1, "relay.evervault.dev", 3, true}, + {1, "exe.xyz", 2, true}, {1, "expo.app", 2, true}, + {1, "on.expo.app", 3, true}, {1, "staging.expo.app", 3, true}, + {1, "on.staging.expo.app", 4, true}, {1, "onfabrica.com", 2, true}, + {1, "fspages.org", 2, true}, {1, "ru.net", 2, true}, {1, "adygeya.ru", 2, true}, {1, "bashkiria.ru", 2, true}, @@ -8684,6 +8695,7 @@ var r = [10154]Rule{ {1, "app.os.stg.fedoraproject.org", 5, true}, {1, "mydobiss.com", 2, true}, {1, "fh-muenster.io", 2, true}, + {1, "payload.dev", 2, true}, {1, "figma.site", 2, true}, {1, "figma-gov.site", 2, true}, {1, "preview.site", 2, true}, @@ -8895,8 +8907,6 @@ var r = [10154]Rule{ {1, "cloudfunctions.net", 2, true}, {1, "goupile.fr", 2, true}, {1, "pymnt.uk", 2, true}, - {1, "cloudapps.digital", 2, true}, - {1, "london.cloudapps.digital", 3, true}, {1, "gov.nl", 2, true}, {1, "grafana-dev.net", 2, true}, {1, "grayjayleagues.com", 2, true}, @@ -8937,6 +8947,7 @@ var r = [10154]Rule{ {2, "kin.one", 3, true}, {2, "id.pub", 3, true}, {2, "kin.pub", 3, true}, + {1, "seprox.hooc.me", 3, true}, {1, "hoplix.shop", 2, true}, {1, "orx.biz", 2, true}, {1, "biz.ng", 2, true}, @@ -9123,8 +9134,16 @@ var r = [10154]Rule{ {1, "us1-plenit.com", 2, true}, {1, "webadorsite.com", 2, true}, {1, "jouwweb.site", 2, true}, - {2, "triton.zone", 3, true}, {1, "js.org", 2, true}, + {1, "elastic.k2.cloud", 3, true}, + {1, "lb.ru-msk.k2.cloud", 4, true}, + {1, "s3.ru-msk.k2.cloud", 4, true}, + {1, "website.ru-msk.k2.cloud", 4, true}, + {1, "lb.ru-spb.k2.cloud", 4, true}, + {1, "s3.ru-spb.k2.cloud", 4, true}, + {1, "website.ru-spb.k2.cloud", 4, true}, + {1, "s3.k2.cloud", 3, true}, + {1, "website.k2.cloud", 3, true}, {1, "kaas.gg", 2, true}, {1, "khplay.nl", 2, true}, {1, "kapsi.fi", 2, true}, @@ -9252,7 +9271,6 @@ var r = [10154]Rule{ {1, "polyspace.com", 2, true}, {1, "mayfirst.info", 2, true}, {1, "mayfirst.org", 2, true}, - {1, "mazeplay.com", 2, true}, {1, "mcdir.me", 2, true}, {1, "mcdir.ru", 2, true}, {1, "vps.mcdir.ru", 3, true}, @@ -9266,6 +9284,7 @@ var r = [10154]Rule{ {1, "messerli.app", 2, true}, {1, "atmeta.com", 2, true}, {1, "apps.fbsbx.com", 3, true}, + {2, "metaaiusercontent.com", 3, true}, {2, "cloud.metacentrum.cz", 4, true}, {1, "custom.metacentrum.cz", 3, true}, {1, "flt.cloud.muni.cz", 4, true}, @@ -9464,6 +9483,31 @@ var r = [10154]Rule{ {2, "code.run", 3, true}, {2, "database.run", 3, true}, {2, "migration.run", 3, true}, + {1, "aberdeen.wa.us", 3, true}, + {1, "bainbridge-isl.wa.us", 3, true}, + {1, "bellevue.wa.us", 3, true}, + {1, "bremerton.wa.us", 3, true}, + {1, "centralia.wa.us", 3, true}, + {1, "chehalis.wa.us", 3, true}, + {1, "forks.wa.us", 3, true}, + {1, "gig-harbor.wa.us", 3, true}, + {1, "hoquiam.wa.us", 3, true}, + {1, "keyport.wa.us", 3, true}, + {1, "kingston.wa.us", 3, true}, + {1, "olympia.wa.us", 3, true}, + {1, "port-angeles.wa.us", 3, true}, + {1, "port-ludlow.wa.us", 3, true}, + {1, "port-orchard.wa.us", 3, true}, + {1, "port-townsend.wa.us", 3, true}, + {1, "poulsbo.wa.us", 3, true}, + {1, "redmond.wa.us", 3, true}, + {1, "renton.wa.us", 3, true}, + {1, "sea.wa.us", 3, true}, + {1, "seattle.wa.us", 3, true}, + {1, "sequim.wa.us", 3, true}, + {1, "shelton.wa.us", 3, true}, + {1, "silverdale.wa.us", 3, true}, + {1, "yarrow-point.wa.us", 3, true}, {1, "noticeable.news", 2, true}, {1, "notion.site", 2, true}, {1, "dnsking.ch", 2, true}, @@ -9544,7 +9588,6 @@ var r = [10154]Rule{ {1, "ox.rs", 2, true}, {1, "oy.lc", 2, true}, {1, "pgfog.com", 2, true}, - {1, "pagexl.com", 2, true}, {1, "gotpantheon.com", 2, true}, {1, "pantheonsite.io", 2, true}, {2, "paywhirl.com", 3, true}, @@ -9555,6 +9598,7 @@ var r = [10154]Rule{ {1, "gh.srv.us", 3, true}, {1, "gl.srv.us", 3, true}, {1, "mypep.link", 2, true}, + {1, "pplx.app", 2, true}, {1, "perspecta.cloud", 2, true}, {1, "forgeblocks.com", 2, true}, {1, "id.forgerock.io", 3, true}, @@ -9843,6 +9887,9 @@ var r = [10154]Rule{ {1, "co.ua", 2, true}, {1, "pp.ua", 2, true}, {1, "as.sh.cn", 3, true}, + {1, "vicp.fun", 2, true}, + {1, "yicp.fun", 2, true}, + {1, "zicp.fun", 2, true}, {1, "sheezy.games", 2, true}, {1, "myshopblocks.com", 2, true}, {1, "myshopify.com", 2, true}, @@ -9915,6 +9962,7 @@ var r = [10154]Rule{ {1, "stackit.rocks", 2, true}, {1, "stackit.run", 2, true}, {1, "stackit.zone", 2, true}, + {1, "sryze.cc", 2, true}, {1, "indevs.in", 2, true}, {1, "musician.io", 2, true}, {1, "novecore.site", 2, true}, @@ -10040,6 +10088,7 @@ var r = [10154]Rule{ {2, "transurl.eu", 3, true}, {1, "site.transip.me", 3, true}, {2, "transurl.nl", 3, true}, + {2, "triton.zone", 3, true}, {1, "tunnelmole.net", 2, true}, {1, "tuxfamily.org", 2, true}, {1, "typedream.app", 2, true}, @@ -10078,6 +10127,8 @@ var r = [10154]Rule{ {1, "v-info.info", 2, true}, {1, "vistablog.ir", 2, true}, {1, "deus-canvas.com", 2, true}, + {1, "vivenushop.com", 2, true}, + {1, "vivenushop.dev", 2, true}, {1, "voorloper.cloud", 2, true}, {2, "vultrobjects.com", 3, true}, {1, "wafflecell.com", 2, true}, @@ -10157,6 +10208,8 @@ var r = [10154]Rule{ {1, "zap.cloud", 2, true}, {1, "zeabur.app", 2, true}, {2, "zerops.app", 3, true}, + {1, "prg1-zerops.zone", 2, true}, + {2, "zerops.zone", 3, true}, {1, "bss.design", 2, true}, {1, "basicserver.io", 2, true}, {1, "virtualserver.io", 2, true}, diff --git a/vendor/github.com/zmap/zcrypto/json/ecdhe.go b/vendor/github.com/zmap/zcrypto/json/ecdhe.go index aa808ab1c62..0029539c61c 100644 --- a/vendor/github.com/zmap/zcrypto/json/ecdhe.go +++ b/vendor/github.com/zmap/zcrypto/json/ecdhe.go @@ -49,12 +49,17 @@ type ECPoint struct { // MarshalJSON implements the json.Marshler interface func (p *ECPoint) MarshalJSON() ([]byte, error) { + var y *cryptoParameter + if p.Y != nil { // Not present for x25519 + y = &cryptoParameter{Int: p.Y} + } + aux := struct { X *cryptoParameter `json:"x"` - Y *cryptoParameter `json:"y"` + Y *cryptoParameter `json:"y,omitempty"` }{ X: &cryptoParameter{Int: p.X}, - Y: &cryptoParameter{Int: p.Y}, + Y: y, } return json.Marshal(&aux) } diff --git a/vendor/github.com/zmap/zcrypto/json/names.go b/vendor/github.com/zmap/zcrypto/json/names.go index 38828248679..efbafa8d5d6 100644 --- a/vendor/github.com/zmap/zcrypto/json/names.go +++ b/vendor/github.com/zmap/zcrypto/json/names.go @@ -45,6 +45,7 @@ const ( BrainpoolP256r1 TLSCurveID = 26 BrainpoolP384r1 TLSCurveID = 27 BrainpoolP512r1 TLSCurveID = 28 + X25519 TLSCurveID = 29 ) var ecIDToName map[TLSCurveID]string @@ -80,6 +81,7 @@ func init() { ecIDToName[BrainpoolP256r1] = "brainpoolp256r1" ecIDToName[BrainpoolP384r1] = "brainpoolp384r1" ecIDToName[BrainpoolP512r1] = "brainpoolp512r1" + ecIDToName[X25519] = "x25519" ecNameToID = make(map[string]TLSCurveID, 64) ecNameToID["sect163k1"] = Sect163k1 diff --git a/vendor/github.com/zmap/zcrypto/json/rsa.go b/vendor/github.com/zmap/zcrypto/json/rsa.go index 270256973b7..10ae71c0268 100644 --- a/vendor/github.com/zmap/zcrypto/json/rsa.go +++ b/vendor/github.com/zmap/zcrypto/json/rsa.go @@ -15,10 +15,11 @@ package json import ( - "crypto/rsa" "encoding/json" "fmt" "math/big" + + "github.com/zmap/zcrypto/rsa" ) // RSAPublicKey provides JSON methods for the standard rsa.PublicKey. @@ -26,10 +27,13 @@ type RSAPublicKey struct { *rsa.PublicKey } +// ZCrypto - auxRSAPublicKey uses json.Number for the exponent so that +// arbitrarily large values can be marshaled as JSON integers (not strings). +// Original: Exponent int type auxRSAPublicKey struct { - Exponent int `json:"exponent"` - Modulus []byte `json:"modulus"` - Length int `json:"length"` + Exponent json.Number `json:"exponent"` + Modulus []byte `json:"modulus"` + Length int `json:"length"` } // RSAClientParams are the TLS key exchange parameters for RSA keys. @@ -42,7 +46,7 @@ type RSAClientParams struct { func (rp *RSAPublicKey) MarshalJSON() ([]byte, error) { var aux auxRSAPublicKey if rp.PublicKey != nil { - aux.Exponent = rp.E + aux.Exponent = json.Number(rp.E.String()) aux.Modulus = rp.N.Bytes() aux.Length = len(aux.Modulus) * 8 } @@ -58,7 +62,11 @@ func (rp *RSAPublicKey) UnmarshalJSON(b []byte) error { if rp.PublicKey == nil { rp.PublicKey = new(rsa.PublicKey) } - rp.E = aux.Exponent + rp.E = new(big.Int) + _, ok := rp.E.SetString(aux.Exponent.String(), 10) // ZCrypto - to handle arbitrarily large exponents, we use bigint + if !ok { + return fmt.Errorf("failed to parse exponent: %s", aux.Exponent.String()) + } rp.N = big.NewInt(0).SetBytes(aux.Modulus) if len(aux.Modulus)*8 != aux.Length { return fmt.Errorf("mismatched length (got %d, field specified %d)", len(aux.Modulus), aux.Length) diff --git a/vendor/github.com/zmap/zcrypto/rsa/FORK_SOURCE.md b/vendor/github.com/zmap/zcrypto/rsa/FORK_SOURCE.md new file mode 100644 index 00000000000..5d49886fee7 --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/rsa/FORK_SOURCE.md @@ -0,0 +1,2 @@ +The files in this rsa/ folder were taken from release-branch.go1.23 crypto/rsa and any modifications are annotated. +https://github.com/golang/go/tree/go1.23.0/src/crypto/rsa \ No newline at end of file diff --git a/vendor/github.com/zmap/zcrypto/rsa/pkcs1v15.go b/vendor/github.com/zmap/zcrypto/rsa/pkcs1v15.go new file mode 100644 index 00000000000..26dd5402bae --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/rsa/pkcs1v15.go @@ -0,0 +1,373 @@ +// 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 file. + +package rsa + +import ( + "bytes" + "crypto" + // ZCrypto - crypto/internal/randutil removed; MaybeReadByte call removed below + // "crypto/internal/randutil" + "crypto/subtle" + "errors" + "io" +) + +// This file implements encryption and decryption using PKCS #1 v1.5 padding. + +// PKCS1v15DecryptOptions is for passing options to PKCS #1 v1.5 decryption using +// the [crypto.Decrypter] interface. +type PKCS1v15DecryptOptions struct { + // SessionKeyLen is the length of the session key that is being + // decrypted. If not zero, then a padding error during decryption will + // cause a random plaintext of this length to be returned rather than + // an error. These alternatives happen in constant time. + SessionKeyLen int +} + +// EncryptPKCS1v15 encrypts the given message with RSA and the padding +// scheme from PKCS #1 v1.5. The message must be no longer than the +// length of the public modulus minus 11 bytes. +// +// The random parameter is used as a source of entropy to ensure that +// encrypting the same message twice doesn't result in the same +// ciphertext. Most applications should use [crypto/rand.Reader] +// as random. Note that the returned ciphertext does not depend +// deterministically on the bytes read from random, and may change +// between calls and/or between versions. +// +// WARNING: use of this function to encrypt plaintexts other than +// session keys is dangerous. Use RSA OAEP in new protocols. +func EncryptPKCS1v15(random io.Reader, pub *PublicKey, msg []byte) ([]byte, error) { + // ZCrypto - randutil.MaybeReadByte removed (crypto/internal dependency) + // randutil.MaybeReadByte(random) + + if err := checkPub(pub); err != nil { + return nil, err + } + k := pub.Size() + if len(msg) > k-11 { + return nil, ErrMessageTooLong + } + + // ZCrypto - boring removed + // if boring.Enabled && random == boring.RandReader { + // bkey, err := boringPublicKey(pub) + // if err != nil { + // return nil, err + // } + // return boring.EncryptRSAPKCS1(bkey, msg) + // } + // boring.UnreachableExceptTests() + + // EM = 0x00 || 0x02 || PS || 0x00 || M + em := make([]byte, k) + em[1] = 2 + ps, mm := em[2:len(em)-len(msg)-1], em[len(em)-len(msg):] + err := nonZeroRandomBytes(ps, random) + if err != nil { + return nil, err + } + em[len(em)-len(msg)-1] = 0 + copy(mm, msg) + + // ZCrypto - boring removed + // if boring.Enabled { + // var bkey *boring.PublicKeyRSA + // bkey, err = boringPublicKey(pub) + // if err != nil { + // return nil, err + // } + // return boring.EncryptRSANoPadding(bkey, em) + // } + + return encrypt(pub, em) +} + +// DecryptPKCS1v15 decrypts a plaintext using RSA and the padding scheme from PKCS #1 v1.5. +// The random parameter is legacy and ignored, and it can be nil. +// +// Note that whether this function returns an error or not discloses secret +// information. If an attacker can cause this function to run repeatedly and +// learn whether each instance returned an error then they can decrypt and +// forge signatures as if they had the private key. See +// DecryptPKCS1v15SessionKey for a way of solving this problem. +func DecryptPKCS1v15(random io.Reader, priv *PrivateKey, ciphertext []byte) ([]byte, error) { + if err := checkPub(&priv.PublicKey); err != nil { + return nil, err + } + + // ZCrypto - boring removed + // if boring.Enabled { + // bkey, err := boringPrivateKey(priv) + // if err != nil { + // return nil, err + // } + // out, err := boring.DecryptRSAPKCS1(bkey, ciphertext) + // if err != nil { + // return nil, ErrDecryption + // } + // return out, nil + // } + + valid, out, index, err := decryptPKCS1v15(priv, ciphertext) + if err != nil { + return nil, err + } + if valid == 0 { + return nil, ErrDecryption + } + return out[index:], nil +} + +// DecryptPKCS1v15SessionKey decrypts a session key using RSA and the padding +// scheme from PKCS #1 v1.5. The random parameter is legacy and ignored, and it +// can be nil. +// +// DecryptPKCS1v15SessionKey returns an error if the ciphertext is the wrong +// length or if the ciphertext is greater than the public modulus. Otherwise, no +// error is returned. If the padding is valid, the resulting plaintext message +// is copied into key. Otherwise, key is unchanged. These alternatives occur in +// constant time. It is intended that the user of this function generate a +// random session key beforehand and continue the protocol with the resulting +// value. +// +// Note that if the session key is too small then it may be possible for an +// attacker to brute-force it. If they can do that then they can learn whether a +// random value was used (because it'll be different for the same ciphertext) +// and thus whether the padding was correct. This also defeats the point of this +// function. Using at least a 16-byte key will protect against this attack. +// +// This method implements protections against Bleichenbacher chosen ciphertext +// attacks [0] described in RFC 3218 Section 2.3.2 [1]. While these protections +// make a Bleichenbacher attack significantly more difficult, the protections +// are only effective if the rest of the protocol which uses +// DecryptPKCS1v15SessionKey is designed with these considerations in mind. In +// particular, if any subsequent operations which use the decrypted session key +// leak any information about the key (e.g. whether it is a static or random +// key) then the mitigations are defeated. This method must be used extremely +// carefully, and typically should only be used when absolutely necessary for +// compatibility with an existing protocol (such as TLS) that is designed with +// these properties in mind. +// +// - [0] “Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption +// Standard PKCS #1”, Daniel Bleichenbacher, Advances in Cryptology (Crypto '98) +// - [1] RFC 3218, Preventing the Million Message Attack on CMS, +// https://www.rfc-editor.org/rfc/rfc3218.html +func DecryptPKCS1v15SessionKey(random io.Reader, priv *PrivateKey, ciphertext []byte, key []byte) error { + if err := checkPub(&priv.PublicKey); err != nil { + return err + } + k := priv.Size() + if k-(len(key)+3+8) < 0 { + return ErrDecryption + } + + valid, em, index, err := decryptPKCS1v15(priv, ciphertext) + if err != nil { + return err + } + + if len(em) != k { + // This should be impossible because decryptPKCS1v15 always + // returns the full slice. + return ErrDecryption + } + + valid &= subtle.ConstantTimeEq(int32(len(em)-index), int32(len(key))) + subtle.ConstantTimeCopy(valid, key, em[len(em)-len(key):]) + return nil +} + +// decryptPKCS1v15 decrypts ciphertext using priv. It returns one or zero in +// valid that indicates whether the plaintext was correctly structured. +// In either case, the plaintext is returned in em so that it may be read +// independently of whether it was valid in order to maintain constant memory +// access patterns. If the plaintext was valid then index contains the index of +// the original message in em, to allow constant time padding removal. +func decryptPKCS1v15(priv *PrivateKey, ciphertext []byte) (valid int, em []byte, index int, err error) { + k := priv.Size() + if k < 11 { + err = ErrDecryption + return + } + + // ZCrypto - boring removed + // if boring.Enabled { + // var bkey *boring.PrivateKeyRSA + // bkey, err = boringPrivateKey(priv) + // ... + // em, err = boring.DecryptRSANoPadding(bkey, ciphertext) + // } else { + em, err = decrypt(priv, ciphertext, noCheck) + if err != nil { + return + } + // } + + firstByteIsZero := subtle.ConstantTimeByteEq(em[0], 0) + secondByteIsTwo := subtle.ConstantTimeByteEq(em[1], 2) + + // The remainder of the plaintext must be a string of non-zero random + // octets, followed by a 0, followed by the message. + // lookingForIndex: 1 iff we are still looking for the zero. + // index: the offset of the first zero byte. + lookingForIndex := 1 + + for i := 2; i < len(em); i++ { + equals0 := subtle.ConstantTimeByteEq(em[i], 0) + index = subtle.ConstantTimeSelect(lookingForIndex&equals0, i, index) + lookingForIndex = subtle.ConstantTimeSelect(equals0, 0, lookingForIndex) + } + + // The PS padding must be at least 8 bytes long, and it starts two + // bytes into em. + validPS := subtle.ConstantTimeLessOrEq(2+8, index) + + valid = firstByteIsZero & secondByteIsTwo & (^lookingForIndex & 1) & validPS + index = subtle.ConstantTimeSelect(valid, index+1, 0) + return valid, em, index, nil +} + +// nonZeroRandomBytes fills the given slice with non-zero random octets. +func nonZeroRandomBytes(s []byte, random io.Reader) (err error) { + _, err = io.ReadFull(random, s) + if err != nil { + return + } + + for i := 0; i < len(s); i++ { + for s[i] == 0 { + _, err = io.ReadFull(random, s[i:i+1]) + if err != nil { + return + } + // In tests, the PRNG may return all zeros so we do + // this to break the loop. + s[i] ^= 0x42 + } + } + + return +} + +// These are ASN1 DER structures: +// +// DigestInfo ::= SEQUENCE { +// digestAlgorithm AlgorithmIdentifier, +// digest OCTET STRING +// } +// +// For performance, we don't use the generic ASN1 encoder. Rather, we +// precompute a prefix of the digest value that makes a valid ASN1 DER string +// with the correct contents. +var hashPrefixes = map[crypto.Hash][]byte{ + crypto.MD5: {0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, 0x10}, + crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14}, + crypto.SHA224: {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c}, + crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, + crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, + crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, + crypto.MD5SHA1: {}, // A special TLS case which doesn't use an ASN1 prefix. + crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14}, +} + +// SignPKCS1v15 calculates the signature of hashed using +// RSASSA-PKCS1-V1_5-SIGN from RSA PKCS #1 v1.5. Note that hashed must +// be the result of hashing the input message using the given hash +// function. If hash is zero, hashed is signed directly. This isn't +// advisable except for interoperability. +// +// The random parameter is legacy and ignored, and it can be nil. +// +// This function is deterministic. Thus, if the set of possible +// messages is small, an attacker may be able to build a map from +// messages to signatures and identify the signed messages. As ever, +// signatures provide authenticity, not confidentiality. +func SignPKCS1v15(random io.Reader, priv *PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) { + // pkcs1v15ConstructEM is called before boring.SignRSAPKCS1v15 to return + // consistent errors, including ErrMessageTooLong. + em, err := pkcs1v15ConstructEM(&priv.PublicKey, hash, hashed) + if err != nil { + return nil, err + } + + // ZCrypto - boring removed + // if boring.Enabled { + // bkey, err := boringPrivateKey(priv) + // ... + // return boring.SignRSAPKCS1v15(bkey, hash, hashed) + // } + + return decrypt(priv, em, withCheck) +} + +func pkcs1v15ConstructEM(pub *PublicKey, hash crypto.Hash, hashed []byte) ([]byte, error) { + // Special case: crypto.Hash(0) is used to indicate that the data is + // signed directly. + var prefix []byte + if hash != 0 { + if len(hashed) != hash.Size() { + return nil, errors.New("crypto/rsa: input must be hashed message") + } + var ok bool + prefix, ok = hashPrefixes[hash] + if !ok { + return nil, errors.New("crypto/rsa: unsupported hash function") + } + } + + // EM = 0x00 || 0x01 || PS || 0x00 || T + k := pub.Size() + if k < len(prefix)+len(hashed)+2+8+1 { + return nil, ErrMessageTooLong + } + em := make([]byte, k) + em[1] = 1 + for i := 2; i < k-len(prefix)-len(hashed)-1; i++ { + em[i] = 0xff + } + copy(em[k-len(prefix)-len(hashed):], prefix) + copy(em[k-len(hashed):], hashed) + return em, nil +} + +// VerifyPKCS1v15 verifies an RSA PKCS #1 v1.5 signature. +// hashed is the result of hashing the input message using the given hash +// function and sig is the signature. A valid signature is indicated by +// returning a nil error. If hash is zero then hashed is used directly. This +// isn't advisable except for interoperability. +// +// The inputs are not considered confidential, and may leak through timing side +// channels, or if an attacker has control of part of the inputs. +func VerifyPKCS1v15(pub *PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error { + // ZCrypto - boring removed + // if boring.Enabled { + // bkey, err := boringPublicKey(pub) + // ... + // return boring.VerifyRSAPKCS1v15(bkey, hash, hashed, sig) + // } + + // RFC 8017 Section 8.2.2: If the length of the signature S is not k + // octets (where k is the length in octets of the RSA modulus n), output + // "invalid signature" and stop. + if pub.Size() != len(sig) { + return ErrVerification + } + + em, err := encrypt(pub, sig) + if err != nil { + return ErrVerification + } + + expected, err := pkcs1v15ConstructEM(pub, hash, hashed) + if err != nil { + return ErrVerification + } + if !bytes.Equal(em, expected) { + return ErrVerification + } + + return nil +} diff --git a/vendor/github.com/zmap/zcrypto/rsa/pss.go b/vendor/github.com/zmap/zcrypto/rsa/pss.go new file mode 100644 index 00000000000..7c5a8fbd658 --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/rsa/pss.go @@ -0,0 +1,375 @@ +// Copyright 2013 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 file. + +package rsa + +// This file implements the RSASSA-PSS signature scheme according to RFC 8017. + +import ( + "bytes" + "crypto" + // ZCrypto - crypto/internal/boring removed + // "crypto/internal/boring" + "errors" + "hash" + "io" +) + +// Per RFC 8017, Section 9.1 +// +// EM = MGF1 xor DB || H( 8*0x00 || mHash || salt ) || 0xbc +// +// where +// +// DB = PS || 0x01 || salt +// +// and PS can be empty so +// +// emLen = dbLen + hLen + 1 = psLen + sLen + hLen + 2 +// + +func emsaPSSEncode(mHash []byte, emBits int, salt []byte, hash hash.Hash) ([]byte, error) { + // See RFC 8017, Section 9.1.1. + + hLen := hash.Size() + sLen := len(salt) + emLen := (emBits + 7) / 8 + + // 1. If the length of M is greater than the input limitation for the + // hash function (2^61 - 1 octets for SHA-1), output "message too + // long" and stop. + // + // 2. Let mHash = Hash(M), an octet string of length hLen. + + if len(mHash) != hLen { + return nil, errors.New("crypto/rsa: input must be hashed with given hash") + } + + // 3. If emLen < hLen + sLen + 2, output "encoding error" and stop. + + if emLen < hLen+sLen+2 { + return nil, ErrMessageTooLong + } + + em := make([]byte, emLen) + psLen := emLen - sLen - hLen - 2 + db := em[:psLen+1+sLen] + h := em[psLen+1+sLen : emLen-1] + + // 4. Generate a random octet string salt of length sLen; if sLen = 0, + // then salt is the empty string. + // + // 5. Let + // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt; + // + // M' is an octet string of length 8 + hLen + sLen with eight + // initial zero octets. + // + // 6. Let H = Hash(M'), an octet string of length hLen. + + var prefix [8]byte + + hash.Write(prefix[:]) + hash.Write(mHash) + hash.Write(salt) + + h = hash.Sum(h[:0]) + hash.Reset() + + // 7. Generate an octet string PS consisting of emLen - sLen - hLen - 2 + // zero octets. The length of PS may be 0. + // + // 8. Let DB = PS || 0x01 || salt; DB is an octet string of length + // emLen - hLen - 1. + + db[psLen] = 0x01 + copy(db[psLen+1:], salt) + + // 9. Let dbMask = MGF(H, emLen - hLen - 1). + // + // 10. Let maskedDB = DB \xor dbMask. + + mgf1XOR(db, hash, h) + + // 11. Set the leftmost 8 * emLen - emBits bits of the leftmost octet in + // maskedDB to zero. + + db[0] &= 0xff >> (8*emLen - emBits) + + // 12. Let EM = maskedDB || H || 0xbc. + em[emLen-1] = 0xbc + + // 13. Output EM. + return em, nil +} + +func emsaPSSVerify(mHash, em []byte, emBits, sLen int, hash hash.Hash) error { + // See RFC 8017, Section 9.1.2. + + hLen := hash.Size() + if sLen == PSSSaltLengthEqualsHash { + sLen = hLen + } + emLen := (emBits + 7) / 8 + if emLen != len(em) { + return errors.New("rsa: internal error: inconsistent length") + } + + // 1. If the length of M is greater than the input limitation for the + // hash function (2^61 - 1 octets for SHA-1), output "inconsistent" + // and stop. + // + // 2. Let mHash = Hash(M), an octet string of length hLen. + if hLen != len(mHash) { + return ErrVerification + } + + // 3. If emLen < hLen + sLen + 2, output "inconsistent" and stop. + if emLen < hLen+sLen+2 { + return ErrVerification + } + + // 4. If the rightmost octet of EM does not have hexadecimal value + // 0xbc, output "inconsistent" and stop. + if em[emLen-1] != 0xbc { + return ErrVerification + } + + // 5. Let maskedDB be the leftmost emLen - hLen - 1 octets of EM, and + // let H be the next hLen octets. + db := em[:emLen-hLen-1] + h := em[emLen-hLen-1 : emLen-1] + + // 6. If the leftmost 8 * emLen - emBits bits of the leftmost octet in + // maskedDB are not all equal to zero, output "inconsistent" and + // stop. + var bitMask byte = 0xff >> (8*emLen - emBits) + if em[0] & ^bitMask != 0 { + return ErrVerification + } + + // 7. Let dbMask = MGF(H, emLen - hLen - 1). + // + // 8. Let DB = maskedDB \xor dbMask. + mgf1XOR(db, hash, h) + + // 9. Set the leftmost 8 * emLen - emBits bits of the leftmost octet in DB + // to zero. + db[0] &= bitMask + + // If we don't know the salt length, look for the 0x01 delimiter. + if sLen == PSSSaltLengthAuto { + psLen := bytes.IndexByte(db, 0x01) + if psLen < 0 { + return ErrVerification + } + sLen = len(db) - psLen - 1 + } + + // 10. If the emLen - hLen - sLen - 2 leftmost octets of DB are not zero + // or if the octet at position emLen - hLen - sLen - 1 (the leftmost + // position is "position 1") does not have hexadecimal value 0x01, + // output "inconsistent" and stop. + psLen := emLen - hLen - sLen - 2 + for _, e := range db[:psLen] { + if e != 0x00 { + return ErrVerification + } + } + if db[psLen] != 0x01 { + return ErrVerification + } + + // 11. Let salt be the last sLen octets of DB. + salt := db[len(db)-sLen:] + + // 12. Let + // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt ; + // M' is an octet string of length 8 + hLen + sLen with eight + // initial zero octets. + // + // 13. Let H' = Hash(M'), an octet string of length hLen. + var prefix [8]byte + hash.Write(prefix[:]) + hash.Write(mHash) + hash.Write(salt) + + h0 := hash.Sum(nil) + + // 14. If H = H', output "consistent." Otherwise, output "inconsistent." + if !bytes.Equal(h0, h) { // TODO: constant time? + return ErrVerification + } + return nil +} + +// signPSSWithSalt calculates the signature of hashed using PSS with specified salt. +// Note that hashed must be the result of hashing the input message using the +// given hash function. salt is a random sequence of bytes whose length will be +// later used to verify the signature. +func signPSSWithSalt(priv *PrivateKey, hash crypto.Hash, hashed, salt []byte) ([]byte, error) { + emBits := priv.N.BitLen() - 1 + em, err := emsaPSSEncode(hashed, emBits, salt, hash.New()) + if err != nil { + return nil, err + } + + // ZCrypto - boring removed + // if boring.Enabled { + // bkey, err := boringPrivateKey(priv) + // ... + // s, err := boring.DecryptRSANoPadding(bkey, em) + // return s, nil + // } + + // RFC 8017: "Note that the octet length of EM will be one less than k if + // modBits - 1 is divisible by 8 and equal to k otherwise, where k is the + // length in octets of the RSA modulus n." 🙄 + // + // This is extremely annoying, as all other encrypt and decrypt inputs are + // always the exact same size as the modulus. Since it only happens for + // weird modulus sizes, fix it by padding inefficiently. + if emLen, k := len(em), priv.Size(); emLen < k { + emNew := make([]byte, k) + copy(emNew[k-emLen:], em) + em = emNew + } + + return decrypt(priv, em, withCheck) +} + +const ( + // PSSSaltLengthAuto causes the salt in a PSS signature to be as large + // as possible when signing, and to be auto-detected when verifying. + PSSSaltLengthAuto = 0 + // PSSSaltLengthEqualsHash causes the salt length to equal the length + // of the hash used in the signature. + PSSSaltLengthEqualsHash = -1 +) + +// PSSOptions contains options for creating and verifying PSS signatures. +type PSSOptions struct { + // SaltLength controls the length of the salt used in the PSS signature. It + // can either be a positive number of bytes, or one of the special + // PSSSaltLength constants. + SaltLength int + + // Hash is the hash function used to generate the message digest. If not + // zero, it overrides the hash function passed to SignPSS. It's required + // when using PrivateKey.Sign. + Hash crypto.Hash +} + +// HashFunc returns opts.Hash so that [PSSOptions] implements [crypto.SignerOpts]. +func (opts *PSSOptions) HashFunc() crypto.Hash { + return opts.Hash +} + +func (opts *PSSOptions) saltLength() int { + if opts == nil { + return PSSSaltLengthAuto + } + return opts.SaltLength +} + +var invalidSaltLenErr = errors.New("crypto/rsa: PSSOptions.SaltLength cannot be negative") + +// SignPSS calculates the signature of digest using PSS. +// +// digest must be the result of hashing the input message using the given hash +// function. The opts argument may be nil, in which case sensible defaults are +// used. If opts.Hash is set, it overrides hash. +// +// The signature is randomized depending on the message, key, and salt size, +// using bytes from rand. Most applications should use [crypto/rand.Reader] as +// rand. +func SignPSS(rand io.Reader, priv *PrivateKey, hash crypto.Hash, digest []byte, opts *PSSOptions) ([]byte, error) { + // Note that while we don't commit to deterministic execution with respect + // to the rand stream, we also don't apply MaybeReadByte, so per Hyrum's Law + // it's probably relied upon by some. It's a tolerable promise because a + // well-specified number of random bytes is included in the signature, in a + // well-specified way. + + // ZCrypto - boring removed + // if boring.Enabled && rand == boring.RandReader { + // bkey, err := boringPrivateKey(priv) + // ... + // return boring.SignRSAPSS(bkey, hash, digest, opts.saltLength()) + // } + // boring.UnreachableExceptTests() + + if opts != nil && opts.Hash != 0 { + hash = opts.Hash + } + + saltLength := opts.saltLength() + switch saltLength { + case PSSSaltLengthAuto: + saltLength = (priv.N.BitLen()-1+7)/8 - 2 - hash.Size() + if saltLength < 0 { + return nil, ErrMessageTooLong + } + case PSSSaltLengthEqualsHash: + saltLength = hash.Size() + default: + // If we get here saltLength is either > 0 or < -1, in the + // latter case we fail out. + if saltLength <= 0 { + return nil, invalidSaltLenErr + } + } + salt := make([]byte, saltLength) + if _, err := io.ReadFull(rand, salt); err != nil { + return nil, err + } + return signPSSWithSalt(priv, hash, digest, salt) +} + +// VerifyPSS verifies a PSS signature. +// +// A valid signature is indicated by returning a nil error. digest must be the +// result of hashing the input message using the given hash function. The opts +// argument may be nil, in which case sensible defaults are used. opts.Hash is +// ignored. +// +// The inputs are not considered confidential, and may leak through timing side +// channels, or if an attacker has control of part of the inputs. +func VerifyPSS(pub *PublicKey, hash crypto.Hash, digest []byte, sig []byte, opts *PSSOptions) error { + // ZCrypto - boring removed + // if boring.Enabled { + // bkey, err := boringPublicKey(pub) + // ... + // return boring.VerifyRSAPSS(bkey, hash, digest, sig, opts.saltLength()) + // } + if len(sig) != pub.Size() { + return ErrVerification + } + // Salt length must be either one of the special constants (-1 or 0) + // or otherwise positive. If it is < PSSSaltLengthEqualsHash (-1) + // we return an error. + if opts.saltLength() < PSSSaltLengthEqualsHash { + return invalidSaltLenErr + } + + emBits := pub.N.BitLen() - 1 + emLen := (emBits + 7) / 8 + em, err := encrypt(pub, sig) + if err != nil { + return ErrVerification + } + + // Like in signPSSWithSalt, deal with mismatches between emLen and the size + // of the modulus. The spec would have us wire emLen into the encoding + // function, but we'd rather always encode to the size of the modulus and + // then strip leading zeroes if necessary. This only happens for weird + // modulus sizes anyway. + for len(em) > emLen && len(em) > 0 { + if em[0] != 0 { + return ErrVerification + } + em = em[1:] + } + + return emsaPSSVerify(digest, em, emBits, opts.saltLength(), hash.New()) +} diff --git a/vendor/github.com/zmap/zcrypto/rsa/rsa.go b/vendor/github.com/zmap/zcrypto/rsa/rsa.go new file mode 100644 index 00000000000..832ffd9189b --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/rsa/rsa.go @@ -0,0 +1,789 @@ +// 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 file. + +// Package rsa implements RSA encryption as specified in PKCS #1 and RFC 8017. +// +// RSA is a single, fundamental operation that is used in this package to +// implement either public-key encryption or public-key signatures. +// +// The original specification for encryption and signatures with RSA is PKCS #1 +// and the terms "RSA encryption" and "RSA signatures" by default refer to +// PKCS #1 version 1.5. However, that specification has flaws and new designs +// should use version 2, usually called by just OAEP and PSS, where +// possible. +// +// Two sets of interfaces are included in this package. When a more abstract +// interface isn't necessary, there are functions for encrypting/decrypting +// with v1.5/OAEP and signing/verifying with v1.5/PSS. If one needs to abstract +// over the public key primitive, the PrivateKey type implements the +// Decrypter and Signer interfaces from the crypto package. +// +// Operations involving private keys are implemented using constant-time +// algorithms, except for [GenerateKey], [PrivateKey.Precompute], and +// [PrivateKey.Validate]. +package rsa + +import ( + "crypto" + cryptorand "crypto/rand" // ZCrypto - added for GenerateKey; avoids conflict with io.Reader param named "random" + "crypto/subtle" + "errors" + "hash" + "io" + "math" + "math/big" + + "github.com/zmap/zcrypto/internal/randutil" +) + +var bigOne = big.NewInt(1) + +// A PublicKey represents the public part of an RSA key. +// +// The value of the modulus N is considered secret by this library and protected +// from leaking through timing side-channels. However, neither the value of the +// exponent E nor the precise bit size of N are similarly protected. +type PublicKey struct { + N *big.Int // modulus + // ZCrypto - modified to use big.Int + E *big.Int // public exponent +} + +// Any methods implemented on PublicKey might need to also be implemented on +// PrivateKey, as the latter embeds the former and will expose its methods. + +// Size returns the modulus size in bytes. Raw signatures and ciphertexts +// for or by this public key will have the same size. +func (pub *PublicKey) Size() int { + return (pub.N.BitLen() + 7) / 8 +} + +// Equal reports whether pub and x have the same value. +func (pub *PublicKey) Equal(x crypto.PublicKey) bool { + xx, ok := x.(*PublicKey) + if !ok { + return false + } + return bigIntEqual(pub.N, xx.N) && bigIntEqual(pub.E, xx.E) +} + +// OAEPOptions is an interface for passing options to OAEP decryption using the +// crypto.Decrypter interface. +type OAEPOptions struct { + // Hash is the hash function that will be used when generating the mask. + Hash crypto.Hash + + // MGFHash is the hash function used for MGF1. + // If zero, Hash is used instead. + MGFHash crypto.Hash + + // Label is an arbitrary byte string that must be equal to the value + // used when encrypting. + Label []byte +} + +var ( + errPublicModulus = errors.New("crypto/rsa: missing public modulus") + errPublicExponentSmall = errors.New("crypto/rsa: public exponent too small") + errPublicExponentLarge = errors.New("crypto/rsa: public exponent too large") +) + +// checkPub sanity checks the public key before we use it. +// ZCrypto - original comment: "We require pub.E to fit into a 32-bit integer so that we +// do not have different behavior depending on whether int is 32 or 64 bits." +// That constraint is removed here to allow arbitrarily large public exponents. +// See https://www.imperialviolet.org/2012/03/16/rsae.html. +func checkPub(pub *PublicKey) error { + if pub.N == nil { + return errPublicModulus + } + // ZCrypto - E is now *big.Int; nil check added, int comparisons replaced with Cmp + // if pub.E < 2 { + // return errPublicExponentSmall + // } + // if pub.E > 1<<31-1 { + // return errPublicExponentLarge + // } + if pub.E == nil || pub.E.Cmp(big.NewInt(2)) < 0 { + return errPublicExponentSmall + } + // ZCrypto - upper bound check removed; zcrypto supports arbitrarily large public exponents + return nil +} + +// A PrivateKey represents an RSA key +type PrivateKey struct { + PublicKey // public part. + D *big.Int // private exponent + Primes []*big.Int // prime factors of N, has >= 2 elements. + + // Precomputed contains precomputed values that speed up RSA operations, + // if available. It must be generated by calling PrivateKey.Precompute and + // must not be modified. + Precomputed PrecomputedValues +} + +// Public returns the public key corresponding to priv. +func (priv *PrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +// Equal reports whether priv and x have equivalent values. It ignores +// Precomputed values. +func (priv *PrivateKey) Equal(x crypto.PrivateKey) bool { + xx, ok := x.(*PrivateKey) + if !ok { + return false + } + if !priv.PublicKey.Equal(&xx.PublicKey) || !bigIntEqual(priv.D, xx.D) { + return false + } + if len(priv.Primes) != len(xx.Primes) { + return false + } + for i := range priv.Primes { + if !bigIntEqual(priv.Primes[i], xx.Primes[i]) { + return false + } + } + return true +} + +// bigIntEqual reports whether a and b are equal leaking only their bit length +// through timing side-channels. +func bigIntEqual(a, b *big.Int) bool { + return subtle.ConstantTimeCompare(a.Bytes(), b.Bytes()) == 1 +} + +// Sign signs digest with priv, reading randomness from rand. If opts is a +// *[PSSOptions] then the PSS algorithm will be used, otherwise PKCS #1 v1.5 will +// be used. digest must be the result of hashing the input message using +// opts.HashFunc(). +// +// This method implements [crypto.Signer], which is an interface to support keys +// where the private part is kept in, for example, a hardware module. Common +// uses should use the Sign* functions in this package directly. +func (priv *PrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if pssOpts, ok := opts.(*PSSOptions); ok { + return SignPSS(rand, priv, pssOpts.Hash, digest, pssOpts) + } + + return SignPKCS1v15(rand, priv, opts.HashFunc(), digest) +} + +// Decrypt decrypts ciphertext with priv. If opts is nil or of type +// *[PKCS1v15DecryptOptions] then PKCS #1 v1.5 decryption is performed. Otherwise +// opts must have type *[OAEPOptions] and OAEP decryption is done. +func (priv *PrivateKey) Decrypt(rand io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { + if opts == nil { + return DecryptPKCS1v15(rand, priv, ciphertext) + } + + switch opts := opts.(type) { + case *OAEPOptions: + if opts.MGFHash == 0 { + return decryptOAEP(opts.Hash.New(), opts.Hash.New(), rand, priv, ciphertext, opts.Label) + } else { + return decryptOAEP(opts.Hash.New(), opts.MGFHash.New(), rand, priv, ciphertext, opts.Label) + } + + case *PKCS1v15DecryptOptions: + if l := opts.SessionKeyLen; l > 0 { + plaintext = make([]byte, l) + if _, err := io.ReadFull(rand, plaintext); err != nil { + return nil, err + } + if err := DecryptPKCS1v15SessionKey(rand, priv, ciphertext, plaintext); err != nil { + return nil, err + } + return plaintext, nil + } else { + return DecryptPKCS1v15(rand, priv, ciphertext) + } + + default: + return nil, errors.New("crypto/rsa: invalid options for Decrypt") + } +} + +type PrecomputedValues struct { + Dp, Dq *big.Int // D mod (P-1) (or mod Q-1) + Qinv *big.Int // Q^-1 mod P + + // CRTValues is used for the 3rd and subsequent primes. Due to a + // historical accident, the CRT for the first two primes is handled + // differently in PKCS #1 and interoperability is sufficiently + // important that we mirror this. + // + // Deprecated: These values are still filled in by Precompute for + // backwards compatibility but are not used. Multi-prime RSA is very rare, + // and is implemented by this package without CRT optimizations to limit + // complexity. + CRTValues []CRTValue + + // ZCrypto - bigmod moduli removed; math/big is used directly in decrypt + // n, p, q *bigmod.Modulus +} + +// CRTValue contains the precomputed Chinese remainder theorem values. +type CRTValue struct { + Exp *big.Int // D mod (prime-1). + Coeff *big.Int // R·Coeff ≡ 1 mod Prime. + R *big.Int // product of primes prior to this (inc p and q). +} + +// Validate performs basic sanity checks on the key. +// It returns nil if the key is valid, or else an error describing a problem. +func (priv *PrivateKey) Validate() error { + if err := checkPub(&priv.PublicKey); err != nil { + return err + } + + // Check that Πprimes == n. + modulus := new(big.Int).Set(bigOne) + for _, prime := range priv.Primes { + // Any primes ≤ 1 will cause divide-by-zero panics later. + if prime.Cmp(bigOne) <= 0 { + return errors.New("crypto/rsa: invalid prime value") + } + modulus.Mul(modulus, prime) + } + if modulus.Cmp(priv.N) != 0 { + return errors.New("crypto/rsa: invalid modulus") + } + + // Check that de ≡ 1 mod p-1, for each prime. + // This implies that e is coprime to each p-1 as e has a multiplicative + // inverse. Therefore e is coprime to lcm(p-1,q-1,r-1,...) = + // exponent(ℤ/nℤ). It also implies that a^de ≡ a mod p as a^(p-1) ≡ 1 + // mod p. Thus a^de ≡ a mod n for all a coprime to n, as required. + congruence := new(big.Int) + // ZCrypto - E is now *big.Int; use Set instead of SetInt64 + // de := new(big.Int).SetInt64(int64(priv.E)) + de := new(big.Int).Set(priv.E) + de.Mul(de, priv.D) + for _, prime := range priv.Primes { + pminus1 := new(big.Int).Sub(prime, bigOne) + congruence.Mod(de, pminus1) + if congruence.Cmp(bigOne) != 0 { + return errors.New("crypto/rsa: invalid exponents") + } + } + return nil +} + +// GenerateKey generates a random RSA private key of the given bit size. +// +// Most applications should use [crypto/rand.Reader] as rand. Note that the +// returned key does not depend deterministically on the bytes read from rand, +// and may change between calls and/or between versions. +func GenerateKey(random io.Reader, bits int) (*PrivateKey, error) { + return GenerateMultiPrimeKey(random, 2, bits) +} + +// GenerateMultiPrimeKey generates a multi-prime RSA keypair of the given bit +// size and the given random source. +// +// Table 1 in "[On the Security of Multi-prime RSA]" suggests maximum numbers of +// primes for a given bit size. +// +// Although the public keys are compatible (actually, indistinguishable) from +// the 2-prime case, the private keys are not. Thus it may not be possible to +// export multi-prime private keys in certain formats or to subsequently import +// them into other code. +// +// This package does not implement CRT optimizations for multi-prime RSA, so the +// keys with more than two primes will have worse performance. +// +// Deprecated: The use of this function with a number of primes different from +// two is not recommended for the above security, compatibility, and performance +// reasons. Use [GenerateKey] instead. +// +// [On the Security of Multi-prime RSA]: http://www.cacr.math.uwaterloo.ca/techreports/2006/cacr2006-16.pdf +func GenerateMultiPrimeKey(random io.Reader, nprimes int, bits int) (*PrivateKey, error) { + randutil.MaybeReadByte(random) + + // ZCrypto - commented out since boring is never enabled + //if boring.Enabled && random == boring.RandReader && nprimes == 2 && + // (bits == 2048 || bits == 3072 || bits == 4096) { + // bN, bE, bD, bP, bQ, bDp, bDq, bQinv, err := boring.GenerateKeyRSA(bits) + // if err != nil { + // return nil, err + // } + // N := bbig.Dec(bN) + // E := bbig.Dec(bE) + // D := bbig.Dec(bD) + // P := bbig.Dec(bP) + // Q := bbig.Dec(bQ) + // Dp := bbig.Dec(bDp) + // Dq := bbig.Dec(bDq) + // Qinv := bbig.Dec(bQinv) + // e64 := E.Int64() + // if !E.IsInt64() || int64(int(e64)) != e64 { + // return nil, errors.New("crypto/rsa: generated key exponent too large") + // } + // + // mn, err := bigmod.NewModulusFromBig(N) + // if err != nil { + // return nil, err + // } + // mp, err := bigmod.NewModulusFromBig(P) + // if err != nil { + // return nil, err + // } + // mq, err := bigmod.NewModulusFromBig(Q) + // if err != nil { + // return nil, err + // } + // + // key := &PrivateKey{ + // PublicKey: PublicKey{ + // N: N, + // E: int(e64), + // }, + // D: D, + // Primes: []*big.Int{P, Q}, + // Precomputed: PrecomputedValues{ + // Dp: Dp, + // Dq: Dq, + // Qinv: Qinv, + // CRTValues: make([]CRTValue, 0), // non-nil, to match Precompute + // n: mn, + // p: mp, + // q: mq, + // }, + // } + // return key, nil + //} + + priv := new(PrivateKey) + priv.E = big.NewInt(65537) // ZCrypto - modified to use BigInt + + if nprimes < 2 { + return nil, errors.New("crypto/rsa: GenerateMultiPrimeKey: nprimes must be >= 2") + } + + if bits < 64 { + primeLimit := float64(uint64(1) << uint(bits/nprimes)) + // pi approximates the number of primes less than primeLimit + pi := primeLimit / (math.Log(primeLimit) - 1) + // Generated primes start with 11 (in binary) so we can only + // use a quarter of them. + pi /= 4 + // Use a factor of two to ensure that key generation terminates + // in a reasonable amount of time. + pi /= 2 + if pi <= float64(nprimes) { + return nil, errors.New("crypto/rsa: too few primes of given length to generate an RSA key") + } + } + + primes := make([]*big.Int, nprimes) + +NextSetOfPrimes: + for { + todo := bits + // crypto/rand should set the top two bits in each prime. + // Thus each prime has the form + // p_i = 2^bitlen(p_i) × 0.11... (in base 2). + // And the product is: + // P = 2^todo × α + // where α is the product of nprimes numbers of the form 0.11... + // + // If α < 1/2 (which can happen for nprimes > 2), we need to + // shift todo to compensate for lost bits: the mean value of 0.11... + // is 7/8, so todo + shift - nprimes * log2(7/8) ~= bits - 1/2 + // will give good results. + if nprimes >= 7 { + todo += (nprimes - 2) / 5 + } + for i := 0; i < nprimes; i++ { + var err error + // ZCrypto - swapped rand. for cryptorand + primes[i], err = cryptorand.Prime(random, todo/(nprimes-i)) + if err != nil { + return nil, err + } + todo -= primes[i].BitLen() + } + + // Make sure that primes is pairwise unequal. + for i, prime := range primes { + for j := 0; j < i; j++ { + if prime.Cmp(primes[j]) == 0 { + continue NextSetOfPrimes + } + } + } + + n := new(big.Int).Set(bigOne) + totient := new(big.Int).Set(bigOne) + pminus1 := new(big.Int) + for _, prime := range primes { + n.Mul(n, prime) + pminus1.Sub(prime, bigOne) + totient.Mul(totient, pminus1) + } + if n.BitLen() != bits { + // This should never happen for nprimes == 2 because + // crypto/rand should set the top two bits in each prime. + // For nprimes > 2 we hope it does not happen often. + continue NextSetOfPrimes + } + + priv.D = new(big.Int) + e := priv.E + ok := priv.D.ModInverse(e, totient) + + if ok != nil { + priv.Primes = primes + priv.N = n + break + } + } + + priv.Precompute() + return priv, nil +} + +// incCounter increments a four byte, big-endian counter. +func incCounter(c *[4]byte) { + if c[3]++; c[3] != 0 { + return + } + if c[2]++; c[2] != 0 { + return + } + if c[1]++; c[1] != 0 { + return + } + c[0]++ +} + +// mgf1XOR XORs the bytes in out with a mask generated using the MGF1 function +// specified in PKCS #1 v2.1. +func mgf1XOR(out []byte, hash hash.Hash, seed []byte) { + var counter [4]byte + var digest []byte + + done := 0 + for done < len(out) { + hash.Write(seed) + hash.Write(counter[0:4]) + digest = hash.Sum(digest[:0]) + hash.Reset() + + for i := 0; i < len(digest) && done < len(out); i++ { + out[done] ^= digest[i] + done++ + } + incCounter(&counter) + } +} + +// ErrMessageTooLong is returned when attempting to encrypt or sign a message +// which is too large for the size of the key. When using [SignPSS], this can also +// be returned if the size of the salt is too large. +var ErrMessageTooLong = errors.New("crypto/rsa: message too long for RSA key size") + +// ZCrypto - replaced bigmod with math/big to remove crypto/internal dependency; +// E is now *big.Int so the uint cast is gone. Constant-time guarantees are not +// required here — zcrypto is used for scanning/research, not production crypto. +// Original: +// +// N, err := bigmod.NewModulusFromBig(pub.N) +// m, err := bigmod.NewNat().SetBytes(plaintext, N) +// e := uint(pub.E) +// return bigmod.NewNat().ExpShortVarTime(m, e, N).Bytes(N), nil +func encrypt(pub *PublicKey, plaintext []byte) ([]byte, error) { + m := new(big.Int).SetBytes(plaintext) + if m.Cmp(pub.N) >= 0 { + return nil, errors.New("crypto/rsa: message too large for modulus") + } + c := new(big.Int).Exp(m, pub.E, pub.N) + em := make([]byte, pub.Size()) + cBytes := c.Bytes() + copy(em[len(em)-len(cBytes):], cBytes) + return em, nil +} + +// EncryptOAEP encrypts the given message with RSA-OAEP. +// +// OAEP is parameterised by a hash function that is used as a random oracle. +// Encryption and decryption of a given message must use the same hash function +// and sha256.New() is a reasonable choice. +// +// The random parameter is used as a source of entropy to ensure that +// encrypting the same message twice doesn't result in the same ciphertext. +// Most applications should use [crypto/rand.Reader] as random. +// +// The label parameter may contain arbitrary data that will not be encrypted, +// but which gives important context to the message. For example, if a given +// public key is used to encrypt two types of messages then distinct label +// values could be used to ensure that a ciphertext for one purpose cannot be +// used for another by an attacker. If not required it can be empty. +// +// The message must be no longer than the length of the public modulus minus +// twice the hash length, minus a further 2. +func EncryptOAEP(hash hash.Hash, random io.Reader, pub *PublicKey, msg []byte, label []byte) ([]byte, error) { + // Note that while we don't commit to deterministic execution with respect + // to the random stream, we also don't apply MaybeReadByte, so per Hyrum's + // Law it's probably relied upon by some. It's a tolerable promise because a + // well-specified number of random bytes is included in the ciphertext, in a + // well-specified way. + + if err := checkPub(pub); err != nil { + return nil, err + } + hash.Reset() + k := pub.Size() + if len(msg) > k-2*hash.Size()-2 { + return nil, ErrMessageTooLong + } + + // ZCrypto - boring removed + // if boring.Enabled && random == boring.RandReader { + // bkey, err := boringPublicKey(pub) + // if err != nil { + // return nil, err + // } + // return boring.EncryptRSAOAEP(hash, hash, bkey, msg, label) + // } + // boring.UnreachableExceptTests() + + hash.Write(label) + lHash := hash.Sum(nil) + hash.Reset() + + em := make([]byte, k) + seed := em[1 : 1+hash.Size()] + db := em[1+hash.Size():] + + copy(db[0:hash.Size()], lHash) + db[len(db)-len(msg)-1] = 1 + copy(db[len(db)-len(msg):], msg) + + _, err := io.ReadFull(random, seed) + if err != nil { + return nil, err + } + + mgf1XOR(db, hash, seed) + mgf1XOR(seed, hash, db) + + // ZCrypto - boring never enabled + //if boring.Enabled { + // var bkey *boring.PublicKeyRSA + // bkey, err = boringPublicKey(pub) + // if err != nil { + // return nil, err + // } + // return boring.EncryptRSANoPadding(bkey, em) + //} + + return encrypt(pub, em) +} + +// ErrDecryption represents a failure to decrypt a message. +// It is deliberately vague to avoid adaptive attacks. +var ErrDecryption = errors.New("crypto/rsa: decryption error") + +// ErrVerification represents a failure to verify a signature. +// It is deliberately vague to avoid adaptive attacks. +var ErrVerification = errors.New("crypto/rsa: verification error") + +// Precompute performs some calculations that speed up private key operations +// in the future. +func (priv *PrivateKey) Precompute() { + // ZCrypto - bigmod modulus precomputation removed; decrypt uses math/big directly + // if priv.Precomputed.n == nil && len(priv.Primes) == 2 { + // priv.Precomputed.n, err = bigmod.NewModulusFromBig(priv.N) + // priv.Precomputed.p, err = bigmod.NewModulusFromBig(priv.Primes[0]) + // priv.Precomputed.q, err = bigmod.NewModulusFromBig(priv.Primes[1]) + // } + + // Fill in the backwards-compatibility *big.Int values. + if priv.Precomputed.Dp != nil { + return + } + + priv.Precomputed.Dp = new(big.Int).Sub(priv.Primes[0], bigOne) + priv.Precomputed.Dp.Mod(priv.D, priv.Precomputed.Dp) + + priv.Precomputed.Dq = new(big.Int).Sub(priv.Primes[1], bigOne) + priv.Precomputed.Dq.Mod(priv.D, priv.Precomputed.Dq) + + priv.Precomputed.Qinv = new(big.Int).ModInverse(priv.Primes[1], priv.Primes[0]) + + r := new(big.Int).Mul(priv.Primes[0], priv.Primes[1]) + priv.Precomputed.CRTValues = make([]CRTValue, len(priv.Primes)-2) + for i := 2; i < len(priv.Primes); i++ { + prime := priv.Primes[i] + values := &priv.Precomputed.CRTValues[i-2] + + values.Exp = new(big.Int).Sub(prime, bigOne) + values.Exp.Mod(priv.D, values.Exp) + + values.R = new(big.Int).Set(r) + values.Coeff = new(big.Int).ModInverse(r, prime) + + r.Mul(r, prime) + } +} + +const withCheck = true +const noCheck = false + +// decrypt performs an RSA decryption of ciphertext into out. If check is true, +// m^e is calculated and compared with ciphertext, in order to defend against +// errors in the CRT computation. +// +// ZCrypto - replaced bigmod with math/big throughout. The bigmod implementation +// provided constant-time guarantees for private key operations; math/big does +// not. This is acceptable for zcrypto's scanning/research use case. +// Original bigmod-based implementation preserved in comments below. +func decrypt(priv *PrivateKey, ciphertext []byte, check bool) ([]byte, error) { + c := new(big.Int).SetBytes(ciphertext) + if c.Cmp(priv.N) >= 0 { + return nil, ErrDecryption + } + + var m *big.Int + if len(priv.Primes) == 2 && priv.Precomputed.Dp != nil { + // ZCrypto - CRT path using math/big + p := priv.Primes[0] + q := priv.Primes[1] + // m1 = c^Dp mod p + m1 := new(big.Int).Exp(c, priv.Precomputed.Dp, p) + // m2 = c^Dq mod q + m2 := new(big.Int).Exp(c, priv.Precomputed.Dq, q) + // h = Qinv * (m1 - m2) mod p + h := new(big.Int).Sub(m1, m2) + if h.Sign() < 0 { + h.Add(h, p) + } + h.Mul(h, priv.Precomputed.Qinv) + h.Mod(h, p) + // m = m2 + h*q + m = new(big.Int).Mul(h, q) + m.Add(m, m2) + } else { + m = new(big.Int).Exp(c, priv.D, priv.N) + } + + if check { + // ZCrypto - E is now *big.Int; uint cast removed + // Original: c1 := bigmod.NewNat().ExpShortVarTime(m, uint(priv.E), N) + c1 := new(big.Int).Exp(m, priv.E, priv.N) + if c1.Cmp(c) != 0 { + return nil, ErrDecryption + } + } + + em := make([]byte, priv.Size()) + mBytes := m.Bytes() + copy(em[len(em)-len(mBytes):], mBytes) + return em, nil +} + +// ZCrypto - original bigmod-based decrypt preserved for reference: +// func decrypt(priv *PrivateKey, ciphertext []byte, check bool) ([]byte, error) { +// if len(priv.Primes) <= 2 { boring.Unreachable() } +// var (err error; m, c *bigmod.Nat; N *bigmod.Modulus; t0 = bigmod.NewNat()) +// if priv.Precomputed.n == nil { +// N, err = bigmod.NewModulusFromBig(priv.N) ... +// m = bigmod.NewNat().Exp(c, priv.D.Bytes(), N) +// } else { +// N = priv.Precomputed.n; P, Q := priv.Precomputed.p, priv.Precomputed.q +// ... CRT using bigmod.Nat operations ... +// } +// if check { c1 := bigmod.NewNat().ExpShortVarTime(m, uint(priv.E), N) ... } +// return m.Bytes(N), nil +// } + +// DecryptOAEP decrypts ciphertext using RSA-OAEP. +// +// OAEP is parameterised by a hash function that is used as a random oracle. +// Encryption and decryption of a given message must use the same hash function +// and sha256.New() is a reasonable choice. +// +// The random parameter is legacy and ignored, and it can be nil. +// +// The label parameter must match the value given when encrypting. See +// [EncryptOAEP] for details. +func DecryptOAEP(hash hash.Hash, random io.Reader, priv *PrivateKey, ciphertext []byte, label []byte) ([]byte, error) { + return decryptOAEP(hash, hash, random, priv, ciphertext, label) +} + +func decryptOAEP(hash, mgfHash hash.Hash, random io.Reader, priv *PrivateKey, ciphertext []byte, label []byte) ([]byte, error) { + if err := checkPub(&priv.PublicKey); err != nil { + return nil, err + } + k := priv.Size() + if len(ciphertext) > k || + k < hash.Size()*2+2 { + return nil, ErrDecryption + } + + // ZCrypto - boring never enabled + //if boring.Enabled { + // bkey, err := boringPrivateKey(priv) + // if err != nil { + // return nil, err + // } + // out, err := boring.DecryptRSAOAEP(hash, mgfHash, bkey, ciphertext, label) + // if err != nil { + // return nil, ErrDecryption + // } + // return out, nil + //} + + em, err := decrypt(priv, ciphertext, noCheck) + if err != nil { + return nil, err + } + + hash.Write(label) + lHash := hash.Sum(nil) + hash.Reset() + + firstByteIsZero := subtle.ConstantTimeByteEq(em[0], 0) + + seed := em[1 : hash.Size()+1] + db := em[hash.Size()+1:] + + mgf1XOR(seed, mgfHash, db) + mgf1XOR(db, mgfHash, seed) + + lHash2 := db[0:hash.Size()] + + // We have to validate the plaintext in constant time in order to avoid + // attacks like: J. Manger. A Chosen Ciphertext Attack on RSA Optimal + // Asymmetric Encryption Padding (OAEP) as Standardized in PKCS #1 + // v2.0. In J. Kilian, editor, Advances in Cryptology. + lHash2Good := subtle.ConstantTimeCompare(lHash, lHash2) + + // The remainder of the plaintext must be zero or more 0x00, followed + // by 0x01, followed by the message. + // lookingForIndex: 1 iff we are still looking for the 0x01 + // index: the offset of the first 0x01 byte + // invalid: 1 iff we saw a non-zero byte before the 0x01. + var lookingForIndex, index, invalid int + lookingForIndex = 1 + rest := db[hash.Size():] + + for i := 0; i < len(rest); i++ { + equals0 := subtle.ConstantTimeByteEq(rest[i], 0) + equals1 := subtle.ConstantTimeByteEq(rest[i], 1) + index = subtle.ConstantTimeSelect(lookingForIndex&equals1, i, index) + lookingForIndex = subtle.ConstantTimeSelect(equals1, 0, lookingForIndex) + invalid = subtle.ConstantTimeSelect(lookingForIndex&^equals0, 1, invalid) + } + + if firstByteIsZero&lHash2Good&^invalid&^lookingForIndex != 1 { + return nil, ErrDecryption + } + + return rest[index+1:], nil +} diff --git a/vendor/github.com/zmap/zcrypto/x509/cert_pool.go b/vendor/github.com/zmap/zcrypto/x509/cert_pool.go index a6c6d2b0365..d7bf903a666 100644 --- a/vendor/github.com/zmap/zcrypto/x509/cert_pool.go +++ b/vendor/github.com/zmap/zcrypto/x509/cert_pool.go @@ -25,6 +25,11 @@ func NewCertPool() *CertPool { } } +// cert returns cert index n in s. +func (s *CertPool) cert(n int) (*Certificate, error) { + return s.certs[n], nil +} + // findVerifiedParents attempts to find certificates in s which have signed the // given certificate. If any candidates were rejected then errCert will be set // to one of them, arbitrarily, and err will contain the reason that it was @@ -44,7 +49,7 @@ func (s *CertPool) findVerifiedParents(cert *Certificate) (parents []int, errCer for _, c := range candidates { if err = cert.CheckSignatureFrom(s.certs[c]); err == nil { - cert.validSignature = true + cert.ValidSignature = true parents = append(parents, c) } else { errCert = s.certs[c] diff --git a/vendor/github.com/zmap/zcrypto/x509/extensions.go b/vendor/github.com/zmap/zcrypto/x509/extensions.go index ffb87a265be..7497aa19f68 100644 --- a/vendor/github.com/zmap/zcrypto/x509/extensions.go +++ b/vendor/github.com/zmap/zcrypto/x509/extensions.go @@ -91,6 +91,7 @@ type CertificatePoliciesData struct { ExplicitTexts [][]string NoticeRefOrganization [][]string NoticeRefNumbers [][]NoticeNumber + UserNotices [][]UserNotice } func (cp *CertificatePoliciesData) MarshalJSON() ([]byte, error) { @@ -252,6 +253,7 @@ func (nc *NameConstraints) UnmarshalJSON(b []byte) error { if err != nil { return err } + nc.Critical = ncJson.Critical for _, dns := range ncJson.PermittedDNSNames { nc.PermittedDNSNames = append(nc.PermittedDNSNames, GeneralSubtreeString{Data: dns}) } @@ -320,6 +322,7 @@ func (nc *NameConstraints) UnmarshalJSON(b []byte) error { func (nc NameConstraints) MarshalJSON() ([]byte, error) { var out NameConstraintsJSON + out.Critical = nc.Critical for _, dns := range nc.PermittedDNSNames { out.PermittedDNSNames = append(out.PermittedDNSNames, dns.Data) } @@ -727,7 +730,7 @@ type CABFOrganizationIdentifier struct { Reference string `json:"reference,omitempty"` } -func (c *Certificate) jsonifyExtensions() (*CertificateExtensions, UnknownCertificateExtensions) { +func (c *Certificate) JsonifyExtensions() (*CertificateExtensions, UnknownCertificateExtensions) { exts := new(CertificateExtensions) unk := make([]pkix.Extension, 0, 2) for _, e := range c.Extensions { @@ -771,7 +774,6 @@ func (c *Certificate) jsonifyExtensions() (*CertificateExtensions, UnknownCertif exts.NameConstraints.PermittedDirectoryNames = c.PermittedDirectoryNames exts.NameConstraints.PermittedEdiPartyNames = c.PermittedEdiPartyNames exts.NameConstraints.PermittedRegisteredIDs = c.PermittedRegisteredIDs - exts.NameConstraints.ExcludedEmailAddresses = c.ExcludedEmailAddresses exts.NameConstraints.ExcludedDNSNames = c.ExcludedDNSNames exts.NameConstraints.ExcludedURIs = c.ExcludedURIs @@ -795,6 +797,7 @@ func (c *Certificate) jsonifyExtensions() (*CertificateExtensions, UnknownCertif exts.CertificatePolicies.ExplicitTexts = c.ParsedExplicitTexts exts.CertificatePolicies.QualifierId = c.QualifierId exts.CertificatePolicies.CPSUri = c.CPSuri + exts.CertificatePolicies.UserNotices = c.UserNotices } else if e.Id.Equal(oidExtAuthorityInfoAccess) { exts.AuthorityInfoAccess = new(AuthorityInfoAccess) diff --git a/vendor/github.com/zmap/zcrypto/x509/json.go b/vendor/github.com/zmap/zcrypto/x509/json.go index af26c4958e9..aaba3a10e26 100644 --- a/vendor/github.com/zmap/zcrypto/x509/json.go +++ b/vendor/github.com/zmap/zcrypto/x509/json.go @@ -6,7 +6,6 @@ package x509 import ( "crypto/ecdsa" - "crypto/rsa" "encoding/json" "errors" "net" @@ -17,6 +16,7 @@ import ( "github.com/zmap/zcrypto/dsa" "github.com/zmap/zcrypto/encoding/asn1" jsonKeys "github.com/zmap/zcrypto/json" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/util" "github.com/zmap/zcrypto/x509/pkix" ) @@ -481,13 +481,13 @@ func (c *Certificate) MarshalJSON() ([]byte, error) { } jc.SubjectKeyInfo = c.jsonifySubjectKey() - jc.Extensions, jc.UnknownExtensions = c.jsonifyExtensions() + jc.Extensions, jc.UnknownExtensions = c.JsonifyExtensions() // TODO: Handle the fact this might not match jc.SignatureAlgorithm = c.jsonifySignatureAlgorithm() jc.Signature.SignatureAlgorithm = jc.SignatureAlgorithm jc.Signature.Value = c.Signature - jc.Signature.Valid = c.validSignature + jc.Signature.Valid = c.ValidSignature jc.Signature.SelfSigned = c.SelfSigned if c.SelfSigned { jc.Signature.Valid = true diff --git a/vendor/github.com/zmap/zcrypto/x509/pkcs1.go b/vendor/github.com/zmap/zcrypto/x509/pkcs1.go index 008112ec587..dc5fb4489b9 100644 --- a/vendor/github.com/zmap/zcrypto/x509/pkcs1.go +++ b/vendor/github.com/zmap/zcrypto/x509/pkcs1.go @@ -5,18 +5,18 @@ package x509 import ( - "crypto/rsa" "errors" "math/big" "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/rsa" ) // pkcs1PrivateKey is a structure which mirrors the PKCS#1 ASN.1 for an RSA private key. type pkcs1PrivateKey struct { Version int N *big.Int - E int + E *big.Int // ZCrypto - use bigint to capture large exponents D *big.Int P *big.Int Q *big.Int @@ -37,9 +37,12 @@ type pkcs1AdditionalRSAPrime struct { } // pkcs1PublicKey reflects the ASN.1 structure of a PKCS#1 public key. +// ZCrypto - E changed from int to *big.Int to allow parsing certificates with +// public exponents too large to fit in a Go int (e.g. > 2^63). +// Original: E int type pkcs1PublicKey struct { N *big.Int - E int + E *big.Int } // ParsePKCS1PrivateKey returns an RSA private key from its ASN.1 PKCS#1 DER encoded form. @@ -63,7 +66,7 @@ func ParsePKCS1PrivateKey(der []byte) (*rsa.PrivateKey, error) { key := new(rsa.PrivateKey) key.PublicKey = rsa.PublicKey{ - E: priv.E, + E: priv.E, // ZCrypto - convert to using BigInt N: priv.N, } @@ -89,6 +92,36 @@ func ParsePKCS1PrivateKey(der []byte) (*rsa.PrivateKey, error) { return key, nil } +// ParsePKCS1PublicKey parses an [RSA] public key in PKCS #1, ASN.1 DER form. +// +// This kind of key is commonly encoded in PEM blocks of type "RSA PUBLIC KEY". +func ParsePKCS1PublicKey(der []byte) (*rsa.PublicKey, error) { + var pub pkcs1PublicKey + rest, err := asn1.Unmarshal(der, &pub) + if err != nil { + if _, err := asn1.Unmarshal(der, &publicKeyInfo{}); err == nil { + return nil, errors.New("x509: failed to parse public key (use ParsePKIXPublicKey instead for this key format)") + } + return nil, err + } + if len(rest) > 0 { + return nil, asn1.SyntaxError{Msg: "trailing data"} + } + + if pub.N.Sign() <= 0 || pub.E.Sign() <= 0 { + return nil, errors.New("x509: public key contains zero or negative value") + } + // ZCrypto - with our fork of crypto/rsa, we allow large public exponents + //if pub.E > 1<<31-1 { + // return nil, errors.New("x509: public key contains large public exponent") + //} + + return &rsa.PublicKey{ + E: pub.E, + N: pub.N, + }, nil +} + // MarshalPKCS1PrivateKey converts a private key to ASN.1 DER encoded form. func MarshalPKCS1PrivateKey(key *rsa.PrivateKey) []byte { key.Precompute() diff --git a/vendor/github.com/zmap/zcrypto/x509/pkcs8.go b/vendor/github.com/zmap/zcrypto/x509/pkcs8.go index 4f4d12a9c27..943731423fd 100644 --- a/vendor/github.com/zmap/zcrypto/x509/pkcs8.go +++ b/vendor/github.com/zmap/zcrypto/x509/pkcs8.go @@ -5,10 +5,13 @@ package x509 import ( + "crypto/ecdsa" + "crypto/ed25519" "errors" "fmt" "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509/pkix" ) @@ -22,11 +25,21 @@ type pkcs8 struct { // optional attributes omitted. } -// ParsePKCS8PrivateKey parses an unencrypted, PKCS#8 private key. -// See RFC 5208. +// ParsePKCS8PrivateKey parses an unencrypted private key in PKCS #8, ASN.1 DER form. +// +// It returns a *rsa.PrivateKey, a *ecdsa.PrivateKey, or a ed25519.PrivateKey. +// More types might be supported in the future. +// +// This kind of key is commonly encoded in PEM blocks of type "PRIVATE KEY". func ParsePKCS8PrivateKey(der []byte) (key interface{}, err error) { var privKey pkcs8 if _, err := asn1.Unmarshal(der, &privKey); err != nil { + if _, err := asn1.Unmarshal(der, &ecPrivateKey{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParseECPrivateKey instead for this key format)") + } + if _, err := asn1.Unmarshal(der, &pkcs1PrivateKey{}); err == nil { + return nil, errors.New("x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)") + } return nil, err } switch { @@ -49,7 +62,76 @@ func ParsePKCS8PrivateKey(der []byte) (key interface{}, err error) { } return key, nil + case privKey.Algo.Algorithm.Equal(oidPublicKeyEd25519): + if l := len(privKey.Algo.Parameters.FullBytes); l != 0 { + return nil, errors.New("x509: invalid Ed25519 private key parameters") + } + var curvePrivateKey []byte + if _, err := asn1.Unmarshal(privKey.PrivateKey, &curvePrivateKey); err != nil { + return nil, fmt.Errorf("x509: invalid Ed25519 private key: %v", err) + } + if l := len(curvePrivateKey); l != ed25519.SeedSize { + return nil, fmt.Errorf("x509: invalid Ed25519 private key length: %d", l) + } + return ed25519.NewKeyFromSeed(curvePrivateKey), nil + default: return nil, fmt.Errorf("x509: PKCS#8 wrapping contained private key with unknown algorithm: %v", privKey.Algo.Algorithm) } } + +// MarshalPKCS8PrivateKey converts a private key to PKCS #8, ASN.1 DER form. +// +// The following key types are currently supported: *rsa.PrivateKey, *ecdsa.PrivateKey +// and ed25519.PrivateKey. Unsupported key types result in an error. +// +// This kind of key is commonly encoded in PEM blocks of type "PRIVATE KEY". +func MarshalPKCS8PrivateKey(key interface{}) ([]byte, error) { + var privKey pkcs8 + + switch k := key.(type) { + case *rsa.PrivateKey: + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyRSA, + Parameters: asn1.NullRawValue, + } + privKey.PrivateKey = MarshalPKCS1PrivateKey(k) + + case *ecdsa.PrivateKey: + oid, err := oidFromNamedCurve(k.Curve) + if err != nil { + return nil, err + } + + oidBytes, err := asn1.Marshal(oid) + if err != nil { + return nil, errors.New("x509: failed to marshal curve OID: " + err.Error()) + } + + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyECDSA, + Parameters: asn1.RawValue{ + FullBytes: oidBytes, + }, + } + + if privKey.PrivateKey, err = marshalECPrivateKeyWithOID(k, nil); err != nil { + return nil, errors.New("x509: failed to marshal EC private key while building PKCS#8: " + err.Error()) + } + + case ed25519.PrivateKey: + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyEd25519, + } + curvePrivateKey, err := asn1.Marshal(k.Seed()) + if err != nil { + return nil, fmt.Errorf("x509: failed to marshal private key: %v", err) + } + privKey.PrivateKey = curvePrivateKey + + default: + return nil, fmt.Errorf("x509: unknown key type while marshaling PKCS#8: %T", key) + } + + return asn1.Marshal(privKey) +} diff --git a/vendor/github.com/zmap/zcrypto/x509/pkix/pkix.go b/vendor/github.com/zmap/zcrypto/x509/pkix/pkix.go index 57eed3b4437..6b1d89472fe 100644 --- a/vendor/github.com/zmap/zcrypto/x509/pkix/pkix.go +++ b/vendor/github.com/zmap/zcrypto/x509/pkix/pkix.go @@ -314,26 +314,7 @@ func (certList *CertificateList) HasExpired(now time.Time) bool { // String returns the string form of n, roughly following // the RFC 2253 Distinguished Names syntax. func (n Name) String() string { - var rdns RDNSequence - // If there are no ExtraNames, surface the parsed value (all entries in - // Names) instead. - if n.ExtraNames == nil { - for _, atv := range n.Names { - t := atv.Type - if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 { - switch t[3] { - case 3, 5, 6, 7, 8, 9, 10, 11, 17: - // These attributes were already parsed into named fields. - continue - } - } - // Place non-standard parsed values at the beginning of the sequence - // so they will be at the end of the string. See Issue 39924. - rdns = append(rdns, []AttributeTypeAndValue{atv}) - } - } - rdns = append(rdns, n.ToRDNSequence()...) - return rdns.String() + return n.ToRDNSequence().String() } // OtherName represents the ASN.1 structure of the same name. See RFC diff --git a/vendor/github.com/zmap/zcrypto/x509/root.go b/vendor/github.com/zmap/zcrypto/x509/root.go new file mode 100644 index 00000000000..cc53f7aefca --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/x509/root.go @@ -0,0 +1,31 @@ +// Copyright 2012 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 file. + +package x509 + +// To update the embedded iOS root store, update the -version +// argument to the latest security_certificates version from +// https://opensource.apple.com/source/security_certificates/ +// and run "go generate". See https://golang.org/issue/38843. +//go:generate go run root_ios_gen.go -version 55188.40.9 + +import "sync" + +var ( + once sync.Once + systemRoots *CertPool + systemRootsErr error +) + +func systemRootsPool() *CertPool { + once.Do(initSystemRoots) + return systemRoots +} + +func initSystemRoots() { + systemRoots, systemRootsErr = loadSystemRoots() + if systemRootsErr != nil { + systemRoots = nil + } +} diff --git a/vendor/github.com/zmap/zcrypto/x509/root_darwin.go b/vendor/github.com/zmap/zcrypto/x509/root_darwin.go new file mode 100644 index 00000000000..2c114b6f5db --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/x509/root_darwin.go @@ -0,0 +1,16 @@ +// Copyright 2020 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 file. + +//go:build !ios +// +build !ios + +package x509 + +func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) { + return nil, nil +} + +func loadSystemRoots() (*CertPool, error) { + return nil, nil +} diff --git a/vendor/github.com/zmap/zcrypto/x509/root_linux.go b/vendor/github.com/zmap/zcrypto/x509/root_linux.go new file mode 100644 index 00000000000..ad6ce5cae79 --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/x509/root_linux.go @@ -0,0 +1,23 @@ +// Copyright 2015 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 file. + +package x509 + +// Possible certificate files; stop after finding one. +var certFiles = []string{ + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 + "/etc/ssl/cert.pem", // Alpine Linux +} + +// Possible directories with certificate files; stop after successfully +// reading at least one file from a directory. +var certDirectories = []string{ + "/etc/ssl/certs", // SLES10/SLES11, https://golang.org/issue/12139 + "/etc/pki/tls/certs", // Fedora/RHEL + "/system/etc/security/cacerts", // Android +} diff --git a/vendor/github.com/zmap/zcrypto/x509/root_unix.go b/vendor/github.com/zmap/zcrypto/x509/root_unix.go new file mode 100644 index 00000000000..e6a1a885d16 --- /dev/null +++ b/vendor/github.com/zmap/zcrypto/x509/root_unix.go @@ -0,0 +1,109 @@ +// Copyright 2011 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 file. + +//go:build aix || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris +// +build aix dragonfly freebsd js,wasm linux netbsd openbsd solaris + +package x509 + +import ( + "io/fs" + "os" + "path/filepath" + "strings" +) + +const ( + // certFileEnv is the environment variable which identifies where to locate + // the SSL certificate file. If set this overrides the system default. + certFileEnv = "SSL_CERT_FILE" + + // certDirEnv is the environment variable which identifies which directory + // to check for SSL certificate files. If set this overrides the system default. + // It is a colon separated list of directories. + // See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html. + certDirEnv = "SSL_CERT_DIR" +) + +func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) { + return nil, nil +} + +func loadSystemRoots() (*CertPool, error) { + roots := NewCertPool() + + files := certFiles + if f := os.Getenv(certFileEnv); f != "" { + files = []string{f} + } + + var firstErr error + for _, file := range files { + data, err := os.ReadFile(file) + if err == nil { + roots.AppendCertsFromPEM(data) + break + } + if firstErr == nil && !os.IsNotExist(err) { + firstErr = err + } + } + + dirs := certDirectories + if d := os.Getenv(certDirEnv); d != "" { + // OpenSSL and BoringSSL both use ":" as the SSL_CERT_DIR separator. + // See: + // * https://golang.org/issue/35325 + // * https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html + dirs = strings.Split(d, ":") + } + + for _, directory := range dirs { + fis, err := readUniqueDirectoryEntries(directory) + if err != nil { + if firstErr == nil && !os.IsNotExist(err) { + firstErr = err + } + continue + } + for _, fi := range fis { + data, err := os.ReadFile(directory + "/" + fi.Name()) + if err == nil { + roots.AppendCertsFromPEM(data) + } + } + } + + if roots.Size() > 0 || firstErr == nil { + return roots, nil + } + + return nil, firstErr +} + +// readUniqueDirectoryEntries is like os.ReadDir but omits +// symlinks that point within the directory. +func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + uniq := files[:0] + for _, f := range files { + if !isSameDirSymlink(f, dir) { + uniq = append(uniq, f) + } + } + return uniq, nil +} + +// isSameDirSymlink reports whether fi in dir is a symlink with a +// target not containing a slash. +func isSameDirSymlink(f fs.DirEntry, dir string) bool { + if f.Type()&fs.ModeSymlink == 0 { + return false + } + target, err := os.Readlink(filepath.Join(dir, f.Name())) + return err == nil && !strings.Contains(target, "/") +} diff --git a/vendor/github.com/zmap/zcrypto/x509/sec1.go b/vendor/github.com/zmap/zcrypto/x509/sec1.go index 8d5c1657b60..57ba1a40301 100644 --- a/vendor/github.com/zmap/zcrypto/x509/sec1.go +++ b/vendor/github.com/zmap/zcrypto/x509/sec1.go @@ -38,9 +38,9 @@ func ParseECPrivateKey(der []byte) (*ecdsa.PrivateKey, error) { // MarshalECPrivateKey marshals an EC private key into ASN.1, DER format. func MarshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { - oid, ok := oidFromNamedCurve(key.Curve) - if !ok { - return nil, errors.New("x509: unknown elliptic curve") + oid, err := oidFromNamedCurve(key.Curve) + if err != nil { + return nil, err } privateKeyBytes := key.D.Bytes() @@ -55,6 +55,18 @@ func MarshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { }) } +// marshalECPrivateKey marshals an EC private key into ASN.1, DER format and +// sets the curve ID to the given OID, or omits it if OID is nil. +func marshalECPrivateKeyWithOID(key *ecdsa.PrivateKey, oid asn1.ObjectIdentifier) ([]byte, error) { + privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) + return asn1.Marshal(ecPrivateKey{ + Version: 1, + PrivateKey: key.D.FillBytes(privateKey), + NamedCurveOID: oid, + PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, + }) +} + // parseECPrivateKey parses an ASN.1 Elliptic Curve Private Key Structure. // The OID for the named curve may be provided from another source (such as // the PKCS8 container) - if it is provided then use this instead of the OID @@ -70,12 +82,12 @@ func parseECPrivateKey(namedCurveOID *asn1.ObjectIdentifier, der []byte) (key *e var curve elliptic.Curve if namedCurveOID != nil { - curve = namedCurveFromOID(*namedCurveOID) + curve, err = namedCurveFromOID(*namedCurveOID) } else { - curve = namedCurveFromOID(privKey.NamedCurveOID) + curve, err = namedCurveFromOID(privKey.NamedCurveOID) } - if curve == nil { - return nil, errors.New("x509: unknown elliptic curve") + if err != nil { + return nil, err } k := new(big.Int).SetBytes(privKey.PrivateKey) diff --git a/vendor/github.com/zmap/zcrypto/x509/verify.go b/vendor/github.com/zmap/zcrypto/x509/verify.go index 450f985c152..7042aedd6ab 100644 --- a/vendor/github.com/zmap/zcrypto/x509/verify.go +++ b/vendor/github.com/zmap/zcrypto/x509/verify.go @@ -94,6 +94,8 @@ func (e CertificateInvalidError) Error() string { return "x509: issuer name does not match subject from issuing certificate" case NeverValid: return "x509: certificate will never be valid" + case IsSelfSigned: + return "x509: certificate is self-signed and not a trusted root" } return "x509: unknown error" } @@ -279,10 +281,26 @@ func (c *Certificate) isValid(certType CertificateType, currentChain Certificate // // WARNING: this doesn't do any revocation checking. func (c *Certificate) Verify(opts VerifyOptions) (current, expired, never []CertificateChain, err error) { + // Platform-specific verification needs the ASN.1 contents so + // this makes the behavior consistent across platforms. + if len(c.Raw) == 0 { + return nil, nil, nil, errNotParsed + } + for i := 0; i < opts.Intermediates.Size(); i++ { + c, err := opts.Intermediates.cert(i) + if err != nil { + return nil, nil, nil, fmt.Errorf("crypto/x509: error fetching intermediate: %w", err) + } + if len(c.Raw) == 0 { + return nil, nil, nil, errNotParsed + } + } if opts.Roots == nil { - err = SystemRootsError{} - return + opts.Roots = systemRootsPool() + if opts.Roots == nil { + return nil, nil, nil, SystemRootsError{systemRootsErr} + } } err = c.isValid(CertificateTypeLeaf, nil) @@ -290,9 +308,14 @@ func (c *Certificate) Verify(opts VerifyOptions) (current, expired, never []Cert return } - candidateChains, err := c.buildChains(make(map[int][]CertificateChain), []*Certificate{c}, &opts) - if err != nil { - return + var candidateChains []CertificateChain + if opts.Roots.Contains(c) { + candidateChains = append(candidateChains, CertificateChain{c}) + } else { + candidateChains, err = c.buildChains(make(map[int][]CertificateChain), CertificateChain{c}, &opts) + if err != nil { + return nil, nil, nil, err + } } keyUsages := opts.KeyUsages diff --git a/vendor/github.com/zmap/zcrypto/x509/x509.go b/vendor/github.com/zmap/zcrypto/x509/x509.go index 61b10e38b4f..bc64042e19b 100644 --- a/vendor/github.com/zmap/zcrypto/x509/x509.go +++ b/vendor/github.com/zmap/zcrypto/x509/x509.go @@ -17,6 +17,7 @@ import ( "crypto/sha256" _ "crypto/sha512" "io" + "os" "strings" "unicode" @@ -24,7 +25,6 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" - "crypto/rsa" _ "crypto/sha1" _ "crypto/sha256" "encoding/pem" @@ -36,13 +36,27 @@ import ( "time" "github.com/weppos/publicsuffix-go/publicsuffix" + "golang.org/x/crypto/ed25519" + "github.com/zmap/zcrypto/dsa" "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509/ct" "github.com/zmap/zcrypto/x509/pkix" - "golang.org/x/crypto/ed25519" ) +func init() { + // Go's crypto/rsa package by default rejects RSA keys smaller than 1024, we'll disable this check to allow + // handshakes with servers using 512-bit RSA keys. + if !strings.Contains(os.Getenv("GODEBUG"), "rsa1024min=0") { + if os.Getenv("GODEBUG") == "" { + os.Setenv("GODEBUG", "rsa1024min=0") + } else { + os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",rsa1024min=0") + } + } +} + // pkixPublicKey reflects a PKIX public key structure. See SubjectPublicKeyInfo // in RFC 3280. type pkixPublicKey struct { @@ -88,9 +102,10 @@ func marshalPublicKey(pub interface{}) (publicKeyBytes []byte, publicKeyAlgorith publicKeyAlgorithm.Parameters = asn1.NullRawValue case *ecdsa.PublicKey: publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) - oid, ok := oidFromNamedCurve(pub.Curve) - if !ok { - return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: unsupported elliptic curve") + var oid asn1.ObjectIdentifier + oid, err = oidFromNamedCurve(pub.Curve) + if err != nil { + return nil, pkix.AlgorithmIdentifier{}, err } publicKeyAlgorithm.Algorithm = oidPublicKeyECDSA var paramBytes []byte @@ -507,9 +522,10 @@ func GetSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) SignatureAlgorithm // id-ecPublicKey OBJECT IDENTIFIER ::= { // iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } var ( - oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} - oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} - oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} + oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + oidPublicKeyEd25519 = oidSignatureEd25519 ) func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm { @@ -559,33 +575,33 @@ var ( oidKeyEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} ) -func namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve { +func namedCurveFromOID(oid asn1.ObjectIdentifier) (elliptic.Curve, error) { switch { case oid.Equal(oidNamedCurveP224): - return elliptic.P224() + return elliptic.P224(), nil case oid.Equal(oidNamedCurveP256): - return elliptic.P256() + return elliptic.P256(), nil case oid.Equal(oidNamedCurveP384): - return elliptic.P384() + return elliptic.P384(), nil case oid.Equal(oidNamedCurveP521): - return elliptic.P521() + return elliptic.P521(), nil } - return nil + return nil, ErrUnsupportedEllipticCurve } -func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { +func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, error) { switch curve { case elliptic.P224(): - return oidNamedCurveP224, true + return oidNamedCurveP224, nil case elliptic.P256(): - return oidNamedCurveP256, true + return oidNamedCurveP256, nil case elliptic.P384(): - return oidNamedCurveP384, true + return oidNamedCurveP384, nil case elliptic.P521(): - return oidNamedCurveP521, true + return oidNamedCurveP521, nil } - return nil, false + return nil, ErrUnsupportedEllipticCurve } // KeyUsage represents the set of actions that are valid for a given key. It's @@ -821,6 +837,8 @@ type Certificate struct { ParsedExplicitTexts [][]string ParsedNoticeRefOrganization [][]string + UserNotices [][]UserNotice + // Name constraints NameConstraintsCritical bool // if true then the name constraints are marked critical. PermittedDNSNames []GeneralSubtreeString @@ -863,8 +881,9 @@ type Certificate struct { IsPrecert bool - // Internal - validSignature bool + // ValidSignature is true if the certificate was signed by any roots or + // intermediates given in a call to (*Certificate).Verify(). + ValidSignature bool // CT SignedCertificateTimestampList []*ct.SignedCertificateTimestamp @@ -940,6 +959,10 @@ func (c *Certificate) GetParsedSubjectCommonName(invalidateCache bool) ParsedDom // involves algorithms that are not currently implemented. var ErrUnsupportedAlgorithm = errors.New("x509: cannot verify signature: algorithm unimplemented") +// ErrUnsupportedEllipticCurve results from attempting to perform an operation that +// involves elliptic curves that are not currently implemented. +var ErrUnsupportedEllipticCurve = errors.New("x509: unsupported elliptic curve") + // An InsecureAlgorithmError type InsecureAlgorithmError SignatureAlgorithm @@ -1174,6 +1197,11 @@ type userNotice struct { ExplicitText asn1.RawValue `asn1:"optional"` } +type UserNotice struct { + ExplicitText *string + NoticeReference *NoticeReference +} + type noticeReference struct { Organization asn1.RawValue NoticeNumbers []int @@ -1310,16 +1338,17 @@ func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{ if p.N.Sign() <= 0 { return nil, errors.New("x509: RSA modulus is not a positive number") } - if p.E <= 0 { + // ZCrypto - p.E is now *big.Int; use Sign() instead of <= 0 + // Original: if p.E <= 0 { + if p.E.Sign() <= 0 { return nil, errors.New("x509: RSA public exponent is not a positive number") } } - pub := &rsa.PublicKey{ + return &rsa.PublicKey{ E: p.E, N: p.N, - } - return pub, nil + }, nil case DSA: var p *big.Int rest, err := asn1.Unmarshal(asn1Data, &p) @@ -1360,9 +1389,9 @@ func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{ if len(rest) != 0 { return nil, errors.New("x509: trailing data after ECDSA parameters") } - namedCurve := namedCurveFromOID(*namedCurveOID) - if namedCurve == nil { - return nil, errors.New("x509: unsupported elliptic curve") + namedCurve, err := namedCurveFromOID(*namedCurveOID) + if err != nil { + return nil, err } x, y := elliptic.Unmarshal(namedCurve, asn1Data) if x == nil { @@ -1656,6 +1685,7 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.SubjectUniqueId = in.TBSCertificate.SubjectUniqueId out.ExtensionsMap = make(map[string]pkix.Extension, len(in.TBSCertificate.Extensions)) + for _, e := range in.TBSCertificate.Extensions { out.Extensions = append(out.Extensions, e) out.ExtensionsMap[e.Id.String()] = e @@ -1694,6 +1724,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.URIs, out.DirectoryNames, out.EDIPartyNames, out.IPAddresses, out.RegisteredIDs, out.FailedToParseNames, err = parseGeneralNames(e.Value) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } @@ -1707,6 +1740,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.IANURIs, out.IANDirectoryNames, out.IANEDIPartyNames, out.IANIPAddresses, out.IANRegisteredIDs, out.FailedToParseNames, err = parseGeneralNames(e.Value) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } @@ -1731,6 +1767,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var constraints nameConstraints _, err := asn1.Unmarshal(e.Value, &constraints) + if err != nil && asn1.AllowPermissiveParsing { + continue + } if err != nil { return nil, err } @@ -1750,6 +1789,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { case 4: var rawdn pkix.RDNSequence if _, err := asn1.Unmarshal(subtree.Value.Bytes, &rawdn); err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } var dn pkix.Name @@ -1759,6 +1801,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var ediName pkix.EDIPartyName _, err = asn1.UnmarshalWithParams(subtree.Value.FullBytes, &ediName, "tag:5") if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } out.PermittedEdiPartyNames = append(out.PermittedEdiPartyNames, GeneralSubtreeEdi{Data: ediName, Max: subtree.Max, Min: subtree.Min}) @@ -1781,6 +1826,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var id asn1.ObjectIdentifier _, err = asn1.UnmarshalWithParams(subtree.Value.FullBytes, &id, "tag:8") if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } out.PermittedRegisteredIDs = append(out.PermittedRegisteredIDs, GeneralSubtreeOid{Data: id, Max: subtree.Max, Min: subtree.Min}) @@ -1797,6 +1845,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { case 4: var rawdn pkix.RDNSequence if _, err := asn1.Unmarshal(subtree.Value.Bytes, &rawdn); err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } var dn pkix.Name @@ -1806,6 +1857,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var ediName pkix.EDIPartyName _, err = asn1.Unmarshal(subtree.Value.Bytes, &ediName) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } out.ExcludedEdiPartyNames = append(out.ExcludedEdiPartyNames, GeneralSubtreeEdi{Data: ediName, Max: subtree.Max, Min: subtree.Min}) @@ -1828,6 +1882,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var id asn1.ObjectIdentifier _, err = asn1.Unmarshal(subtree.Value.Bytes, &id) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return out, err } out.ExcludedRegisteredIDs = append(out.ExcludedRegisteredIDs, GeneralSubtreeOid{Data: id, Max: subtree.Max, Min: subtree.Min}) @@ -1851,6 +1908,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var cdp []distributionPoint _, err := asn1.Unmarshal(e.Value, &cdp) + if err != nil && asn1.AllowPermissiveParsing { + continue + } if err != nil { return nil, err } @@ -1871,6 +1931,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { for len(dpName) > 0 { dpName, err = asn1.Unmarshal(dpName, &n) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } if n.Tag == 6 { @@ -1885,6 +1948,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { var a authKeyId _, err = asn1.Unmarshal(e.Value, &a) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } out.AuthorityKeyId = a.Id @@ -1922,6 +1988,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { // RFC 5280, 4.2.1.2 var keyid []byte _, err = asn1.Unmarshal(e.Value, &keyid) + if err != nil && asn1.AllowPermissiveParsing { + continue + } if err != nil { return nil, err } @@ -1932,6 +2001,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { // RFC 5280 4.2.1.4: Certificate Policies var policies []policyInformation if _, err = asn1.Unmarshal(e.Value, &policies); err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } out.PolicyIdentifiers = make([]asn1.ObjectIdentifier, len(policies)) @@ -1942,6 +2014,7 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.ParsedExplicitTexts = make([][]string, len(policies)) out.ParsedNoticeRefOrganization = make([][]string, len(policies)) out.CPSuri = make([][]string, len(policies)) + out.UserNotices = make([][]UserNotice, len(policies)) for i, policy := range policies { out.PolicyIdentifiers[i] = policy.Policy @@ -1950,6 +2023,7 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.QualifierId[i] = append(out.QualifierId[i], qualifier.PolicyQualifierId) userNoticeOID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 2} cpsURIOID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1} + if qualifier.PolicyQualifierId.Equal(userNoticeOID) { var un userNotice _, err := asn1.Unmarshal(qualifier.Qualifier.FullBytes, &un) @@ -1957,15 +2031,27 @@ func parseCertificate(in *certificate) (*Certificate, error) { return nil, err } if err == nil { + groupUserNotice := UserNotice{} if len(un.ExplicitText.Bytes) != 0 { out.ExplicitTexts[i] = append(out.ExplicitTexts[i], un.ExplicitText) - out.ParsedExplicitTexts[i] = append(out.ParsedExplicitTexts[i], string(un.ExplicitText.Bytes)) + parsed := string(un.ExplicitText.Bytes) + out.ParsedExplicitTexts[i] = append(out.ParsedExplicitTexts[i], parsed) + + groupUserNotice.ExplicitText = &parsed } + if un.NoticeRef.Organization.Bytes != nil || un.NoticeRef.NoticeNumbers != nil { out.NoticeRefOrgnization[i] = append(out.NoticeRefOrgnization[i], un.NoticeRef.Organization) out.NoticeRefNumbers[i] = append(out.NoticeRefNumbers[i], un.NoticeRef.NoticeNumbers) out.ParsedNoticeRefOrganization[i] = append(out.ParsedNoticeRefOrganization[i], string(un.NoticeRef.Organization.Bytes)) + + groupUserNotice.NoticeReference = &NoticeReference{ + Organization: string(un.NoticeRef.Organization.Bytes), + NoticeNumbers: un.NoticeRef.NoticeNumbers, + } } + + out.UserNotices[i] = append(out.UserNotices[i], groupUserNotice) } } if qualifier.PolicyQualifierId.Equal(cpsURIOID) { @@ -2001,6 +2087,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { // RFC 5280 4.2.2.1: Authority Information Access var aia []authorityInfoAccess if _, err = asn1.Unmarshal(e.Value, &aia); err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } @@ -2017,11 +2106,14 @@ func parseCertificate(in *certificate) (*Certificate, error) { } } else if e.Id.Equal(oidExtensionSignedCertificateTimestampList) { err := parseSignedCertificateTimestampList(out, e) + if err != nil && asn1.AllowPermissiveParsing { + continue + } if err != nil { return nil, err } } else if e.Id.Equal(oidExtensionCTPrecertificatePoison) { - if e.Value[0] == 5 && e.Value[1] == 0 { + if len(e.Value) == 2 && e.Value[0] == 5 && e.Value[1] == 0 { out.IsPrecert = true continue } else { @@ -2032,6 +2124,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { } else if e.Id.Equal(oidBRTorServiceDescriptor) { descs, err := parseTorServiceDescriptorSyntax(e) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } out.TorServiceDescriptors = descs @@ -2039,6 +2134,9 @@ func parseCertificate(in *certificate) (*Certificate, error) { cabf := CABFOrganizationIDASN{} _, err := asn1.Unmarshal(e.Value, &cabf) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } out.CABFOrganizationIdentifier = &CABFOrganizationIdentifier{ @@ -2051,10 +2149,16 @@ func parseCertificate(in *certificate) (*Certificate, error) { rawStatements := QCStatementsASN{} _, err := asn1.Unmarshal(e.Value, &rawStatements.QCStatements) if err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } qcStatements := QCStatements{} if err := qcStatements.Parse(&rawStatements); err != nil { + if asn1.AllowPermissiveParsing { + continue + } return nil, err } out.QCStatements = &qcStatements @@ -2486,6 +2590,7 @@ func signingParamsForPublicKey(pub interface{}, requestedSigAlgo SignatureAlgori switch pub := pub.(type) { case *rsa.PublicKey: + _ = pub pubType = RSA hashFunc = crypto.SHA256 sigAlgo.Algorithm = oidSignatureSHA256WithRSA diff --git a/vendor/github.com/zmap/zlint/v3/.goreleaser.yml b/vendor/github.com/zmap/zlint/v3/.goreleaser.yml index cdd31639942..2210bdad975 100644 --- a/vendor/github.com/zmap/zlint/v3/.goreleaser.yml +++ b/vendor/github.com/zmap/zlint/v3/.goreleaser.yml @@ -1,22 +1,24 @@ +version: 2 + project_name: zlint + before: hooks: - go mod tidy + builds: - - - main: ./cmd/zlint/main.go + - main: ./cmd/zlint/main.go binary: zlint env: - CGO_ENABLED=0 goos: - linux - - freebsd - - windows - darwin goarch: - amd64 + archives: - - + - formats: [tar.gz] wrap_in_directory: true name_template: >- {{- .ProjectName }}_ @@ -26,8 +28,10 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end -}} + snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" + release: draft: true prerelease: auto diff --git a/vendor/github.com/zmap/zlint/v3/lint/base.go b/vendor/github.com/zmap/zlint/v3/lint/base.go index 73b78ee3eeb..a505d0a412c 100644 --- a/vendor/github.com/zmap/zlint/v3/lint/base.go +++ b/vendor/github.com/zmap/zlint/v3/lint/base.go @@ -91,6 +91,26 @@ type LintMetadata struct { // true but with NotBefore >= IneffectiveDate. This check is bypassed if // IneffectiveDate is zero. Please see CheckEffective for more information. IneffectiveDate time.Time `json:"-"` + + // The ZLint linting framework performs a kind of pre-flight "CheckApplies" + // for every lint that gets ran. For example, if that lint in question + // is targeting a CABF baseline requirement, then the framework will + // assert that the certificate in question is a server auth certificate. + // Doing so allows for nearly universal "CheckApplies" logic to be hoisted + // out of each individual lint and into the framework itself. + // + // However, there are rare occasions wherein a lint disagrees with the + // framework's pre-flight "CheckApplies" logic. For example, CABF 4.9.9 + // places a constraint on OCSP signing certificates. However, since an + // OCSP signing certificate is not a server auth certificate, this lint + // never gets ran due to the framework filtering CABF lints to only + // apply to server auth certificates. + // + // If a lint declares OverrideFrameworkFilter to be true, then the framework + // will perform no pre-flight check. This means that the lint in question + // is entirely responsible for accurately encoding all applicability rules + // in its own CheckApplies method. + OverrideFrameworkFilter bool `json:"overrideFrameworkFilter,omitempty"` } // A Lint struct represents a single lint, e.g. @@ -244,14 +264,16 @@ func (l *CertificateLint) Execute(cert *x509.Certificate, config Configuration) // CheckEffective() // Execute() func (l *CertificateLint) execute(cert *x509.Certificate, config Configuration) *LintResult { - if l.Source == CABFBaselineRequirements && !util.IsServerAuthCert(cert) { - return &LintResult{Status: NA} - } - if l.Source == CABFSMIMEBaselineRequirements && !util.IsEmailProtectionCert(cert) { - return &LintResult{Status: NA} - } - if l.Source == CABFCSBaselineRequirements && !util.IsCodeSigning(cert.PolicyIdentifiers) { - return &LintResult{Status: NA} + if !l.OverrideFrameworkFilter { + if l.Source == CABFBaselineRequirements && !util.IsServerAuthCert(cert) { + return &LintResult{Status: NA} + } + if l.Source == CABFSMIMEBaselineRequirements && !util.IsEmailProtectionCert(cert) { + return &LintResult{Status: NA} + } + if l.Source == CABFCSBaselineRequirements && !util.IsCodeSigning(cert.PolicyIdentifiers) { + return &LintResult{Status: NA} + } } lint := l.Lint() err := config.MaybeConfigure(lint, l.Name) diff --git a/vendor/github.com/zmap/zlint/v3/lint/configuration.go b/vendor/github.com/zmap/zlint/v3/lint/configuration.go index 9c60a97cbd3..4714bad10cd 100644 --- a/vendor/github.com/zmap/zlint/v3/lint/configuration.go +++ b/vendor/github.com/zmap/zlint/v3/lint/configuration.go @@ -112,7 +112,7 @@ func NewConfigFromFile(path string) (Configuration, error) { if err != nil { return Configuration{}, fmt.Errorf("failed to open the provided configuration at %s. Error: %s", path, err.Error()) } - defer f.Close() + defer f.Close() //nolint:errcheck return NewConfig(f) } diff --git a/vendor/github.com/zmap/zlint/v3/lint/global_configurations.go b/vendor/github.com/zmap/zlint/v3/lint/global_configurations.go index 4d77584415f..8b7d3b88b9f 100644 --- a/vendor/github.com/zmap/zlint/v3/lint/global_configurations.go +++ b/vendor/github.com/zmap/zlint/v3/lint/global_configurations.go @@ -66,9 +66,11 @@ func (r RFC5891Config) namespace() string { // CABFBaselineRequirementsConfig is the higher scoped configuration which services as the deserialization target for... // // [CABFBaselineRequirementsConfig] +// CrossSignedCa = false # Used to indicate that the certificate is a Cross-Certified Subordinate CA // ... -// ... -type CABFBaselineRequirementsConfig struct{} +type CABFBaselineRequirementsConfig struct { + CrossSignedCa bool +} func (c CABFBaselineRequirementsConfig) namespace() string { return "CABFBaselineRequirementsConfig" @@ -143,7 +145,6 @@ type GlobalConfiguration interface { // out a TOML document that is the full default configuration for ZLint. var defaultGlobals = []GlobalConfiguration{ &Global{}, - &CABFBaselineRequirementsConfig{}, &RFC5280Config{}, &RFC5480Config{}, &RFC5891Config{}, diff --git a/vendor/github.com/zmap/zlint/v3/lint/source.go b/vendor/github.com/zmap/zlint/v3/lint/source.go index 3a7b84233f8..b967ffc6118 100644 --- a/vendor/github.com/zmap/zlint/v3/lint/source.go +++ b/vendor/github.com/zmap/zlint/v3/lint/source.go @@ -41,6 +41,7 @@ const ( CABFEVGuidelines LintSource = "CABF_EV" MozillaRootStorePolicy LintSource = "Mozilla" AppleRootStorePolicy LintSource = "Apple" + ChromeRootStorePolicy LintSource = "Chrome" Community LintSource = "Community" EtsiEsi LintSource = "ETSI_ESI" ) @@ -67,6 +68,7 @@ func (s *LintSource) UnmarshalJSON(data []byte) error { CABFEVGuidelines, MozillaRootStorePolicy, AppleRootStorePolicy, + ChromeRootStorePolicy, Community, EtsiEsi: *s = LintSource(throwAway) @@ -110,6 +112,8 @@ func (s *LintSource) FromString(src string) { *s = MozillaRootStorePolicy case AppleRootStorePolicy: *s = AppleRootStorePolicy + case ChromeRootStorePolicy: + *s = ChromeRootStorePolicy case Community: *s = Community case EtsiEsi: diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_aia_must_contain_permitted_access_method.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_aia_must_contain_permitted_access_method.go index ca74124080b..a1315a891ed 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_aia_must_contain_permitted_access_method.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_aia_must_contain_permitted_access_method.go @@ -92,7 +92,7 @@ func (l *bRAIAAccessMethodAllowed) Execute(c *x509.Certificate) *lint.LintResult return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Certificate has an invalid GeneralName with tag %d in an accessLocation.", v.Location.Tag)} } - if !(v.Method.Equal(idAdCaIssuers) || v.Method.Equal(idAdOCSP)) { + if !v.Method.Equal(idAdCaIssuers) && !v.Method.Equal(idAdOCSP) { return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Certificate has an invalid accessMethod with OID %s.", v.Method)} } } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_arpa_domain_not_allowed.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_arpa_domain_not_allowed.go new file mode 100644 index 00000000000..494a0d2ef91 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_arpa_domain_not_allowed.go @@ -0,0 +1,64 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "strings" +) + +func init() { + + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_arpa_domain_not_allowed", + Description: "CAs SHALL NOT issue Certificates containing Domain Names that end in an IP Reverse Zone Suffix", + Citation: "CABF TLS BRs section 4.2.2", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC086_EffectiveDate, + }, + Lint: NewARPADomainNotAllowed, + }) +} + +type ARPADomainNotAllowed struct{} + +func NewARPADomainNotAllowed() lint.LintInterface { + return &ARPADomainNotAllowed{} +} + +func (l *ARPADomainNotAllowed) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) +} + +func (l *ARPADomainNotAllowed) Execute(c *x509.Certificate) *lint.LintResult { + + reverseZoneSuffixV4 := ".in-addr.arpa" + reverseZoneSuffixV6 := ".ip6.arpa" + + for _, d := range c.DNSNames { + if strings.HasSuffix(strings.ToLower(d), reverseZoneSuffixV4) || + strings.HasSuffix(strings.ToLower(d), reverseZoneSuffixV6) { + return &lint.LintResult{ + Status: lint.Error, + Details: "Domain Names that end in an IP Reverse Zone Suffix are not allowed", + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_aia_non_http_url.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_aia_non_http_url.go new file mode 100644 index 00000000000..89b9b9cf7b5 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_aia_non_http_url.go @@ -0,0 +1,98 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "strings" +) + +/* +--- Citation History of this Requirement --- +v2.0.0 to v2.1.7: 7.1.2.10.3 + +--- Version Notes --- +This requirement was baselined at v2.1.7 and is current. + +--- Requirements Language --- +BRs: 7.1.2 +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles + +[Each of the CA profiles specifies the authorityInformationAccess extension follows 7.1.2.10.3] + +BRs: 7.1.2.10.3 +If present, the AuthorityInfoAccessSyntax MUST contain one or moreAccessDescriptions. Each +AccessDescription MUST only contain a permitted accessMethod, as detailed below, and each accessLocation +MUST be encoded as the specified GeneralName type. ++-----------------+-----------+---------------------------+----------+---------+---------------------------+ +| Access Method | OID | Access Location | Presence | Maximum | Description | ++-----------------+-----------+---------------------------+----------+---------+---------------------------+ +| id-ad-ocsp | 1.3.6.1.5 | uniformResourceIdentifier | MAY | * | A HTTP URL of the Issuing | +| | .5.7.48.1 | | | | CA’s OCSP responder. | ++-----------------+-----------+---------------------------+----------+---------+---------------------------+ +| id-ad-caIssuers | 1.3.6.1.5 | uniformResourceIdentifier | MAY | * | A HTTP URL of the Issuing | +| | .5.7.48.2 | | | | CA’s certificate. | ++-----------------+-----------+---------------------------+----------+---------+---------------------------+ +*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ca_aia_non_http_url", + Description: "Within the AIA extension of CA certificates, accessLocations must contain HTTP URLs", + Citation: "CABF BRs section 7.1.2.10.3 (CA Certificate Authority Information Access)", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC62EffectiveDate, + }, + Lint: NewCAAIANonHTTPURL, + }) +} + +type CAAIANonHTTPURL struct{} + +func NewCAAIANonHTTPURL() lint.LintInterface { + return &CAAIANonHTTPURL{} +} + +func (l *CAAIANonHTTPURL) CheckApplies(c *x509.Certificate) bool { + return util.IsSubCA(c) && + (len(c.IssuingCertificateURL) > 0 || len(c.OCSPServer) > 0) +} + +func (l *CAAIANonHTTPURL) Execute(c *x509.Certificate) *lint.LintResult { + for _, url := range c.IssuingCertificateURL { + if !strings.HasPrefix(strings.ToLower(url), "http://") { + return &lint.LintResult{ + Status: lint.Error, + Details: "For the 'caIssuers' accessMethod within the AIA extension, accessLocation must contain an HTTP URL", + } + } + } + + for _, url := range c.OCSPServer { + if !strings.HasPrefix(strings.ToLower(url), "http://") { + return &lint.LintResult{ + Status: lint.Error, + Details: "For the 'ocsp' accessMethod within the AIA extension, accessLocation must contain an HTTP URL", + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_common_name_missing.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_common_name_missing.go index 5e27380f989..1326678c5be 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_common_name_missing.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_common_name_missing.go @@ -20,14 +20,50 @@ import ( "github.com/zmap/zlint/v3/util" ) -type caCommonNameMissing struct{} +/* +--- Citation History of this Requirement --- +v1.4.8 to v1.8.7: 7.1.4.3.1a +v2.0.0 to v2.1.6: 7.1.2.10.2 + +--- Version Notes --- +As of v2.0.0, this requirement no longer applies to CA certificates that conform to the Cross-Certified +Subordinate CA Certificate Profile. This lint uses the global CrossSignedCa setting to determine if the +certificate under test should be treated as an exempt cross-signed CA. It does not attempt to determine +if the issuance date is after CABFBRs_2_0_0_Date because CAs are permitted to back-date the notBefore +to that of the earliest existing certificate under section 7.1.2.2.1 + +This requirement was baselined at v2.1.6 and is current. + +--- Requirements Language --- +BRs: 7.1.2 +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles + +[Each of the CA profiles, excepting the Cross-Certified Subordinate CA Certificate Profile, +specifies the subject follows 7.1.2.10.2] + +BRs: 7.1.2.10.2 +The following table details the acceptable AttributeTypes that may appear within the type +field of an AttributeTypeAndValue, as well as the contents permitted within the value field. ++----------------+----------+----------------------------------------------------------+----------------+ +| Attribute Name | Presence | Value | Verification | ++----------------+----------+----------------------------------------------------------+----------------+ +| commonName | MUST | The contents SHOULD be an identifier for the certificate | | +| | | such that the certificate’s Name is unique across all | | +| | | certificates issued by the issuing certificate. | | ++----------------+----------+----------------------------------------------------------+----------------+ +*/ + +type caCommonNameMissing struct { + TlsBrConfig *lint.CABFBaselineRequirementsConfig +} func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_ca_common_name_missing", Description: "CA Certificates common name MUST be included.", - Citation: "BRs: 7.1.4.3.1", + Citation: "BRs: 7.1.2.10.2", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABV148Date, }, @@ -39,8 +75,12 @@ func NewCaCommonNameMissing() lint.LintInterface { return &caCommonNameMissing{} } +func (l *caCommonNameMissing) Configure() interface{} { + return l +} + func (l *caCommonNameMissing) CheckApplies(c *x509.Certificate) bool { - return util.IsCACert(c) + return util.IsCACert(c) && (l.TlsBrConfig == nil || !l.TlsBrConfig.CrossSignedCa) } func (l *caCommonNameMissing) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_invalid.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_invalid.go index dae179d2dd9..6e7e1b5c2ac 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_invalid.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_invalid.go @@ -20,22 +20,52 @@ import ( "github.com/zmap/zlint/v3/util" ) -type caCountryNameInvalid struct{} - /************************************************ -BRs: 7.1.2.1e -The Certificate Subject MUST contain the following: -‐ countryName (OID 2.5.4.6). -This field MUST contain the two‐letter ISO 3166‐1 country code for the country -in which the CA’s place of business is located. +--- Citation History of this Requirement --- +v1.0 to v1.2.4: 9.1.4 +v1.2.5: Appendix B 1E, 2H +v1.3.0 to v1.4.7: 7.1.2.1e, 7.1.2.2h +v1.4.8 to v1.8.7: 7.1.4.3.1c +v2.0.0 to v2.1.6: 7.1.2.10.2 + +--- Version Notes --- +As of v2.0.0, this requirement no longer applies to CA certificates that conform to the Cross-Certified +Subordinate CA Certificate Profile. This lint uses the global CrossSignedCa setting to determine if the +certificate under test should be treated as an exempt cross-signed CA. It does not attempt to determine +if the issuance date is after CABFBRs_2_0_0_Date because CAs are permitted to back-date the notBefore +to that of the earliest existing certificate under section 7.1.2.2.1 + +This requirement was baselined at v2.1.6 and is current. + +--- Requirements Language --- +BRs: 7.1.2 +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles + +[Each of the CA profiles, excepting the Cross-Certified Subordinate CA Certificate Profile, +specifies the subject follows 7.1.2.10.2 in the profile table.] + +BRs: 7.1.2.10.2 +The following table details the acceptable AttributeTypes that may appear within the type +field of an AttributeTypeAndValue, as well as the contents permitted within the value field. ++----------------+----------+--------------------------------------------------------+-----------------+ +| Attribute Name | Presence | Value | Verification | ++----------------+----------+--------------------------------------------------------+-----------------+ +| countryName | MUST | The two‐letter ISO 3166‐1 country code for the country | Section 3.2.2.3 | +| | | in which the CA’s place of business is located. | | ++----------------+----------+--------------------------------------------------------+-----------------+ ************************************************/ +type caCountryNameInvalid struct { + TlsBrConfig *lint.CABFBaselineRequirementsConfig +} + func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_ca_country_name_invalid", Description: "Root and Subordinate CA certificates MUST have a two-letter country code specified in ISO 3166-1", - Citation: "BRs: 7.1.2.1", + Citation: "BRs: 7.1.2.10.2", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, @@ -47,19 +77,19 @@ func NewCaCountryNameInvalid() lint.LintInterface { return &caCountryNameInvalid{} } +func (l *caCountryNameInvalid) Configure() interface{} { + return l +} + func (l *caCountryNameInvalid) CheckApplies(c *x509.Certificate) bool { - return c.IsCA + return c.IsCA && c.Subject.Country != nil && (l.TlsBrConfig == nil || !l.TlsBrConfig.CrossSignedCa) } func (l *caCountryNameInvalid) Execute(c *x509.Certificate) *lint.LintResult { - if c.Subject.Country != nil { - for _, j := range c.Subject.Country { - if !util.IsISOCountryCode(j) { - return &lint.LintResult{Status: lint.Error} - } + for _, j := range c.Subject.Country { + if !util.IsISOCountryCode(j) { + return &lint.LintResult{Status: lint.Error} } - return &lint.LintResult{Status: lint.Pass} - } else { - return &lint.LintResult{Status: lint.NA} } + return &lint.LintResult{Status: lint.Pass} } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_missing.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_missing.go index fa97bd9776c..79bca212ae5 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_missing.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_country_name_missing.go @@ -20,22 +20,52 @@ import ( "github.com/zmap/zlint/v3/util" ) -type caCountryNameMissing struct{} - /************************************************ -BRs: 7.1.2.1e -The Certificate Subject MUST contain the following: -‐ countryName (OID 2.5.4.6). -This field MUST contain the two‐letter ISO 3166‐1 country code for the country -in which the CA’s place of business is located. +--- Citation History of this Requirement --- +v1.0 to v1.2.4: 9.1.4 +v1.2.5: Appendix B 1E, 2H +v1.3.0 to v1.4.7: 7.1.2.1e, 7.1.2.2h +v1.4.8 to v1.8.7: 7.1.4.3.1c +v2.0.0 to v2.1.6: 7.1.2.10.2 + +--- Version Notes --- +As of v2.0.0, this requirement no longer applies to CA certificates that conform to the Cross-Certified +Subordinate CA Certificate Profile. This lint uses the global CrossSignedCa setting to determine if the +certificate under test should be treated as an exempt cross-signed CA. It does not attempt to determine +if the issuance date is after CABFBRs_2_0_0_Date because CAs are permitted to back-date the notBefore +to that of the earliest existing certificate under section 7.1.2.2.1 + +This requirement was baselined at v2.1.6 and is current. + +--- Requirements Language --- +BRs: 7.1.2 +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles + +[Each of the CA profiles, excepting the Cross-Certified Subordinate CA Certificate Profile, +specifies the subject follows 7.1.2.10.2 in the profile table.] + +BRs: 7.1.2.10.2 +The following table details the acceptable AttributeTypes that may appear within the type +field of an AttributeTypeAndValue, as well as the contents permitted within the value field. ++----------------+----------+--------------------------------------------------------+-----------------+ +| Attribute Name | Presence | Value | Verification | ++----------------+----------+--------------------------------------------------------+-----------------+ +| countryName | MUST | The two‐letter ISO 3166‐1 country code for the country | Section 3.2.2.3 | +| | | in which the CA’s place of business is located. | | ++----------------+----------+--------------------------------------------------------+-----------------+ ************************************************/ +type caCountryNameMissing struct { + TlsBrConfig *lint.CABFBaselineRequirementsConfig +} + func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_ca_country_name_missing", Description: "Root and Subordinate CA certificates MUST have a countryName present in subject information", - Citation: "BRs: 7.1.2.1", + Citation: "BRs: 7.1.2.10.2", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, @@ -47,8 +77,12 @@ func NewCaCountryNameMissing() lint.LintInterface { return &caCountryNameMissing{} } +func (l *caCountryNameMissing) Configure() interface{} { + return l +} + func (l *caCountryNameMissing) CheckApplies(c *x509.Certificate) bool { - return c.IsCA + return c.IsCA && (l.TlsBrConfig == nil || !l.TlsBrConfig.CrossSignedCa) } func (l *caCountryNameMissing) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_crl_sign_not_set.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_crl_sign_not_set.go index 8530f09413f..2c058c04200 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_crl_sign_not_set.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_crl_sign_not_set.go @@ -23,10 +23,31 @@ import ( type caCRLSignNotSet struct{} /************************************************ -BRs: 7.1.2.1b -This extension MUST be present and MUST be marked critical. Bit positions for -keyCertSign and cRLSign MUST be set. If the Root CA Private Key is used for -signing OCSP responses, then the digitalSignature bit MUST be set. +--- Citation History of this Requirement --- +v1 to v1.2.5: Appendix B §1(B) (roots) and §2(E) (subordinate CAs) +v1.3.0 to v1.8.7: 7.1.2.1b (roots) and §7.1.2.2e (subordinate CAs) +v2.0.0 to v2.1.7: 7.1.2.10.7 + +--- Version Notes --- +In v1.1.3, Appendix B's sections were numbered but retained their previous titles. "Root CA Certificate" +became section 1 and "Subordinate CA Certificate" became section 2. The numerical section references are +used here for all versions following the original document format of the Baseline Requirements. + +This requirement was baselined at v2.1.7 and is current. + +--- Requirements Language --- +BRs: 7.1.2 "Certificate Content and Extensions" +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles, which incorporate, and are derived from RFC 5280. + +[Each of the CA profiles specifies the keyUsage extension follows section 7.1.2.10.7] + +BRs: 7.1.2.10.7 "CA Certificate Key Usage" ++-------------+-----------+----------+ +| Key Usage | Permitted | Required | ++-------------+-----------+----------+ +| cRLSign | Y | Y | ++-------------+-----------+----------+ ************************************************/ func init() { @@ -34,7 +55,7 @@ func init() { LintMetadata: lint.LintMetadata{ Name: "e_ca_crl_sign_not_set", Description: "Root and Subordinate CA certificate keyUsage extension's crlSign bit MUST be set", - Citation: "BRs: 7.1.2.1", + Citation: "BRs: 7.1.2.10.7", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_digital_signature_not_set.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_digital_signature_not_set.go index 1d1f84be2fa..2bf817b61f3 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_digital_signature_not_set.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_digital_signature_not_set.go @@ -23,13 +23,34 @@ import ( type caDigSignNotSet struct{} /************************************************ -BRs: 7.1.2.1b: Root CA Certificate keyUsage -This extension MUST be present and MUST be marked critical. Bit positions for keyCertSign and cRLSign MUST be set. -If the Root CA Private Key is used for signing OCSP responses, then the digitalSignature bit MUST be set. +--- Citation History of this Requirement --- +v1 to v1.2.5: Appendix B §1(B) (roots) and §2(E) (subordinate CAs) +v1.3.0 to v1.8.7: 7.1.2.1b (roots) and §7.1.2.2e (subordinate CAs) +v2.0.0 to v2.1.7: 7.1.2.10.7 -BRs: 7.1.2.2e: Subordinate CA Certificate keyUsage -This extension MUST be present and MUST be marked critical. Bit positions for keyCertSign and cRLSign MUST be set. -If the Root CA Private Key is used for signing OCSP responses, then the digitalSignature bit MUST be set. +--- Version Notes --- +In v1.1.3, Appendix B's sections were numbered but retained their previous titles. "Root CA Certificate" +became section 1 and "Subordinate CA Certificate" became section 2. The numerical section references are +used here for all versions following the original document format of the Baseline Requirements. + +This requirement was baselined at v2.1.7 and is current. + +--- Requirements Language --- +BRs: 7.1.2 "Certificate Content and Extensions" +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles, which incorporate, and are derived from RFC 5280. + +[Each of the CA profiles specifies the keyUsage extension follows section 7.1.2.10.7] + +BRs: 7.1.2.10.7 "CA Certificate Key Usage" ++------------------+-----------+----------+ +| Key Usage | Permitted | Required | ++------------------+-----------+----------+ +| digitalSignature | Y | N^15 | ++------------------+-----------+----------+ +Footnote 15: +If a CA Certificate does not assert the digitalSignature bit, the CA Private Key MUST NOT be +used to sign an OCSP Response. See Section 7.3 for more information. ************************************************/ func init() { @@ -37,7 +58,7 @@ func init() { LintMetadata: lint.LintMetadata{ Name: "n_ca_digital_signature_not_set", Description: "Root and Subordinate CA Certificates that wish to use their private key for signing OCSP responses will not be able to without their digital signature set", - Citation: "BRs: 7.1.2.1", + Citation: "BRs: 7.1.2.10.7", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, @@ -57,6 +78,9 @@ func (l *caDigSignNotSet) Execute(c *x509.Certificate) *lint.LintResult { if c.KeyUsage&x509.KeyUsageDigitalSignature != 0 { return &lint.LintResult{Status: lint.Pass} } else { - return &lint.LintResult{Status: lint.Notice} + return &lint.LintResult{ + Status: lint.Notice, + Details: "CA certificate does not assert digitalSignature and MUST NOT be used to sign OCSP responses", + } } } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_is_ca.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_is_ca.go index eed50419534..e8d9c9a135a 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_is_ca.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_is_ca.go @@ -21,6 +21,39 @@ import ( "github.com/zmap/zlint/v3/util" ) +/* +--- Citation History of this Requirement --- +v1.0 to v1.2.5: Appendix B §1(A) (roots) and §2(D) (subordinate CAs) +v1.3.0 to v1.8.7: §7.1.2.1(a) (roots) and §7.1.2.2(d) (subordinate CAs) +v2.0.0 to v2.1.7: §7.1.2.1.4 (roots) and §7.1.2.10.4 (all other CA profiles) + +--- Version Notes --- +In v1.1.3, Appendix B's sections were numbered but retained their previous titles. "Root CA Certificate" +became section 1 and "Subordinate CA Certificate" became section 2. The numerical section references are +used here for all versions following the original document format of the Baseline Requirements. + +This requirement was baselined at v2.2.6 and is current. + +--- Requirements Language --- +BRs: 7.1.2.1.4 Root CA Basic Constraints ++-------------------+------------------+ +| Field | Description | ++-------------------+------------------+ +| cA | MUST be set TRUE | ++-------------------+------------------+ +| pathLenConstraint | NOT RECOMMENDED | ++-------------------+------------------+ + +BRs: 7.1.2.10.4 CA Certificate Basic Constraints ++-------------------+------------------+ +| Field | Description | ++-------------------+------------------+ +| cA | MUST be set TRUE | ++-------------------+------------------+ +| pathLenConstraint | MAY be present | ++-------------------+------------------+ +*/ + type caIsCA struct{} func init() { @@ -28,7 +61,7 @@ func init() { LintMetadata: lint.LintMetadata{ Name: "e_ca_is_ca", Description: "Root and Sub CA Certificate: The CA field MUST be set to true.", - Citation: "BRs: 7.1.2.1, BRs: 7.1.2.2", + Citation: "BRs: 7.1.2.1.4, 7.1.2.10.4", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_key_cert_sign_not_set.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_key_cert_sign_not_set.go index 481f08b66f3..deacf7ca9e8 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_key_cert_sign_not_set.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_key_cert_sign_not_set.go @@ -23,17 +23,39 @@ import ( type caKeyCertSignNotSet struct{} /************************************************ -BRs: 7.1.2.1b -This extension MUST be present and MUST be marked critical. Bit positions for keyCertSign and cRLSign MUST be set. -If the Root CA Private Key is used for signing OCSP responses, then the digitalSignature bit MUST be set. +--- Citation History of this Requirement --- +v1 to v1.2.5: Appendix B §1(B) (roots) and §2(E) (subordinate CAs) +v1.3.0 to v1.8.7: 7.1.2.1b (roots) and §7.1.2.2e (subordinate CAs) +v2.0.0 to v2.1.7: 7.1.2.10.7 + +--- Version Notes --- +In v1.1.3, Appendix B's sections were numbered but retained their previous titles. "Root CA Certificate" +became section 1 and "Subordinate CA Certificate" became section 2. The numerical section references are +used here for all versions following the original document format of the Baseline Requirements. + +This requirement was baselined at v2.1.7 and is current. + +--- Requirements Language --- +BRs: 7.1.2 "Certificate Content and Extensions" +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles, which incorporate, and are derived from RFC 5280. + +[Each of the CA profiles specifies the keyUsage extension follows section 7.1.2.10.7] + +BRs: 7.1.2.10.7 "CA Certificate Key Usage" ++-------------+-----------+----------+ +| Key Usage | Permitted | Required | ++-------------+-----------+----------+ +| keyCertSign | Y | Y | ++-------------+-----------+----------+ ************************************************/ func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_ca_key_cert_sign_not_set", - Description: "Root CA Certificate: Bit positions for keyCertSign and cRLSign MUST be set.", - Citation: "BRs: 7.1.2.1", + Description: "CA Certificate Key Usage: Bit position for keyCertSign is REQUIRED.", + Citation: "BRs: 7.1.2.10.7", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_multiple_reserved_policy_oids.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_multiple_reserved_policy_oids.go new file mode 100644 index 00000000000..a2f9007f884 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_multiple_reserved_policy_oids.go @@ -0,0 +1,93 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ca_multiple_reserved_policy_oids", + Description: "The CA MUST include exactly one Reserved Certificate Policy Identifier", + Citation: "CABF BRs §7.1.2.10.5, Table 73 (Policy Restricted)", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABFBRs_2_0_0_Date, + }, + Lint: NewCAMultipleReservedPolicyOIDs, + }) +} + +type CAMultipleReservedPolicyOIDs struct { + CrossCert bool `comment:"Set this to true if the certificate to be linted is a cross-certificate"` +} + +func NewCAMultipleReservedPolicyOIDs() lint.LintInterface { + return &CAMultipleReservedPolicyOIDs{ + CrossCert: false, + } +} + +func (l *CAMultipleReservedPolicyOIDs) Configure() interface{} { + return l +} + +func (l *CAMultipleReservedPolicyOIDs) CheckApplies(c *x509.Certificate) bool { + // Exclude non-policy-restricted SubCAs and cross-certificates + return util.IsSubCA(c) && isPolicyRestricted(c) && !l.CrossCert +} + +func (l *CAMultipleReservedPolicyOIDs) Execute(c *x509.Certificate) *lint.LintResult { + if hasMultipleReservedPolicyOIDs(c) { + return &lint.LintResult{ + Status: lint.Error, + Details: "A Subordinate CA certificate MUST include exactly one Reserved Certificate Policy Identifier", + } + } + return &lint.LintResult{Status: lint.Pass} +} + +// By definition, a Policy Restricted CA is one that does NOT +// contain the anyPolicy OID in its CertificatePolicies extension +func isPolicyRestricted(c *x509.Certificate) bool { + return !util.SliceContainsOID(c.PolicyIdentifiers, util.AnyPolicyOID) +} + +func hasMultipleReservedPolicyOIDs(c *x509.Certificate) bool { + cabfReservedPolicyOIDs := []asn1.ObjectIdentifier{ + util.BRDomainValidatedOID, + util.BROrganizationValidatedOID, + util.BRIndividualValidatedOID, + util.BRExtendedValidatedOID, + } + + // This way we also detect the weird case of multiple instances of + // the same reserved policy OID, but this would still be an error... + alreadyFoundOne := false + for _, oid := range c.PolicyIdentifiers { + if util.SliceContainsOID(cabfReservedPolicyOIDs, oid) { + if alreadyFoundOne { + return true + } else { + alreadyFoundOne = true + } + } + } + return false +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_organization_name_missing.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_organization_name_missing.go index e8041fe922e..eaff6dfea21 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_organization_name_missing.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ca_organization_name_missing.go @@ -20,19 +20,58 @@ import ( "github.com/zmap/zlint/v3/util" ) -type caOrganizationNameMissing struct{} - /************************************************ -BRs: 7.1.2.1e -The Certificate Subject MUST contain the following: organizationName (OID 2.5.4.10): This field MUST be present and the contents MUST contain either the Subject CA’s name or DBA as verified under Section 3.2.2.2. +--- Citation History of this Requirement --- +v1.0 to v1.2.4: 9.1.3 +v1.2.5: Appendix B 1E, 2H +v1.3.0 to v1.4.7: 7.1.2.1e, 7.1.2.2h +v1.4.8 to v1.8.7: 7.1.4.3.1b +v2.0.0 to v2.1.6: 7.1.2.10.2 + +--- Version Notes --- +As of v2.0.0, this requirement no longer applies to CA certificates that conform to the Cross-Certified +Subordinate CA Certificate Profile. This lint uses the global CrossSignedCa setting to determine if the +certificate under test should be treated as an exempt cross-signed CA. It does not attempt to determine +if the issuance date is after CABFBRs_2_0_0_Date because CAs are permitted to back-date the notBefore +to that of the earliest existing certificate under section 7.1.2.2.1 + +This requirement was baselined at v2.1.6 and is current. + +--- Requirements Language --- +BRs: 7.1.2 +If the CA asserts compliance with these Baseline Requirements, all certificates that it issues MUST +comply with one of the following certificate profiles + +[Each of the CA profiles, excepting the Cross-Certified Subordinate CA Certificate Profile, +specifies the subject follows 7.1.2.10.2 in the profile table.] + +BRs: 7.1.2.10.2 +The following table details the acceptable AttributeTypes that may appear within the type +field of an AttributeTypeAndValue, as well as the contents permitted within the value field. ++------------------+----------+--------------------------------------------------------+-----------------+ +| Attribute Name | Presence | Value | Verification | ++------------------+----------+--------------------------------------------------------+-----------------+ +| organizationName | MUST | The CA’s name or DBA. The CA MAY include information | Section 3.2.2.2 | +| | | in this field that differs slightly from the verified | | +| | | name, such as common variations or abbreviations, | | +| | | provided that the CA documents the difference and any | | +| | | abbreviations used are locally accepted abbreviations; | | +| | | e.g. if the official record shows “Company Name | | +| | | Incorporated”, the CA MAY use “Company Name Inc.” or | | +| | | “Company Name”. | | ++------------------+----------+--------------------------------------------------------+-----------------+ ************************************************/ +type caOrganizationNameMissing struct { + TlsBrConfig *lint.CABFBaselineRequirementsConfig +} + func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_ca_organization_name_missing", - Description: "Root and Subordinate CA certificates MUST have a organizationName present in subject information", - Citation: "BRs: 7.1.2.1", + Description: "Root and Subordinate CA certificates MUST have an organizationName present in subject information", + Citation: "BRs: 7.1.2.10.2", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABEffectiveDate, }, @@ -44,8 +83,12 @@ func NewCaOrganizationNameMissing() lint.LintInterface { return &caOrganizationNameMissing{} } +func (l *caOrganizationNameMissing) Configure() interface{} { + return l +} + func (l *caOrganizationNameMissing) CheckApplies(c *x509.Certificate) bool { - return c.IsCA + return c.IsCA && (l.TlsBrConfig == nil || !l.TlsBrConfig.CrossSignedCa) } func (l *caOrganizationNameMissing) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_locality.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_locality.go index 393f2306e95..bacedee3786 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_locality.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_locality.go @@ -14,8 +14,28 @@ package cabf_br * permissions and limitations under the License. */ -// If the Certificate asserts the policy identifier of 2.23.140.1.2.1, then it MUST NOT include -// organizationName, streetAddress, localityName, stateOrProvinceName, or postalCode in the Subject field. +/* +--- Citation History of this Requirement --- +§9.3.1 v1.0 to v1.2.5 +§7.1.6.1 v1.3.0 to v1.7.2 +§7.1.6.4 v1.7.3 to v1.8.7 +Superseded in v2.0.0 by a new general prohibition on extra name types not specifically allowed. + +--- Version Notes --- +This practice is still prohibited, but the requirements moved from specifically prohibiting certain +name types to blocking everything but commonName and countryName in v2.0.0. (See e_cab_dv_subject_invalid_values) + +This requirement was removed in v2.0.0 and is historical. The language below is from v1.8.7, +the last relevant version. + +--- Requirements Language --- +BRs: 7.1.6.4 +Certificate Policy Identifier: 2.23.140.1.2.1 +If the Certificate complies with these requirements and lacks Subject identity information that +has been verified in accordance with Section 3.2.2.1 or Section 3.2.3. +Such Certificates MUST NOT include organizationName, givenName, surname, +streetAddress, localityName, stateOrProvinceName, or postalCode in the Subject field. +*/ import ( "github.com/zmap/zcrypto/x509" @@ -26,11 +46,12 @@ import ( func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_dv_conflicts_with_locality", - Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, locality name MUST NOT be included in subject", - Citation: "BRs: 7.1.6.1", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "e_cab_dv_conflicts_with_locality", + Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, locality name MUST NOT be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyConflictsWithLocality, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_org.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_org.go index 8f849cf9236..33f9b054270 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_org.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_org.go @@ -23,6 +23,20 @@ import ( type certPolicyConflictsWithOrg struct{} /************************************************ +--- Citation History of this Requirement --- +§9.3.1 v1.0 to v1.2.5 +§7.1.6.1 v1.3.0 to v1.7.2 +§7.1.6.4 v1.7.3 to v1.8.7 +Superseded in v2.0.0 by a new general prohibition on extra name types not specifically allowed. + +--- Version Notes --- +This practice is still prohibited, but the requirements moved from specifically prohibiting certain +name types to blocking everything but commonName and countryName in v2.0.0. (See e_cab_dv_subject_invalid_values) + +This requirement was removed in v2.0.0 and is historical. The language below is from v1.8.7, +the last relevant version. + +--- Requirements Language --- BRs: 7.1.6.4 Certificate Policy Identifier: 2.23.140.1.2.1 If the Certificate complies with these requirements and lacks Subject identity information that @@ -35,11 +49,12 @@ field. func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_dv_conflicts_with_org", - Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, organization name MUST NOT be included in subject", - Citation: "BRs: 7.1.6.4", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "e_cab_dv_conflicts_with_org", + Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, organization name MUST NOT be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyConflictsWithOrg, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_postal.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_postal.go index f982d568867..d08be0009fe 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_postal.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_postal.go @@ -23,6 +23,20 @@ import ( type certPolicyConflictsWithPostal struct{} /************************************************ +--- Citation History of this Requirement --- +§9.3.1 v1.0 to v1.2.5 +§7.1.6.1 v1.3.0 to v1.7.2 +§7.1.6.4 v1.7.3 to v1.8.7 +Superseded in v2.0.0 by a new general prohibition on extra name types not specifically allowed. + +--- Version Notes --- +This practice is still prohibited, but the requirements moved from specifically prohibiting certain +name types to blocking everything but commonName and countryName in v2.0.0. (See e_cab_dv_subject_invalid_values) + +This requirement was removed in v2.0.0 and is historical. The language below is from v1.8.7, +the last relevant version. + +--- Requirements Language --- BRs: 7.1.6.4 Certificate Policy Identifier: 2.23.140.1.2.1 If the Certificate complies with these requirements and lacks Subject identity information that @@ -35,11 +49,12 @@ field. func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_dv_conflicts_with_postal", - Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, postalCode MUST NOT be included in subject", - Citation: "BRs: 7.1.6.4", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "e_cab_dv_conflicts_with_postal", + Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, postalCode MUST NOT be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyConflictsWithPostal, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_province.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_province.go index b2a6f0c3206..f996b135a2e 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_province.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_province.go @@ -23,6 +23,20 @@ import ( type certPolicyConflictsWithProvince struct{} /************************************************ +--- Citation History of this Requirement --- +§9.3.1 v1.0 to v1.2.5 +§7.1.6.1 v1.3.0 to v1.7.2 +§7.1.6.4 v1.7.3 to v1.8.7 +Superseded in v2.0.0 by a new general prohibition on extra name types not specifically allowed. + +--- Version Notes --- +This practice is still prohibited, but the requirements moved from specifically prohibiting certain +name types to blocking everything but commonName and countryName in v2.0.0. (See e_cab_dv_subject_invalid_values) + +This requirement was removed in v2.0.0 and is historical. The language below is from v1.8.7, +the last relevant version. + +--- Requirements Language --- BRs: 7.1.6.4 Certificate Policy Identifier: 2.23.140.1.2.1 If the Certificate complies with these requirements and lacks Subject identity information that @@ -35,11 +49,12 @@ field. func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_dv_conflicts_with_province", - Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, stateOrProvinceName MUST NOT be included in subject", - Citation: "BRs: 7.1.6.4", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "e_cab_dv_conflicts_with_province", + Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, stateOrProvinceName MUST NOT be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyConflictsWithProvince, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_street.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_street.go index 0d9d87eff3c..7fac090e39f 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_street.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_dv_conflicts_with_street.go @@ -23,6 +23,20 @@ import ( type certPolicyConflictsWithStreet struct{} /************************************************ +--- Citation History of this Requirement --- +§9.3.1 v1.0 to v1.2.5 +§7.1.6.1 v1.3.0 to v1.7.2 +§7.1.6.4 v1.7.3 to v1.8.7 +Superseded in v2.0.0 by a new general prohibition on extra name types not specifically allowed. + +--- Version Notes --- +This practice is still prohibited, but the requirements moved from specifically prohibiting certain +name types to blocking everything but commonName and countryName in v2.0.0. (See e_cab_dv_subject_invalid_values) + +This requirement was removed in v2.0.0 and is historical. The language below is from v1.8.7, +the last relevant version. + +--- Requirements Language --- BRs: 7.1.6.4 Certificate Policy Identifier: 2.23.140.1.2.1 If the Certificate complies with these requirements and lacks Subject identity information that @@ -35,11 +49,12 @@ field. func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_dv_conflicts_with_street", - Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, streetAddress MUST NOT be included in subject", - Citation: "BRs: 7.1.6.4", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "e_cab_dv_conflicts_with_street", + Description: "If certificate policy 2.23.140.1.2.1 (CA/B BR domain validated) is included, streetAddress MUST NOT be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyConflictsWithStreet, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name.go index 32c01617573..77a68652c85 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name.go @@ -23,6 +23,15 @@ import ( type CertPolicyRequiresPersonalName struct{} /************************************************ +--- Citation History of this Requirement --- +v1.3.1 to v1.7.2: 7.1.6.1 +v1.7.3 to v1.8.7: 7.1.6.4 + +--- Version Notes --- +This requirement was rewritten in v2.0.0 and this lint was replaced by e_cab_iv_requires_personal_name_strict. +The language below represents the last version of the requirement implemented by this lint as it appeared in v1.8.7. + +--- Requirements Language --- BRs: 7.1.6.4 Certificate Policy Identifier: 2.23.140.1.2.3 If the Certificate complies with these Requirements and includes Subject Identity Information @@ -36,11 +45,12 @@ the Subject field. func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_cab_iv_requires_personal_name", - Description: "If certificate policy 2.23.140.1.2.3 is included, either organizationName or givenName and surname MUST be included in subject", - Citation: "BRs: 7.1.6.4", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABV131Date, + Name: "e_cab_iv_requires_personal_name", + Description: "If certificate policy 2.23.140.1.2.3 is included, either organizationName or givenName and surname MUST be included in subject", + Citation: "BRs: 7.1.6.4", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABV131Date, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewCertPolicyRequiresPersonalName, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name_strict.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name_strict.go new file mode 100644 index 00000000000..6b0ac296493 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cab_iv_requires_personal_name_strict.go @@ -0,0 +1,80 @@ +package cabf_br + +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type CertPolicyRequiresPersonalNameStrict struct{} + +/************************************************ +--- Citation History of this Requirement --- +v2.0.0 to v2.2.5: 7.1.2.7.3 + +--- Version Notes --- +This requirement was baselined at v2.2.5 and is current. + +--- Requirements Language --- +TLS BRs: 7.1.2.7.3 Individual Validated +Certificate Policy Identifier: 2.23.140.1.2.3 + +The following table details the acceptable AttributeTypes that may appear within the +type field of an AttributeTypeAndValue, as well as the contents permitted within the +value field. + ++----------------+----------+---------------------------+---------------+ +| Attribute Name | Presence | Value | Verification | ++----------------+----------+---------------------------+---------------+ +| surname | MUST | The Subject’s surname. | Section 3.2.3 | ++----------------+----------+---------------------------+---------------+ +| givenName | MUST | The Subject’s given name. | Section 3.2.3 | ++----------------+----------+---------------------------+---------------+ + +************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cab_iv_requires_personal_name_strict", + Description: "If certificate policy 2.23.140.1.2.3 is included givenName and surname MUST be included in subject", + Citation: "BRs: 7.1.2.7.3", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABFBRs_2_0_0_Date, + }, + Lint: NewCertPolicyRequiresPersonalNameStrict, + }) +} + +func NewCertPolicyRequiresPersonalNameStrict() lint.LintInterface { + return &CertPolicyRequiresPersonalNameStrict{} +} + +func (l *CertPolicyRequiresPersonalNameStrict) CheckApplies(cert *x509.Certificate) bool { + return util.SliceContainsOID(cert.PolicyIdentifiers, util.BRIndividualValidatedOID) && !util.IsCACert(cert) +} + +func (l *CertPolicyRequiresPersonalNameStrict) Execute(cert *x509.Certificate) *lint.LintResult { + var out lint.LintResult + if util.TypeInName(&cert.Subject, util.GivenNameOID) && util.TypeInName(&cert.Subject, util.SurnameOID) { + out.Status = lint.Pass + } else { + out.Status = lint.Error + out.Details = "Subject MUST include both givenName and surname for Individual Validation (2.23.140.1.2.3) certificates" + } + return &out +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cert_policy_iv_requires_country.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cert_policy_iv_requires_country.go index f05f8553e22..9c0d9082802 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cert_policy_iv_requires_country.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_cert_policy_iv_requires_country.go @@ -23,14 +23,31 @@ import ( type CertPolicyIVRequiresCountry struct{} /************************************************ -BRs: 7.1.6.4 -Certificate Policy Identifier: 2.23.140.1.2.3 -If the Certificate complies with these Requirements and includes Subject Identity Information -that is verified in accordance with Section 3.2.3. -Such Certificates MUST also include either organizationName or both givenName and -surname, localityName (to the extent such field is required under Section 7.1.4.2.2), -stateOrProvinceName (to the extent required under Section 7.1.4.2.2), and countryName in -the Subject field. +--- Citation History of this Requirement --- +v1.3.1 to v1.7.2: 7.1.6.1 +v1.7.3 to v1.8.7: 7.1.6.4 +v2.0.0 to v2.2.5: 7.1.2.7.3 + +--- Version Notes --- +This requirement was baselined at v2.2.5 and is current. + +--- Requirements Language --- +TLS BRs: 7.1.2.7.3 Individual Validated + +The following table details the acceptable AttributeTypes that may appear within the +type field of an AttributeTypeAndValue, as well as the contents permitted within the +value field. + ++----------------+----------+--------------------------------------------------------+---------------+ +| Attribute Name | Presence | Value | Verification | ++----------------+----------+--------------------------------------------------------+---------------+ +| countryName | MUST | The two‐letter ISO 3166‐1 country code for the country | Section 3.2.3 | +| | | associated with the Subject. If a Country is not | | +| | | represented by an official ISO 3166‐1 country code, | | +| | | the CA MUST specify the ISO 3166‐1 user‐assigned code | | +| | | of XX, indicating that an official ISO 3166‐1 alpha‐2 | | +| | | code has not been assigned. | | ++----------------+----------+--------------------------------------------------------+---------------+ ************************************************/ func init() { @@ -38,7 +55,7 @@ func init() { LintMetadata: lint.LintMetadata{ Name: "e_cert_policy_iv_requires_country", Description: "If certificate policy 2.23.140.1.2.3 is included, countryName MUST be included in subject", - Citation: "BRs: 7.1.6.4", + Citation: "BRs: 7.1.2.7.3", Source: lint.CABFBaselineRequirements, EffectiveDate: util.CABV131Date, }, @@ -51,7 +68,7 @@ func NewCertPolicyIVRequiresCountry() lint.LintInterface { } func (l *CertPolicyIVRequiresCountry) CheckApplies(cert *x509.Certificate) bool { - return util.SliceContainsOID(cert.PolicyIdentifiers, util.BRIndividualValidatedOID) + return util.SliceContainsOID(cert.PolicyIdentifiers, util.BRIndividualValidatedOID) && !util.IsCACert(cert) } func (l *CertPolicyIVRequiresCountry) Execute(cert *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_auth_key_id_only_contains_keyid.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_auth_key_id_only_contains_keyid.go new file mode 100644 index 00000000000..104e4900619 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_auth_key_id_only_contains_keyid.go @@ -0,0 +1,74 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "encoding/asn1" + "fmt" + "math/big" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_auth_key_id_only_contains_keyid", + Description: "The AuthKey extension must only contain the KeyIdentifier field.", + Citation: "BRs: 7.2.2", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABFBRs_2_0_1_Date, + }, + Lint: func() lint.RevocationListLintInterface { return &authKeyIDOnlyContainsKeyID{} }, + }) +} + +type authKeyIDOnlyContainsKeyID struct{} + +func (l *authKeyIDOnlyContainsKeyID) CheckApplies(r *x509.RevocationList) bool { + return true +} + +func (l *authKeyIDOnlyContainsKeyID) Execute(r *x509.RevocationList) *lint.LintResult { + for _, ext := range r.Extensions { + if !ext.Id.Equal(util.AuthkeyOID) { + continue + } + var authKey authKey + rest, err := asn1.Unmarshal(ext.Value, &authKey) + if err != nil { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Failed to unmarshal authorityKeyIdentifier extension: %v", err)} + } + if len(rest) != 0 { + return &lint.LintResult{Status: lint.Error, Details: "Unexpected trailing data after authorityKeyIdentifier extension"} + } + if authKey.KeyIdentifier == nil { + return &lint.LintResult{Status: lint.Error, Details: "keyIdentifier field is missing in authorityKeyIdentifier extension"} + } + if authKey.AuthorityCertIssuer != nil || authKey.AuthorityCertSerialNumber != nil { + return &lint.LintResult{Status: lint.Error, Details: "Forbidden authorityCertIssuer or authorityCertSerialNumber in authorityKeyIdentifier extension"} + } + } + return &lint.LintResult{Status: lint.Pass} + +} + +type authKey struct { + KeyIdentifier []byte `asn1:"optional,tag:0"` + AuthorityCertIssuer []byte `asn1:"optional,tag:1"` + AuthorityCertSerialNumber *big.Int `asn1:"optional,tag:2"` +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_extensions.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_extensions.go new file mode 100644 index 00000000000..c79a7dd21a2 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_extensions.go @@ -0,0 +1,95 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zcrypto/x509/pkix" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_extensions_validity", + Description: "Checks that only allowed extensions are present in a CRL and that their criticality is set correctly.", + Citation: "BRs: 7.2.2", + Source: lint.CABFBaselineRequirements, + }, + Lint: func() lint.RevocationListLintInterface { return &crlExtensions{} }, + }) +} + +type crlExtensions struct { + // allowedExtensions maps the OID of an allowed extension to a boolean + // indicating whether the extension MUST be marked critical. + allowedExtensions map[string]bool +} + +// newCRLExtensions initializes and returns a new crlExtensions lint. +// This function is not called directly but is used by the ZLint framework. +func (l *crlExtensions) Initialize() { + l.allowedExtensions = map[string]bool{ + util.CRLNumberOID.String(): false, // cRLNumber + util.AuthkeyOID.String(): false, // authorityKeyIdentifier + util.IssuingDistOID.String(): true, // issuingDistributionPoint + } +} + +// CheckApplies returns true for any CRL, as all CRLs must be checked for extension validity. +func (l *crlExtensions) CheckApplies(c *x509.RevocationList) bool { + return true +} + +// isExtensionAllowed checks if a given extension is in the list of allowed or discouraged extensions. +func (l *crlExtensions) isExtensionAllowed(ext pkix.Extension) bool { + oid := ext.Id.String() + if _, ok := l.allowedExtensions[oid]; ok { + return true + } + return false +} + +// Execute performs the linting checks on the CRL extensions. +func (l *crlExtensions) Execute(c *x509.RevocationList) *lint.LintResult { + l.Initialize() + // First, check for any extensions that are explicitly forbidden. + for _, ext := range c.Extensions { + if !l.isExtensionAllowed(ext) { + return &lint.LintResult{ + Status: lint.Warn, + Details: fmt.Sprintf("CRL Extension %s is NOT RECOMMENDED", ext.Id), + } + } + } + + // Second, check that the criticality of allowed extensions is correct. + for _, ext := range c.Extensions { + oid := ext.Id.String() + if mustBeCritical, ok := l.allowedExtensions[oid]; ok { + if ext.Critical != mustBeCritical { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("CRL extension %s has incorrect criticality; expected %t, got %t", ext.Id, mustBeCritical, ext.Critical), + } + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_number_range.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_number_range.go new file mode 100644 index 00000000000..15a2724b802 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_crl_number_range.go @@ -0,0 +1,72 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + "math/big" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" +) + +/* + * Baseline Requirements: 7.2.2 CRL and CRL entry extensions + * CRLNumber MUST be an INTEGER greater than or equal + * to zero (0) and less than 2^159 and convey a strictly + * increasing sequence. + */ + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_number_out_of_range", + Description: "The CRL number must be greater than or equal to 0 and less than 2^159.", + Citation: "BRs: 7.2.2", + Source: lint.CABFBaselineRequirements, + }, + Lint: func() lint.RevocationListLintInterface { return &crlNumberLimit{} }, + }) +} + +type crlNumberLimit struct{} + +func (*crlNumberLimit) CheckApplies(c *x509.RevocationList) bool { + return true +} + +// Execute checks that the CRL number is within the valid range [0, 2^159). +func (*crlNumberLimit) Execute(c *x509.RevocationList) *lint.LintResult { + if c.Number == nil { + return &lint.LintResult{ + Status: lint.Error, + Details: "CRL number extension is missing", + } + } + if c.Number.Cmp(big.NewInt(0)) < 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("CRL number is negative: %v", c.Number), + } + } + crlNumberUpperBound := new(big.Int).Exp(big.NewInt(2), big.NewInt(159), nil) + if c.Number.Cmp(crlNumberUpperBound) >= 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("CRL number is greater than or equal to 2^159: %v", c.Number), + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_dsa_improper_modulus_or_divisor_size.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_dsa_improper_modulus_or_divisor_size.go index 83a497528c3..84e8f3f417d 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_dsa_improper_modulus_or_divisor_size.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_dsa_improper_modulus_or_divisor_size.go @@ -50,8 +50,8 @@ func (l *dsaImproperSize) Execute(c *x509.Certificate) *lint.LintResult { if !ok { return &lint.LintResult{Status: lint.NA} } - L := dsaKey.Parameters.P.BitLen() - N := dsaKey.Parameters.Q.BitLen() + L := dsaKey.P.BitLen() + N := dsaKey.Q.BitLen() if (L == 2048 && N == 224) || (L == 2048 && N == 256) || (L == 3072 && N == 256) { return &lint.LintResult{Status: lint.Pass} } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_100_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_100_days.go new file mode 100644 index 00000000000..5a92ebce33d --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_100_days.go @@ -0,0 +1,79 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081SecondDate100ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_server_cert_valid_time_longer_than_100_days", + Description: "TLS server certificates issued on or after on or after March 15, 2027 00:00 GMT/UTC must not have a validity period greater than 100 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_SECOND_MILESTONE, + IneffectiveDate: util.CABF_SC081_THIRD_MILESTONE, + }, + Lint: NewSC081SecondDate100ServerCertValidityTooLong, + }) +} + +func NewSC081SecondDate100ServerCertValidityTooLong() lint.LintInterface { + return &sc081SecondDate100ServerCertValidityTooLong{} +} + +func (l *sc081SecondDate100ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081SecondDate100ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 100) { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2027 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_200_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_200_days.go new file mode 100644 index 00000000000..9990529a569 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_200_days.go @@ -0,0 +1,79 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081FirstDate200ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_server_cert_valid_time_longer_than_200_days", + Description: "TLS server certificates issued on or after on or after March 15, 2026 00:00 GMT/UTC must not have a validity period greater than 200 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_FIRST_MILESTONE, + IneffectiveDate: util.CABF_SC081_SECOND_MILESTONE, + }, + Lint: NewSC081FirstDate200ServerCertValidityTooLong, + }) +} + +func NewSC081FirstDate200ServerCertValidityTooLong() lint.LintInterface { + return &sc081FirstDate200ServerCertValidityTooLong{} +} + +func (l *sc081FirstDate200ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081FirstDate200ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 200) { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2026 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_47_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_47_days.go new file mode 100644 index 00000000000..8a73911b198 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_e_server_cert_valid_time_longer_than_47_days.go @@ -0,0 +1,78 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081ThirdDate47ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_server_cert_valid_time_longer_than_47_days", + Description: "TLS server certificates issued on or after on or after March 15, 2029 00:00 GMT/UTC must not have a validity period greater than 47 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_THIRD_MILESTONE, + }, + Lint: NewSC081ThirdDate47ServerCertValidityTooLong, + }) +} + +func NewSC081ThirdDate47ServerCertValidityTooLong() lint.LintInterface { + return &sc081ThirdDate47ServerCertValidityTooLong{} +} + +func (l *sc081ThirdDate47ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081ThirdDate47ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 47) { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2029 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ec_improper_curves.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ec_improper_curves.go index 4309c979a7d..96e703fd1d6 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ec_improper_curves.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ec_improper_curves.go @@ -63,7 +63,7 @@ func (l *ecImproperCurves) Execute(c *x509.Certificate) *lint.LintResult { theKey = keyType } /* Now can actually check the params */ - theParams := theKey.Curve.Params() + theParams := theKey.Params() switch theParams.Name { case "P-256", "P-384", "P-521": return &lint.LintResult{Status: lint.Pass} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ecc_allowed_key_usages.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ecc_allowed_key_usages.go new file mode 100644 index 00000000000..858e833f42e --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ecc_allowed_key_usages.go @@ -0,0 +1,97 @@ +package cabf_br + +/* + * ZLint Copyright 2026 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* +7.1.2.7.11 Subscriber Certificate Key Usage +The acceptable Key Usage values vary based on whether the Certificate’s +subjectPublicKeyInfo identifies an RSA public key or an ECC public key. CAs MUST ensure +the Key Usage is appropriate for the Certificate Public Key. + +Table 56: Key Usage for ECC Public Keys + + +-------------------+-----------+------------------+ + | Key Usage | Permitted | Required | + +-------------------+-----------+------------------+ + | digitalSignature | Y | MUST | + | nonRepudiation | N | – | + | keyEncipherment | N | – | + | dataEncipherment | N | – | + | keyAgreement | Y | NOT RECOMMENDED | + | keyCertSign | N | – | + | cRLSign | N | – | + | encipherOnly | N | – | + | decipherOnly | N | – | + +-------------------+-----------+------------------+ +*/ +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cabf_ecc_allowed_key_usages", + Description: "For certificates with ECC public keys, digitalSignature MUST be present and only digitalSignature and keyAgreement key usages are allowed.", + Citation: "Section 7.1.2.7.11", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABFBRs_2_0_0_Date, // This specific table exists in the CABF BRs as early as 2.0.0 + }, + Lint: NewEccAllowedKU, + }) +} + +type eccAllowedKU struct{} + +func NewEccAllowedKU() lint.LintInterface { + return &eccAllowedKU{} +} + +// CheckApplies returns true on subscriber certificates when the certificate +// has an ECC public key and a key usage extension. +func (l *eccAllowedKU) CheckApplies(c *x509.Certificate) bool { + return c.PublicKeyAlgorithm == x509.ECDSA && + util.HasKeyUsageOID(c) && + util.IsSubscriberCert(c) +} + +func (l *eccAllowedKU) Execute(c *x509.Certificate) *lint.LintResult { + allowedKeyUsages := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement + + if c.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "DigitalSignature key usage is required for certificates with ECC public keys", + } + } + + if c.KeyUsage&x509.KeyUsageKeyAgreement != 0 { + return &lint.LintResult{ + Status: lint.Warn, + Details: "KeyAgreement key usage is not recommended for certificates with ECC public keys", + } + } + + if c.KeyUsage & ^allowedKeyUsages != 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "Only DigitalSignature and KeyAgreement key usages are allowed for certificates with ECC public keys", + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_excessively_backdated.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_excessively_backdated.go new file mode 100644 index 00000000000..556e9655c7c --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_excessively_backdated.go @@ -0,0 +1,69 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "time" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_excessively backdated", + Description: "notBefore [must be] a value within 48 hours of the certificate signing", + Citation: "TLS BRs §7.1.2.7", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC62EffectiveDate, + }, + Lint: NewExcessivelyBackdated, + }) +} + +type ExcessivelyBackdated struct{} + +func NewExcessivelyBackdated() lint.LintInterface { + return &ExcessivelyBackdated{} +} + +func (l *ExcessivelyBackdated) CheckApplies(c *x509.Certificate) bool { + + return len(c.SignedCertificateTimestampList) > 0 +} + +func (l *ExcessivelyBackdated) Execute(c *x509.Certificate) *lint.LintResult { + + // The notBefore must be within 48 hours of the certificate signing (TLS BRs §7.1.2.7) + // (and the signing time cannot be earlier than the timestamp contained in any SCTs) + var maxDelayHours float64 = 48 + + for _, sct := range c.SignedCertificateTimestampList { + + t := time.UnixMilli(int64(sct.Timestamp)) + deltaTime := t.Sub(c.NotBefore) + deltaHours := deltaTime.Hours() + + if deltaHours > maxDelayHours { + return &lint.LintResult{ + Status: lint.Error, + Details: "The Certificate's notBefore is more than 48 hours older than at least one embedded SCT", + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_missing_crl_distrib_point.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_missing_crl_distrib_point.go new file mode 100644 index 00000000000..b587e453236 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_missing_crl_distrib_point.go @@ -0,0 +1,72 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "time" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_missing_crl_distrib_point", + Description: "Checks for the CDP extension in non-Short-lived Subscriber Certificates lacking an OCSP pointer", + Citation: "CABF BRs section 7.1.2.11.2 (CRL Distribution Points)", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC63EffectiveDate, + }, + Lint: NewMissingCRLDistribPoint, + }) +} + +type MissingCRLDistribPoint struct{} + +func NewMissingCRLDistribPoint() lint.LintInterface { + return &MissingCRLDistribPoint{} +} + +func (l *MissingCRLDistribPoint) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) && !IsShortLivedCert(c) +} + +func (l *MissingCRLDistribPoint) Execute(c *x509.Certificate) *lint.LintResult { + + if len(c.CRLDistributionPoints) == 0 && len(c.OCSPServer) == 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "The CRLDistributionPoints extension MUST be present in " + + "non-Short-Lived certificates lacking an OCSP URI", + } + } + + return &lint.LintResult{Status: lint.Pass} +} + +// Based on CABF BRs §1.6.1 (Definitions) +func IsShortLivedCert(c *x509.Certificate) bool { + thresholdDate := time.Date(2026, time.March, 15, 0, 0, 0, 0, time.UTC) + tenDaysInSeconds := 864000 + sevenDaysInSeconds := 604800 + + if c.NotBefore.Before(thresholdDate) { + return c.ValidityPeriod <= tenDaysInSeconds + } else { + return c.ValidityPeriod <= sevenDaysInSeconds + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cdp_forbidden.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cdp_forbidden.go new file mode 100644 index 00000000000..94c24ee48dd --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cdp_forbidden.go @@ -0,0 +1,55 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ocsp_cert_cdp_forbidden", + Description: "In OCSP certificates, the CDP extension MUST NOT appear", + Citation: "CABF TLS BRs §7.1.2.8.2 OCSP Responder Extensions", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC62EffectiveDate, + OverrideFrameworkFilter: true, + }, + Lint: NewOcspCertCDPForbidden, + }) +} + +type OcspCertCDPForbidden struct{} + +func NewOcspCertCDPForbidden() lint.LintInterface { + return &OcspCertCDPForbidden{} +} + +func (l *OcspCertCDPForbidden) CheckApplies(c *x509.Certificate) bool { + return util.HasEKU(c, x509.ExtKeyUsageOcspSigning) +} + +func (l *OcspCertCDPForbidden) Execute(c *x509.Certificate) *lint.LintResult { + if len(c.CRLDistributionPoints) > 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "An OSCP Responder certificate MUST NOT contain the CRLDistributionPoints extension", + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cp_forbidden.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cp_forbidden.go new file mode 100644 index 00000000000..cdc74056817 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_cp_forbidden.go @@ -0,0 +1,55 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ocsp_cert_cp_forbidden", + Description: "In OCSP certificates, the CP extension MUST NOT appear", + Citation: "CABF TLS BRs §7.1.2.8.2 OCSP Responder Extensions", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC62EffectiveDate, + OverrideFrameworkFilter: true, + }, + Lint: NewOcspCertCPForbidden, + }) +} + +type OcspCertCPForbidden struct{} + +func NewOcspCertCPForbidden() lint.LintInterface { + return &OcspCertCPForbidden{} +} + +func (l *OcspCertCPForbidden) CheckApplies(c *x509.Certificate) bool { + return util.HasEKU(c, x509.ExtKeyUsageOcspSigning) +} + +func (l *OcspCertCPForbidden) Execute(c *x509.Certificate) *lint.LintResult { + if len(c.PolicyIdentifiers) > 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "An OSCP Responder certificate MUST NOT contain the CertificatePolicies extension", + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_invalid_ku.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_invalid_ku.go new file mode 100644 index 00000000000..009604b8730 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_cert_invalid_ku.go @@ -0,0 +1,55 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ocsp_cert_invalid_ku", + Description: "For OCSP certificates, only digitalSignature is allowed in the KU ext", + Citation: "CABF TLS BRs §7.1.2.8.7 OCSP Responder Key Usage", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.SC62EffectiveDate, + OverrideFrameworkFilter: true, + }, + Lint: NewOcspCertInvalidKeyUsage, + }) +} + +type OcspCertInvalidKeyUsage struct{} + +func NewOcspCertInvalidKeyUsage() lint.LintInterface { + return &OcspCertInvalidKeyUsage{} +} + +func (l *OcspCertInvalidKeyUsage) CheckApplies(c *x509.Certificate) bool { + return util.HasEKU(c, x509.ExtKeyUsageOcspSigning) +} + +func (l *OcspCertInvalidKeyUsage) Execute(c *x509.Certificate) *lint.LintResult { + if (c.KeyUsage & ^x509.KeyUsageDigitalSignature) > 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "In an OSCP Responder certificate, only digitalSignature is allowed in the KeyUsage extension", + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_id_pkix_ocsp_nocheck_ext_not_included_server_auth.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_id_pkix_ocsp_nocheck_ext_not_included_server_auth.go index ecc0d8cba97..4a8091de3a0 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_id_pkix_ocsp_nocheck_ext_not_included_server_auth.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_ocsp_id_pkix_ocsp_nocheck_ext_not_included_server_auth.go @@ -28,9 +28,10 @@ func init() { Name: "e_ocsp_id_pkix_ocsp_nocheck_ext_not_included_server_auth", Description: "OCSP signing Certificate MUST contain an extension of type id-pkixocsp-nocheck, as" + " defined by RFC6960", - Citation: "BRs: 4.9.9", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Citation: "BRs: 4.9.9", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + OverrideFrameworkFilter: true, }, Lint: NewOCSPIDPKIXOCSPNocheckExtNotIncludedServerAuth, }) @@ -41,7 +42,7 @@ func NewOCSPIDPKIXOCSPNocheckExtNotIncludedServerAuth() lint.LintInterface { } func (l *OCSPIDPKIXOCSPNocheckExtNotIncludedServerAuth) CheckApplies(c *x509.Certificate) bool { - return util.IsDelegatedOCSPResponderCert(c) && util.IsServerAuthCert(c) + return util.IsDelegatedOCSPResponderCert(c) } func (l *OCSPIDPKIXOCSPNocheckExtNotIncludedServerAuth) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_root_ca_rsa_mod_less_than_2048_bits.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_root_ca_rsa_mod_less_than_2048_bits.go index e16c9a06c10..f5708d397b1 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_root_ca_rsa_mod_less_than_2048_bits.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_root_ca_rsa_mod_less_than_2048_bits.go @@ -15,8 +15,7 @@ package cabf_br */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_ca_rsa_mod_less_than_1024_bits.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_ca_rsa_mod_less_than_1024_bits.go index 527d8e3b458..2ed31f5a522 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_ca_rsa_mod_less_than_1024_bits.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_ca_rsa_mod_less_than_1024_bits.go @@ -17,8 +17,7 @@ package cabf_br // CHANGE THIS COMMENT TO MATCH SOURCE TEXT import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_cert_rsa_mod_less_than_1024_bits.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_cert_rsa_mod_less_than_1024_bits.go index 03af2178289..1bcdc3a624c 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_cert_rsa_mod_less_than_1024_bits.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_old_sub_cert_rsa_mod_less_than_1024_bits.go @@ -15,8 +15,7 @@ package cabf_br */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_factors_smaller_than_752_bits.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_factors_smaller_than_752_bits.go index 81c0961d510..c88989e94b1 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_factors_smaller_than_752_bits.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_factors_smaller_than_752_bits.go @@ -15,8 +15,7 @@ package cabf_br */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_less_than_2048_bits.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_less_than_2048_bits.go index e2eb036a030..d4bab5a36f8 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_less_than_2048_bits.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_less_than_2048_bits.go @@ -15,8 +15,7 @@ package cabf_br */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_not_odd.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_not_odd.go index 0ab938329ac..bfe46489eb1 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_not_odd.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_mod_not_odd.go @@ -15,9 +15,9 @@ package cabf_br */ import ( - "crypto/rsa" "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_in_range.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_in_range.go index 69a193944fb..6dbec9b8199 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_in_range.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_in_range.go @@ -15,9 +15,9 @@ package cabf_br */ import ( - "crypto/rsa" "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -60,8 +60,8 @@ func (l *rsaParsedTestsExpInRange) CheckApplies(c *x509.Certificate) bool { func (l *rsaParsedTestsExpInRange) Execute(c *x509.Certificate) *lint.LintResult { key := c.PublicKey.(*rsa.PublicKey) exponent := key.E - const lowerBound = 65537 // 2^16 + 1 - if exponent >= lowerBound && l.upperBound.Cmp(big.NewInt(int64(exponent))) == 1 { + lowerBound := big.NewInt(65537) // 2^16 + 1 + if exponent.Cmp(lowerBound) >= 0 && l.upperBound.Cmp(exponent) == 1 { return &lint.LintResult{Status: lint.Pass} } return &lint.LintResult{Status: lint.Warn} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_odd.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_odd.go index af71f1d2336..84b2b233bcb 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_odd.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_not_odd.go @@ -15,8 +15,7 @@ package cabf_br */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -53,7 +52,7 @@ func (l *rsaParsedTestsKeyExpOdd) CheckApplies(c *x509.Certificate) bool { func (l *rsaParsedTestsKeyExpOdd) Execute(c *x509.Certificate) *lint.LintResult { key := c.PublicKey.(*rsa.PublicKey) - if key.E%2 == 1 { + if key.E.Bit(0) == 1 { return &lint.LintResult{Status: lint.Pass} } else { return &lint.LintResult{Status: lint.Error} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_too_small.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_too_small.go index 351cbb67d0b..d108866421b 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_too_small.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_rsa_public_exponent_too_small.go @@ -15,8 +15,9 @@ package cabf_br */ import ( - "crypto/rsa" + "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -53,7 +54,7 @@ func (l *rsaParsedTestsExpBounds) CheckApplies(c *x509.Certificate) bool { func (l *rsaParsedTestsExpBounds) Execute(c *x509.Certificate) *lint.LintResult { key := c.PublicKey.(*rsa.PublicKey) - if key.E >= 3 { //If Cmp returns 1, means N > E + if key.E.Cmp(big.NewInt(3)) >= 0 { return &lint.LintResult{Status: lint.Pass} } else { return &lint.LintResult{Status: lint.Error} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_state_or_province_name_must_not_contain_control_characters.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_state_or_province_name_must_not_contain_control_characters.go new file mode 100644 index 00000000000..ae0d8bafc10 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_state_or_province_name_must_not_contain_control_characters.go @@ -0,0 +1,65 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "regexp" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + // The issue that was at play here was that CAs were accidentally injecting structured data (such as JSON + // or key-value pairs) into the stateOrProvince field. This is not legal as stateOrProvince needs to be sourced + // from an authoritative database if plain, human readable, names. + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_state_or_province_name_must_not_contain_control_characters", + Description: "stateOrProvinceName MUST come from an authoritative data source of plain, human readable, names", + Citation: "CABF/BRs 3.2.2.1", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + }, + Lint: NewStateOrProvinceNameMustNotContainControlCharacters, + }) +} + +type StateOrProvinceNameMustNotContainControlCharacters struct{} + +var controlCharsRegex = regexp.MustCompile(`[=:{};"|\\[\]]`) + +func NewStateOrProvinceNameMustNotContainControlCharacters() lint.LintInterface { + return &StateOrProvinceNameMustNotContainControlCharacters{} +} + +func (l *StateOrProvinceNameMustNotContainControlCharacters) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *StateOrProvinceNameMustNotContainControlCharacters) Execute(c *x509.Certificate) *lint.LintResult { + for _, province := range c.Subject.Province { + if controlCharsRegex.MatchString(province) { + return &lint.LintResult{Status: lint.Error} + } + } + for _, locality := range c.Subject.Locality { + if controlCharsRegex.MatchString(locality) { + return &lint.LintResult{Status: lint.Error} + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_sub_ca_aia_does_not_contain_issuing_ca_url.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_sub_ca_aia_does_not_contain_issuing_ca_url.go index c65bced54d1..faaae3fafbd 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_sub_ca_aia_does_not_contain_issuing_ca_url.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_sub_ca_aia_does_not_contain_issuing_ca_url.go @@ -35,11 +35,12 @@ It SHOULD contain the HTTP URL of the Issuing CA’s certificate (accessMethod = func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "w_sub_ca_aia_does_not_contain_issuing_ca_url", - Description: "Subordinate CA Certificate: authorityInformationAccess SHOULD also contain the HTTP URL of the Issuing CA's certificate.", - Citation: "BRs: 7.1.2.2", - Source: lint.CABFBaselineRequirements, - EffectiveDate: util.CABEffectiveDate, + Name: "w_sub_ca_aia_does_not_contain_issuing_ca_url", + Description: "Subordinate CA Certificate: authorityInformationAccess SHOULD also contain the HTTP URL of the Issuing CA's certificate.", + Citation: "BRs: 7.1.2.2", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABEffectiveDate, + IneffectiveDate: util.CABFBRs_2_0_0_Date, }, Lint: NewSubCaIssuerUrl, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_199_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_199_days.go new file mode 100644 index 00000000000..859bb61ec65 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_199_days.go @@ -0,0 +1,79 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081FirstDate199ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "w_server_cert_valid_time_longer_than_199_days", + Description: "TLS server certificates issued on or after on or after March 15, 2026 00:00 GMT/UTC should not have a validity period greater than 199 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_FIRST_MILESTONE, + IneffectiveDate: util.CABF_SC081_SECOND_MILESTONE, + }, + Lint: NewSC081FirstDate199ServerCertValidityTooLong, + }) +} + +func NewSC081FirstDate199ServerCertValidityTooLong() lint.LintInterface { + return &sc081FirstDate199ServerCertValidityTooLong{} +} + +func (l *sc081FirstDate199ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081FirstDate199ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 199) { + return &lint.LintResult{Status: lint.Warn, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2026 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_46_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_46_days.go new file mode 100644 index 00000000000..b73ded5304c --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_46_days.go @@ -0,0 +1,79 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081ThirdDate46ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "w_server_cert_valid_time_longer_than_46_days", + Description: "TLS server certificates issued on or after on or after March 15, 2029 00:00 GMT/UTC should not have a validity period greater than 46 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_THIRD_MILESTONE, + }, + Lint: NewSC081ThirdDate46ServerCertValidityTooLong, + }) +} + +func NewSC081ThirdDate46ServerCertValidityTooLong() lint.LintInterface { + return &sc081ThirdDate46ServerCertValidityTooLong{} +} + +func (l *sc081ThirdDate46ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081ThirdDate46ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 46) { + return &lint.LintResult{Status: lint.Warn, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2029 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} + +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_99_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_99_days.go new file mode 100644 index 00000000000..a8c6d3b86df --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_br/lint_w_server_cert_valid_time_longer_than_99_days.go @@ -0,0 +1,79 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_br + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type sc081SecondDate99ServerCertValidityTooLong struct{} + +/************************************************************************ + +CA/B-Forum SC-081 introduces new validity periods for certificates issued on or after + +March 15, 2026 +March 15, 2027 +March 15, 2029 + +The change in the requirements is described here: https://github.com/cabforum/servercert/pull/553/files + +Subscriber Certificates issued on or after 15 March 2026 and before 15 March 2027 SHOULD NOT have a Validity Period greater than 199 days and MUST NOT have a Validity Period greater than 200 days. + +Subscriber Certificates issued on or after 15 March 2027 and before 15 March 2029 SHOULD NOT have a Validity Period greater than 99 days and MUST NOT have a Validity Period greater than 100 days. + +Subscriber Certificates issued on or after 15 March 2029 SHOULD NOT have a Validity Period greater than 46 days and MUST NOT have a Validity Period greater than 47 days. + +| __Certificate issued on or after__ | __Certificate issued before__ | __Maximum Validity Period__ | +| -- | -- | -- | +| | March 15, 2026 | 398 days | +| March 15, 2026 | March 15, 2027 | 200 days | +| March 15, 2027 | March 15, 2029 | 100 days | +| March 15, 2029 | | 47 days | + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "w_server_cert_valid_time_longer_than_99_days", + Description: "TLS server certificates issued on or after on or after March 15, 2027 00:00 GMT/UTC should not have a validity period greater than 99 days", + Citation: "https://github.com/cabforum/servercert/pull/553", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABF_SC081_SECOND_MILESTONE, + IneffectiveDate: util.CABF_SC081_THIRD_MILESTONE, + }, + Lint: NewSC081SecondDate99ServerCertValidityTooLong, + }) +} + +func NewSC081SecondDate99ServerCertValidityTooLong() lint.LintInterface { + return &sc081SecondDate99ServerCertValidityTooLong{} +} + +func (l *sc081SecondDate99ServerCertValidityTooLong) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && !c.IsCA +} + +func (l *sc081SecondDate99ServerCertValidityTooLong) Execute(c *x509.Certificate) *lint.LintResult { + if util.GreaterThan(c, 99) { + return &lint.LintResult{Status: lint.Warn, Details: fmt.Sprintf("Certificate is issued on or after March 15, 2027 and has a validity of %.0f days", util.CertificateValidityInDays(c))} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_missing_ca_issuers_http_url.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_missing_ca_issuers_http_url.go new file mode 100644 index 00000000000..49468d4475d --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_missing_ca_issuers_http_url.go @@ -0,0 +1,66 @@ +package cabf_cs_br + +import ( + "fmt" + "net/url" + "strings" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/*7.1.2.3 a. authorityInformationAccess +It MUST contain the HTTP URL of the Issuing CA's certificate +(accessMethod = 1.3.6.1.5.5.7.48.2).*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_aia_missing_ca_issuers_http_url", + Description: "The authorityInformationAccess extension MUST contain the HTTP URL of the Issuing CA's certificate (id-ad-caIssuers).", + Citation: "CABF CS BRs 7.1.2.3.a", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsAiaMissingCaIssuersHttpUrl, + }) +} + +type csAiaMissingCaIssuersHttpUrl struct{} + +func NewCsAiaMissingCaIssuersHttpUrl() lint.LintInterface { + return &csAiaMissingCaIssuersHttpUrl{} +} + +func (l *csAiaMissingCaIssuersHttpUrl) CheckApplies(c *x509.Certificate) bool { + return (util.IsSubscriberCert(c) || util.IsSubCA(c)) +} + +func (l *csAiaMissingCaIssuersHttpUrl) Execute(c *x509.Certificate) *lint.LintResult { + if len(c.IssuingCertificateURL) == 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "authorityInformationAccess MUST include an id-ad-caIssuers HTTP URL.", + } + } + + for _, u := range c.IssuingCertificateURL { + purl, err := url.Parse(u) + if err != nil { + return &lint.LintResult{ + Status: lint.Error, + Details: "Could not parse caIssuers in AIA.", + } + } + + if !strings.EqualFold(purl.Scheme, "http") { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("Found scheme %s in caIssuers of AIA, which is not allowed.", purl.Scheme), + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_ocsp_not_http.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_ocsp_not_http.go new file mode 100644 index 00000000000..104288cc06f --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_aia_ocsp_not_http.go @@ -0,0 +1,51 @@ +package cabf_cs_br + +import ( + "fmt" + "net/url" + "strings" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/*7.1.2.3 a. authorityInformationAccess +If the CA provides OCSP responses, it MUST contain the HTTP URL of the Issuing +CA's OCSP responder (accessMethod = 1.3.6.1.5.5.7.48.1).*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_aia_ocsp_not_http", + Description: "If the CA provides OCSP responses, the authorityInformationAccess extension MUST contain the HTTP URL of the Issuing CA's OCSP responder (id-ad-ocsp).", + Citation: "CABF CS BRs 7.1.2.3.a", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsAiaOcspNotHttp, + }) +} + +type csAiaOcspNotHttp struct{} + +func NewCsAiaOcspNotHttp() lint.LintInterface { + return &csAiaOcspNotHttp{} +} + +func (l *csAiaOcspNotHttp) CheckApplies(c *x509.Certificate) bool { + return (util.IsSubscriberCert(c) || util.IsSubCA(c)) && len(c.OCSPServer) > 0 +} + +func (l *csAiaOcspNotHttp) Execute(c *x509.Certificate) *lint.LintResult { + for _, u := range c.OCSPServer { + purl, err := url.Parse(u) + if err != nil { + return &lint.LintResult{Status: lint.Error, Details: "Could not parse OCSP URL in AIA."} + } + if !strings.EqualFold(purl.Scheme, "http") { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("Found scheme %s in OCSP URL of AIA, which is not allowed.", purl.Scheme)} + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_allowed_signature_algorithm.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_allowed_signature_algorithm.go new file mode 100644 index 00000000000..8902f46a991 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_allowed_signature_algorithm.go @@ -0,0 +1,88 @@ +package cabf_cs_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/*7.1.3.2.1 RSA +The CA SHALL use one of the following signature algorithms: + RSASSA-PKCS1-v1_5 with SHA-256 + RSASSA-PKCS1-v1_5 with SHA-384 + RSASSA-PKCS1-v1_5 with SHA-512 + RSASSA-PSS with SHA-256 + RSASSA-PSS with SHA-384 + RSASSA-PSS with SHA-512 + +In addition, the CA MAY use RSASSA-PKCS1-v1_5 with SHA-1 if one of the following conditions are met: + It is used within Timestamp Authority Certificate and the date of the notBefore field is not greater than 2022-04-30; or, + It is used within an OCSP response; or, + It is used within a CRL; or, + It is used within a Timestamp Token and the date of the genTime field is not greater than 2022-04-30. + +7.1.3.2.2 ECDSA +The CA SHALL use one of the following signature algorithms: + ECDSA with SHA-256 + ECDSA with SHA-384 + ECDSA with SHA-512 + +7.1.3.2.3 DSA +The CA SHALL use the following signature algorithm: + DSA with SHA-256 + +In addition, the CA MAY use DSA with SHA-1 if one of the following conditions are met: + It is used within Timestamp Authority Certificate and the date of the notBefore field is not greater than 2022-04-30; or, + It is used within an OCSP response; or, + It is used within a CRL; or, + It is used within a Timestamp Token and the date of the genTime field is not greater than 2022-04-30. +*/ + +var ( + passSigAlgs = map[x509.SignatureAlgorithm]bool{ + x509.SHA256WithRSAPSS: true, + x509.SHA384WithRSAPSS: true, + x509.SHA512WithRSAPSS: true, + x509.SHA256WithRSA: true, + x509.SHA384WithRSA: true, + x509.SHA512WithRSA: true, + x509.ECDSAWithSHA256: true, + x509.ECDSAWithSHA384: true, + x509.ECDSAWithSHA512: true, + x509.DSAWithSHA256: true, + } +) + +type csSignatureAlgorithmNotSupported struct{} + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_signature_algorithm_not_supported", + Description: "Certificates MUST meet the following requirements for algorithm Source: SHA-1*, SHA-256, SHA-384, SHA-512", + Citation: "CABF CS BRs 7.1.3.2", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsSignatureAlgorithmNotSupported, + }) +} + +func NewCsSignatureAlgorithmNotSupported() lint.CertificateLintInterface { + return &csSignatureAlgorithmNotSupported{} +} + +func (l *csSignatureAlgorithmNotSupported) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *csSignatureAlgorithmNotSupported) Execute(c *x509.Certificate) *lint.LintResult { + sigAlg := c.SignatureAlgorithm + status := lint.Error + if passSigAlgs[sigAlg] { + status = lint.Pass + } + return &lint.LintResult{ + Status: status, + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_authority_information_access.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_authority_information_access.go new file mode 100644 index 00000000000..3cfd5b3167d --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_authority_information_access.go @@ -0,0 +1,50 @@ +package cabf_cs_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/*7.1.2.3 a. authorityInformationAccess +This extension MUST be present. It MUST NOT be marked critical.*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_authority_information_access", + Description: "The authorityInformationAccess extension MUST be present and MUST NOT be marked critical.", + Citation: "CABF CS BRs 7.1.2.3.a", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsAuthorityInformationAccess, + }) +} + +type csAuthorityInformationAccess struct{} + +func NewCsAuthorityInformationAccess() lint.LintInterface { + return &csAuthorityInformationAccess{} +} + +func (l *csAuthorityInformationAccess) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) || util.IsSubCA(c) +} + +func (l *csAuthorityInformationAccess) Execute(c *x509.Certificate) *lint.LintResult { + aia := util.GetExtFromCert(c, util.AiaOID) + if aia == nil { + return &lint.LintResult{ + Status: lint.Error, + Details: "authorityInformationAccess extension MUST be present."} + } + + if aia.Critical { + return &lint.LintResult{ + Status: lint.Error, + Details: "authorityInformationAccess extension MUST NOT be marked critical."} + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_ecdsa_curve_params.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_ecdsa_curve_params.go new file mode 100644 index 00000000000..44e8de83360 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_ecdsa_curve_params.go @@ -0,0 +1,64 @@ +package cabf_cs_br + +import ( + "crypto/ecdsa" + + "github.com/zmap/zcrypto/x509" + + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/*6.1.5.2 Code signing Certificate and Timestamp Authority key sizes +If the Key is ECDSA, then the curve MUST be one of NIST P-256, P-384, or P-521. + +6.1.6 Public key parameters generation and quality checking +ECDSA: The CA SHOULD confirm the validity of all keys using either the ECC Full +Public Key Validation Routine or the ECC Partial Public Key Validation Routine. +[Source: Sections 5.6.2.3.2 and 5.6.2.3.3, respectively, of NIST SP 800-56A: Revision 2] +*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_ecdsa_prohibited_curve", + Description: "If the Key is ECDSA, then the curve MUST be one of NIST P-256, P-384, or P-521", + Citation: "CABF CS BRs 6.1.5.2", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsEcdsaProhibitedCurve, + }) +} + +type csEcdsaProhibitedCurve struct{} + +func NewCsEcdsaProhibitedCurve() lint.CertificateLintInterface { + return &csEcdsaProhibitedCurve{} +} + +func (l *csEcdsaProhibitedCurve) CheckApplies(c *x509.Certificate) bool { + return c.PublicKeyAlgorithm == x509.ECDSA +} + +func (l *csEcdsaProhibitedCurve) Execute(c *x509.Certificate) *lint.LintResult { + var key *ecdsa.PublicKey + switch k := c.PublicKey.(type) { + case *x509.AugmentedECDSA: + key = k.Pub + case *ecdsa.PublicKey: + key = k + default: + return &lint.LintResult{Status: lint.NA} + } + + switch key.Curve.Params().Name { + case "P-256", "P-384", "P-521": + return &lint.LintResult{Status: lint.Pass} + default: + return &lint.LintResult{ + Status: lint.Error, + Details: "ECDSA key curve must be one of NIST P-256, P-384, or P-521", + } + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_prohibited_subject.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_prohibited_subject.go new file mode 100644 index 00000000000..58f406811d4 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_prohibited_subject.go @@ -0,0 +1,45 @@ +package cabf_cs_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* +7.1.4.2.2 Subject distinguished name fields - EV and Non-EV Code Signing Certificates +c. Certificate Field: subject:domainComponent (OID 0.9.2342.19200300.100.1.25) +Required/Optional: Prohibited +Contents: This field MUST not be present in a Code Signing Certificate. +*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_subject_prohibited", + Description: "The subject:domainComponent MUST not be present in a Code Signing Certificate.", + Citation: "CABF CS BRs 7.1.4.2.3.c", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, + }, + Lint: NewCsSubjectProhibited, + }) +} + +type csSubjectProhibited struct{} + +func NewCsSubjectProhibited() lint.LintInterface { + return &csSubjectProhibited{} +} + +func (l *csSubjectProhibited) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) +} + +func (l *csSubjectProhibited) Execute(c *x509.Certificate) *lint.LintResult { + if len(c.Subject.DomainComponent) > 0 { + return &lint.LintResult{Status: lint.Error, Details: "Domain Component MUST not be present in a Code Signing Certificate."} + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_rsa_key_size.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_rsa_key_size.go index 493e3793d7c..bf97a1adeba 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_rsa_key_size.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_rsa_key_size.go @@ -1,10 +1,8 @@ package cabf_cs_br import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" - "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" ) diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_39_months.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_39_months.go new file mode 100644 index 00000000000..75718c7e701 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_39_months.go @@ -0,0 +1,50 @@ +package cabf_cs_br + +import ( + "github.com/zmap/zcrypto/x509" + + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* +6.3.2 Certificate operational periods and key pair usage periods +For Code Signing Certificates issued before March 1st, 2026, the validity period MUST NOT exceed +39 months. For Code Signing Certificates issued on or after March 1st, 2026, the validity period +MUST NOT exceed 460 days. +*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_max_validity_period_39_months", + Description: "Code Signing certificate validity must not exceed 39 months for certificates issued before March 1st, 2026", + Citation: "CS BR 6.3.2 - v3.10", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_BRs_1_2_Date, // Effective from v1.2, the quote is from v3.10 + IneffectiveDate: util.CABF_CS_CSC_31_Date, + }, + Lint: NewCsMaxValidityPeriodLongerThan39Months, + }) +} + +type csMaxValidityPeriodLongerThan39Months struct{} + +func NewCsMaxValidityPeriodLongerThan39Months() lint.CertificateLintInterface { + return &csMaxValidityPeriodLongerThan39Months{} +} + +func (l *csMaxValidityPeriodLongerThan39Months) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) +} + +func (l *csMaxValidityPeriodLongerThan39Months) Execute(c *x509.Certificate) *lint.LintResult { + // difference between notBefore and notAfter MUST not be longer than 39 months + maxValidity := c.NotBefore.AddDate(0, 39, 0) + + if c.NotAfter.After(maxValidity) { + return &lint.LintResult{Status: lint.Error, Details: "Code Signing certificates must have a validity period of 39 months or less"} + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_460_days.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_460_days.go new file mode 100644 index 00000000000..6f80ed854b5 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_cs_br/lint_cs_validity_period_longer_than_460_days.go @@ -0,0 +1,49 @@ +package cabf_cs_br + +import ( + "github.com/zmap/zcrypto/x509" + + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* +6.3.2 Certificate operational periods and key pair usage periods +For Code Signing Certificates issued before March 1st, 2026, the validity period MUST NOT exceed +39 months. For Code Signing Certificates issued on or after March 1st, 2026, the validity period +MUST NOT exceed 460 days. +*/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_cs_max_validity_period_460_days", + Description: "Code Signing certificate validity must not exceed 460 days for certificates issued on or after March 1st, 2026", + Citation: "CS BR 6.3.2 - v3.10", + Source: lint.CABFCSBaselineRequirements, + EffectiveDate: util.CABF_CS_CSC_31_Date, + }, + Lint: NewCsMaxValidityPeriodLongerThan460Days, + }) +} + +type csMaxValidityPeriodLongerThan460Days struct{} + +func NewCsMaxValidityPeriodLongerThan460Days() lint.CertificateLintInterface { + return &csMaxValidityPeriodLongerThan460Days{} +} + +func (l *csMaxValidityPeriodLongerThan460Days) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) +} + +func (l *csMaxValidityPeriodLongerThan460Days) Execute(c *x509.Certificate) *lint.LintResult { + // difference between notBefore and notAfter MUST not be longer than 460 days + maxValidity := c.NotBefore.AddDate(0, 0, 460) + + if c.NotAfter.After(maxValidity) { + return &lint.LintResult{Status: lint.Error, Details: "Code Signing certificates must have a validity period of 460 days or less"} + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_cabf_policy_missing.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_cabf_policy_missing.go new file mode 100644 index 00000000000..2313166691b --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_cabf_policy_missing.go @@ -0,0 +1,58 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_exactly_one_smime_policy", + Description: "The subscriber cert SHALL include exactly one of the reserved policy OIDs in §7.1.6.1", + Citation: "CABF SMIME BRs §7.1.2.3 Subscriber certificates", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.CABF_SMIME_BRs_1_0_0_Date, + }, + Lint: NewCABFPolicyMissing, + }) +} + +type CABFPolicyMissing struct{} + +func NewCABFPolicyMissing() lint.LintInterface { + return &CABFPolicyMissing{} +} + +func (l *CABFPolicyMissing) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) +} + +func (l *CABFPolicyMissing) Execute(c *x509.Certificate) *lint.LintResult { + + if util.ContainsExactlyOneSMIMEPolicy(c.PolicyIdentifiers) { + return &lint.LintResult{ + Status: lint.Pass, + } + } + + return &lint.LintResult{ + Status: lint.Error, + Details: "There must be exactly one CABF SMIME BR reserved policy OID in CertificatePolicies", + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_e_mailbox_validated_allowed_subjectdn_attributes.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_e_mailbox_validated_allowed_subjectdn_attributes.go new file mode 100644 index 00000000000..4354f551807 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_e_mailbox_validated_allowed_subjectdn_attributes.go @@ -0,0 +1,93 @@ +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/************************************************************************* + +7.1.4.2.3 Subject DN attributes for mailbox?validated profile: +|Attribute | Legacy | Multipurpose| Strict +|commonName | MAY | MAY | MAY +|organizationName | SHALL NOT | SHALL NOT | SHALL NOT +|organizationalUnitName | SHALL NOT | SHALL NOT | SHALL NOT +|organizationIdentifier | SHALL NOT | SHALL NOT | SHALL NOT +|givenName | SHALL NOT | SHALL NOT | SHALL NOT +|surname | SHALL NOT | SHALL NOT | SHALL NOT +|pseudonym | SHALL NOT | SHALL NOT | SHALL NOT +|serialNumber | MAY | MAY | MAY +|emailAddress | MAY | MAY | MAY +|title | SHALL NOT | SHALL NOT | SHALL NOT +|streetAddress | SHALL NOT | SHALL NOT | SHALL NOT +|localityName | SHALL NOT | SHALL NOT | SHALL NOT +|stateOrProvinceName | SHALL NOT | SHALL NOT | SHALL NOT +|postalCode | SHALL NOT | SHALL NOT | SHALL NOT +|countryName | SHALL NOT | SHALL NOT | SHALL NOT +|Other | SHALL NOT | SHALL NOT | SHALL NOT + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_mailbox_validated_allowed_subjectdn_attributes", + Description: "Only certain Subject DN attributes are permitted to be present in mailbox-validated certificates.", + Citation: "S/MIME BRs: 7.1.4.2.3", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.CABF_SMIME_BRs_1_0_0_Date, + }, + Lint: NewMBVSubjectAttributes, + }) +} + +type mbvSubjectAttributes struct{} + +func NewMBVSubjectAttributes() lint.LintInterface { + return &mbvSubjectAttributes{} +} + +func (l *mbvSubjectAttributes) CheckApplies(c *x509.Certificate) bool { + return util.IsMailboxValidatedCertificate(c) +} + +func (l *mbvSubjectAttributes) Execute(c *x509.Certificate) *lint.LintResult { + rdnSequence := util.RawRDNSequence{} + rest, err := asn1.Unmarshal(c.RawSubject, &rdnSequence) + if err != nil { + return &lint.LintResult{Status: lint.Fatal} + } + if len(rest) > 0 { + return &lint.LintResult{Status: lint.Fatal} + } + + notAllowedAttributeFound := false + for _, attrTypeAndValueSet := range rdnSequence { + for _, attrTypeAndValue := range attrTypeAndValueSet { + if !attrTypeAndValue.Type.Equal(util.CommonNameOID) && !attrTypeAndValue.Type.Equal(util.SerialOID) && !attrTypeAndValue.Type.Equal(util.EmailAddressOID) { + notAllowedAttributeFound = true + } + } + } + + if notAllowedAttributeFound { + return &lint.LintResult{Status: lint.Error} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_ecpublickey_other_key_usages.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_ecpublickey_other_key_usages.go index 659288ac71b..5c7a19a126c 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_ecpublickey_other_key_usages.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_ecpublickey_other_key_usages.go @@ -44,7 +44,7 @@ func (l *ecOtherKeyUsages) CheckApplies(c *x509.Certificate) bool { } func (l *ecOtherKeyUsages) Execute(c *x509.Certificate) *lint.LintResult { - if !(util.HasKeyUsage(c, x509.KeyUsageDigitalSignature) || util.HasKeyUsage(c, x509.KeyUsageKeyAgreement)) { + if !util.HasKeyUsage(c, x509.KeyUsageDigitalSignature) && !util.HasKeyUsage(c, x509.KeyUsageKeyAgreement) { if c.KeyUsage != 0 { return &lint.LintResult{Status: lint.Error} } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_individual_identity.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_individual_identity.go new file mode 100644 index 00000000000..5d0559c2451 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_individual_identity.go @@ -0,0 +1,73 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_invalid_individual_identity", + Description: "Non-legacy IV and SV certificates... SHALL include either subject:givenName and/or subject:surname, or the subject:pseudonym.", + Citation: "CABF S/MIME BR 7.1.4.2.5 and 7.1.4.2.6", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.CABF_SMIME_BRs_1_0_0_Date, + }, + Lint: NewInvalidPersonalSubject, + }) +} + +type InvalidPersonalSubject struct{} + +func NewInvalidPersonalSubject() lint.LintInterface { + return &InvalidPersonalSubject{} +} + +func (l *InvalidPersonalSubject) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) && !util.IsLegacySMIMECertificate(c) && + (util.IsIndividualValidatedCertificate(c) || util.IsSponsorValidatedCertificate(c)) +} + +func (l *InvalidPersonalSubject) Execute(c *x509.Certificate) *lint.LintResult { + + if !isPseudonymPresent(c) && !isPersonalNamePresent(c) { + return &lint.LintResult{ + Status: lint.Error, + Details: "Non-Legacy IV and SV S/MIME certificates MUST contain either a Personal Name or a Pseudonym", + } + } + return &lint.LintResult{Status: lint.Pass} +} + +func isPersonalNamePresent(c *x509.Certificate) bool { + return len(c.Subject.GivenName) > 0 || len(c.Subject.Surname) > 0 +} + +func isPseudonymPresent(c *x509.Certificate) bool { + + pseudonymOID := asn1.ObjectIdentifier{2, 5, 4, 65} + + for _, atv := range c.Subject.Names { + if atv.Type.Equal(pseudonymOID) { + return true + } + } + return false +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_legacy_spki_alogid.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_legacy_spki_alogid.go new file mode 100644 index 00000000000..4fe83d9d991 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_legacy_spki_alogid.go @@ -0,0 +1,94 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// This lint checks that the AlgorithmIdentifier, within the SubjectKeyInfo field, +// is valid according to the CABF S/MIME BR section xxx. It does so by comparing +// the entire DER encoding of the AlgorithmIdentifier with a list of allowed +// encodings, as set out in the BR. Since a few PQC algorithms have been added +// by SCMxx to the initial list of allowed algorithms in BR 1.0.0, we perform +// this check taking into account the issuance date (notBefore) of the certificate. + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "bytes" + "encoding/asn1" + "fmt" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_invalid_legacy_spki_algoid", + Description: "Checks that SubjectPublicKeyInfo.AlgorithmIdentifier is allowed", + Citation: "CABF S/MIME BR 7.1.3.1", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.CABF_SMIME_BRs_1_0_0_Date, + IneffectiveDate: util.CABF_SMIME_BRs_1_0_11_Date, + }, + Lint: NewInvalidSPKIAlgoId, + }) +} + +type InvalidLegacySPKIAlgoId struct{} + +func NewInvalidLegacySPKIAlgoId() lint.LintInterface { + return &InvalidLegacySPKIAlgoId{} +} + +func (l *InvalidLegacySPKIAlgoId) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *InvalidLegacySPKIAlgoId) Execute(c *x509.Certificate) *lint.LintResult { + var allowedAlgoIds = [6][]byte{ + // RSA + {0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00}, + // P-256 + {0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}, + // P-384 + {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22}, + // P-521 + {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23}, + // EdDSA Curve25519 + {0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70}, + // EdDSA Curve448 + {0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71}, + } + type SubjectPublicKeyInfo struct { + Algorithm asn1.RawValue + SubjectPublicKey asn1.BitString + } + spki := SubjectPublicKeyInfo{} + _, err := asn1.Unmarshal(c.RawSubjectPublicKeyInfo, &spki) + if err != nil { + return &lint.LintResult{ + Status: lint.Fatal, + Details: fmt.Sprintf("Cannot decode SubjectPublicKeyInfo: %v", err), + } + } + for _, algoId := range allowedAlgoIds { + if bytes.Equal(spki.Algorithm.FullBytes, algoId) { + return &lint.LintResult{Status: lint.Pass} + } + } + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("Invalid Subject Public Key Algorithm Identifier: %X", spki.Algorithm.FullBytes), + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_spki_algoid.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_spki_algoid.go new file mode 100644 index 00000000000..1d0293f8f3d --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_invalid_spki_algoid.go @@ -0,0 +1,105 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// This lint checks that the AlgorithmIdentifier, within the SubjectKeyInfo field, +// is valid according to the CABF S/MIME BR section xxx. It does so by comparing +// the entire DER encoding of the AlgorithmIdentifier with a list of allowed +// encodings, as set out in the BR. Since a few PQC algorithms have been added +// by SCMxx to the initial list of allowed algorithms in BR 1.0.0, we perform +// this check taking into account the issuance date (notBefore) of the certificate. + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "bytes" + "encoding/asn1" + "fmt" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_invalid_spki_algoid", + Description: "Checks that SubjectPublicKeyInfo.AlgorithmIdentifier is allowed, including PQC algorithms.", + Citation: "CABF S/MIME BR 7.1.3.1", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.CABF_SMIME_BRs_1_0_11_Date, + }, + Lint: NewInvalidSPKIAlgoId, + }) +} + +type InvalidSPKIAlgoId struct{} + +func NewInvalidSPKIAlgoId() lint.LintInterface { + return &InvalidSPKIAlgoId{} +} + +func (l *InvalidSPKIAlgoId) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *InvalidSPKIAlgoId) Execute(c *x509.Certificate) *lint.LintResult { + var allowedAlgoIds = [12][]byte{ + // RSA + {0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00}, + // P-256 + {0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}, + // P-384 + {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22}, + // P-521 + {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23}, + // EdDSA Curve25519 + {0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70}, + // EdDSA Curve448 + {0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71}, + // ML-DSA-44 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11}, + // ML-DSA-65 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12}, + // ML-DSA-87 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13}, + // ML-KEM-512 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01}, + // ML-KEM-768 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02}, + // ML-KEM-1024 + {0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03}, + } + type SubjectPublicKeyInfo struct { + Algorithm asn1.RawValue + SubjectPublicKey asn1.BitString + } + spki := SubjectPublicKeyInfo{} + _, err := asn1.Unmarshal(c.RawSubjectPublicKeyInfo, &spki) + if err != nil { + return &lint.LintResult{ + Status: lint.Fatal, + Details: fmt.Sprintf("Cannot decode SubjectPublicKeyInfo: %v", err), + } + } + for _, algoId := range allowedAlgoIds { + if bytes.Equal(spki.Algorithm.FullBytes, algoId) { + return &lint.LintResult{Status: lint.Pass} + } + } + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("Invalid Subject Public Key Algorithm Identifier: %X", spki.Algorithm.FullBytes), + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_legacy_gen_deprecated.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_legacy_gen_deprecated.go new file mode 100644 index 00000000000..0f20c6fa654 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_legacy_gen_deprecated.go @@ -0,0 +1,52 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cabf_smime_br + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_legacy_generation_deprecated", + Description: "S/MIME Subscriber Certificates SHALL NOT be issued using the Legacy Generation profiles", + Citation: "CABF SMIME BRs v1.0.6 implementing the results of ballot SMC08", + Source: lint.CABFSMIMEBaselineRequirements, + EffectiveDate: util.SMC08EffectiveDate, + }, + Lint: NewLegacyGenerationDeprecated, + }) +} + +type LegacyGenerationDeprecated struct{} + +func NewLegacyGenerationDeprecated() lint.LintInterface { + return &LegacyGenerationDeprecated{} +} + +func (l *LegacyGenerationDeprecated) CheckApplies(c *x509.Certificate) bool { + return util.IsLegacySMIMECertificate(c) +} + +func (l *LegacyGenerationDeprecated) Execute(c *x509.Certificate) *lint.LintResult { + return &lint.LintResult{ + Status: lint.Error, + Details: "Legacy generation S/MIME policies are deprecated since " + + util.SMC08EffectiveDate.Format("January 2, 2006"), + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_legacy_multipurpose.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_legacy_multipurpose.go index eb318106a2f..c40dff17a2d 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_legacy_multipurpose.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_legacy_multipurpose.go @@ -15,8 +15,7 @@ package cabf_smime_br import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_strict.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_strict.go index b61de15eef1..0d2d8c3aad9 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_strict.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_key_usage_strict.go @@ -15,8 +15,7 @@ package cabf_smime_br import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -42,7 +41,7 @@ func NewRSAKeyUsageStrict() lint.LintInterface { } func (l *rsaKeyUsageStrict) CheckApplies(c *x509.Certificate) bool { - if !(util.IsSubscriberCert(c) && util.IsStrictSMIMECertificate(c) && util.IsExtInCert(c, util.KeyUsageOID)) { + if !util.IsSubscriberCert(c) || !util.IsStrictSMIMECertificate(c) || !util.IsExtInCert(c, util.KeyUsageOID) { return false } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_other_key_usages.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_other_key_usages.go index b16d86780f3..cdafc6ea890 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_other_key_usages.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_rsa_other_key_usages.go @@ -15,8 +15,7 @@ package cabf_smime_br import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -42,7 +41,7 @@ func NewRSAOtherKeyUsages() lint.LintInterface { } func (l *rsaOtherKeyUsages) CheckApplies(c *x509.Certificate) bool { - if !(util.IsSubscriberCert(c) && util.IsSMIMEBRCertificate(c) && util.IsExtInCert(c, util.KeyUsageOID)) { + if !util.IsSubscriberCert(c) || !util.IsSMIMEBRCertificate(c) || !util.IsExtInCert(c, util.KeyUsageOID) { return false } @@ -51,7 +50,7 @@ func (l *rsaOtherKeyUsages) CheckApplies(c *x509.Certificate) bool { } func (l *rsaOtherKeyUsages) Execute(c *x509.Certificate) *lint.LintResult { - if !(util.HasKeyUsage(c, x509.KeyUsageDigitalSignature) || util.HasKeyUsage(c, x509.KeyUsageKeyEncipherment)) { + if !util.HasKeyUsage(c, x509.KeyUsageDigitalSignature) && !util.HasKeyUsage(c, x509.KeyUsageKeyEncipherment) { if c.KeyUsage != 0 { return &lint.LintResult{Status: lint.Error} } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_single_email_if_present.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_single_email_if_present.go index d9731d55992..03d03d04be5 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_single_email_if_present.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_single_email_if_present.go @@ -30,7 +30,7 @@ All Mailbox Addresses in the subject field or entries of type dirName of this ex repeated as rfc822Name or otherName values of type id-on-SmtpUTF8Mailbox in this extension. -7.1.4.2.2 Subject distinguished name fields +7.1.4.2.2 Subject distinguished name fields //nolint:dupword // dup comes from the specification h. Certificate Field: subject:emailAddress (1.2.840.113549.1.9.1) Contents: If present, the subject:emailAddress SHALL contain a single Mailbox Address as verified under diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_subject_country_name.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_subject_country_name.go index 07a7dfd02c2..d8a58d0d955 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_subject_country_name.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/lint_subject_country_name.go @@ -1,5 +1,5 @@ /* - * ZLint Copyright 2024 Regents of the University of Michigan + * ZLint Copyright 2025 Regents of the University of Michigan * * 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 @@ -22,6 +22,31 @@ import ( "github.com/zmap/zlint/v3/util" ) +/************************************************************************* +7.1.4.2.2 Subject distinguished name fields + +n. Certificate Field: subject:countryName (OID: 2.5.4.6) +Contents: If present, the subject:countryName SHALL contain the two-letter ISO 3166-1 +country code associated with the location of the Subject verified under Section 3.2.3 for +Organization-validated and Sponsor-validated Certificate Types or Section 3.2.4 for +Individual-validated Certificate Types. If a Country is not represented by an official ISO 3166-1 +country code, the CA MAY specify the ISO 3166-1 user-assigned code of XX indicating that an +official ISO 3166-1 alpha-2 code has not been assigned. + +See also: +7.1.4.2.3 Subject DN attributes for mailbox-validated profile: +countryName SHALL NOT SHALL NOT SHALL NOT + +7.1.4.2.4 Subject DN attributes for organization-validated profile: +countryName MAY //nolint:dupword + +7.1.4.2.5 Subject DN attributes for sponsor-validated profile: +countryName MAY //nolint:dupword + +7.1.4.2.6 Subject DN attributes for individual-validated profile: +countryName MAY //nolint:dupword +*************************************************************************/ + func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ @@ -42,7 +67,7 @@ func NewSubjectCountryName() lint.LintInterface { } func (l *subjectCountryName) CheckApplies(c *x509.Certificate) bool { - return util.IsMailboxValidatedCertificate(c) + return util.IsOrganizationValidatedCertificate(c) || util.IsSponsorValidatedCertificate(c) || util.IsIndividualValidatedCertificate(c) } func (l *subjectCountryName) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/mailbox_address_from_san.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/mailbox_address_from_san.go index 139b051d666..336322ef4a3 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/mailbox_address_from_san.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/mailbox_address_from_san.go @@ -45,7 +45,7 @@ func NewMailboxAddressFromSAN() lint.LintInterface { // CheckApplies is returns true if the certificate's policies assert that it conforms to the SMIME BRs func (l *MailboxAddressFromSAN) CheckApplies(c *x509.Certificate) bool { - if !(util.IsSMIMEBRCertificate(c) && util.IsSubscriberCert(c)) { + if !util.IsSMIMEBRCertificate(c) || !util.IsSubscriberCert(c) { return false } diff --git a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/smime_legacy_multipurpose_eku_check.go b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/smime_legacy_multipurpose_eku_check.go index 8f3ac35e1c4..56cf6b01709 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/smime_legacy_multipurpose_eku_check.go +++ b/vendor/github.com/zmap/zlint/v3/lints/cabf_smime_br/smime_legacy_multipurpose_eku_check.go @@ -61,9 +61,10 @@ func (l *legacyMultipurposeEKUCheck) Execute(c *x509.Certificate) *lint.LintResu ekusOK := true for _, eku := range c.ExtKeyUsage { - if eku == x509.ExtKeyUsageEmailProtection { + switch eku { + case x509.ExtKeyUsageEmailProtection: hasEmailProtectionEKU = true - } else if eku == x509.ExtKeyUsageServerAuth || eku == x509.ExtKeyUsageCodeSigning || eku == x509.ExtKeyUsageTimeStamping || eku == x509.ExtKeyUsageAny { + case x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageTimeStamping, x509.ExtKeyUsageAny: ekusOK = false } } diff --git a/vendor/github.com/zmap/zlint/v3/lints/chrome/lint_client_auth_not_allowed.go b/vendor/github.com/zmap/zlint/v3/lints/chrome/lint_client_auth_not_allowed.go new file mode 100644 index 00000000000..612844cda4b --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/chrome/lint_client_auth_not_allowed.go @@ -0,0 +1,58 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package chrome + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "slices" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_client_auth_not_allowed", + Description: "Checks that Server certs do not contain clientAuth in the EKU extension", + Citation: "Chrome Root Program Policy, Version 1.8, section 1.3.2", + Source: lint.ChromeRootStorePolicy, + EffectiveDate: util.ChromePolicyClientAuthDisallowedDate, + }, + Lint: NewClientAuthNotAllowed, + }) +} + +type ClientAuthNotAllowed struct{} + +func NewClientAuthNotAllowed() lint.LintInterface { + return &ClientAuthNotAllowed{} +} + +func (l *ClientAuthNotAllowed) CheckApplies(c *x509.Certificate) bool { + return util.IsServerAuthCert(c) && util.IsSubscriberCert(c) +} + +func (l *ClientAuthNotAllowed) Execute(c *x509.Certificate) *lint.LintResult { + + if slices.Contains(c.ExtKeyUsage, x509.ExtKeyUsageClientAuth) { + return &lint.LintResult{ + Status: lint.Error, + Details: "The Chrome Root Store Policy prohibits the clientAuth key purpose in the EKU extension", + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_no_duplicate_extensions.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_no_duplicate_extensions.go new file mode 100644 index 00000000000..d0ddb3f5205 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_no_duplicate_extensions.go @@ -0,0 +1,54 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package community + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" +) + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_no_duplicate_extensions", + Description: "The CRL must not include duplicate extensions.", + Source: lint.Community, + }, + Lint: func() lint.RevocationListLintInterface { return &noDuplicateExtensions{} }, + }) +} + +type noDuplicateExtensions struct{} + +// CheckApplies returns true if the CRL has any extensions to check. +func (l *noDuplicateExtensions) CheckApplies(c *x509.RevocationList) bool { + return len(c.Extensions) > 0 +} + +// Execute checks for duplicate extensions within the CRL. +func (l *noDuplicateExtensions) Execute(c *x509.RevocationList) *lint.LintResult { + extensions := make(map[string]struct{}) + for _, ext := range c.Extensions { + oid := ext.Id.String() + if _, ok := extensions[oid]; ok { + return &lint.LintResult{ + Status: lint.Error, + Details: "CRL contains duplicate extension " + oid, + } + } + extensions[oid] = struct{}{} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revocation_date_too_early.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revocation_date_too_early.go new file mode 100644 index 00000000000..820073ce572 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revocation_date_too_early.go @@ -0,0 +1,63 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package community + +import ( + "fmt" + "time" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* + * This lint checks that the revocation time for a revoked certificate is not too early. + * This is particularly useful when a programming language (e.g., Go) uses a default + * zero-value (0001-01-01T00:00:00Z) for a time if not set explicitly. + * For all intents and purposes, the revocation time for a revoked certificate should not be + * before the RFC 2459 (Internet X.509 Public Key Infrastructure Certificate and CRL Profile), + * which first introduced the CRL profile. + */ + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_revocation_date_too_early", + Description: "The revocation time of each revoked certificate should not before the publication date of RFC 2459.", + Source: lint.Community, + }, + Lint: func() lint.RevocationListLintInterface { return &revocationDateTooEarly{} }, + }) +} + +type revocationDateTooEarly struct{} + +func (l *revocationDateTooEarly) CheckApplies(c *x509.RevocationList) bool { + // This check applies to any CRL that has at least one revoked certificate. + return len(c.RevokedCertificates) > 0 +} + +func (l *revocationDateTooEarly) Execute(c *x509.RevocationList) *lint.LintResult { + for _, rc := range c.RevokedCertificates { + if rc.RevocationTime.Before(util.RFC2459Date) { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("Revoked certificate with serial number %x has a revocation time (%s) that is before RFC 2459", rc.SerialNumber, rc.RevocationTime.Format(time.RFC3339)), + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revoked_certificate_crl_entry_has_no_duplicate_extensions.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revoked_certificate_crl_entry_has_no_duplicate_extensions.go new file mode 100644 index 00000000000..67285d0dad1 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_crl_revoked_certificate_crl_entry_has_no_duplicate_extensions.go @@ -0,0 +1,57 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package community + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_revoked_certificate_crl_entry_has_no_duplicate_extensions", + Description: "The revoked certificate in the CRL must not have duplicate extensions.", + Source: lint.Community, + EffectiveDate: util.ZeroDate, + }, + Lint: func() lint.RevocationListLintInterface { return &noDuplicatesInCRLEntryExtension{} }, + }) +} + +type noDuplicatesInCRLEntryExtension struct{} + +func (l *noDuplicatesInCRLEntryExtension) CheckApplies(c *x509.RevocationList) bool { + return true +} + +func (l *noDuplicatesInCRLEntryExtension) Execute(c *x509.RevocationList) *lint.LintResult { + for _, rc := range c.RevokedCertificates { + crlEntryExtensions := make(map[string]bool) + for _, ext := range rc.Extensions { + if crlEntryExtensions[ext.Id.String()] { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("Revoked certificate %x has a duplicate extension: %s", rc.SerialNumber, ext.Id.String()), + } + } + crlEntryExtensions[ext.Id.String()] = true + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_exp_negative.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_exp_negative.go index dc0b4a0a8fc..d9d4b76b1a4 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_exp_negative.go +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_exp_negative.go @@ -15,8 +15,9 @@ package community */ import ( - "crypto/rsa" + "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -48,7 +49,7 @@ func (l *rsaExpNegative) CheckApplies(c *x509.Certificate) bool { func (l *rsaExpNegative) Execute(c *x509.Certificate) *lint.LintResult { key := c.PublicKey.(*rsa.PublicKey) - if key.E < 0 { + if key.E.Cmp(big.NewInt(0)) < 0 { return &lint.LintResult{Status: lint.Error} } return &lint.LintResult{Status: lint.Pass} diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_fermat_factorization.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_fermat_factorization.go index 1d6ac0a7ce0..68be058da31 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_fermat_factorization.go +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_fermat_factorization.go @@ -15,10 +15,10 @@ package community */ import ( - "crypto/rsa" "fmt" "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_no_public_key.go b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_no_public_key.go index 0539a19d591..719e940c10c 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_no_public_key.go +++ b/vendor/github.com/zmap/zlint/v3/lints/community/lint_rsa_no_public_key.go @@ -15,8 +15,7 @@ package community */ import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_correct_ku_setting.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_correct_ku_setting.go new file mode 100644 index 00000000000..6a3198520a1 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_correct_ku_setting.go @@ -0,0 +1,89 @@ +/* + * ZLint Copyright 2026 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package etsi + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcNaturalPersonKUCorrectSetting struct{} + +/************************************************ +4.3.2 Key usage +The key usage extension shall be present and shall contain one (and only one) of the key usage settings defined in +table 1 (A, B, C, D, E or F). Type A, C or E should be used to avoid mixed usage of keys. +************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_etsi_natural_person_key_usage_correct_values", + Description: "The key usage extension shall contain a valid key usage setting for ETSI certificates issued to natural persons", + Citation: "ETSI EN 319 412-2 V2.2.1 (2020-07) / Section 4.3.2", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_412_2_V2_2_1_Date, + }, + Lint: NewQcNaturalPersonKUCorrectSetting, + }) +} + +func NewQcNaturalPersonKUCorrectSetting() lint.LintInterface { + return &qcNaturalPersonKUCorrectSetting{} +} + +func (l *qcNaturalPersonKUCorrectSetting) CheckApplies(c *x509.Certificate) bool { + return util.IsEtsiQcNaturalPerson(c) && util.HasKeyUsageOID(c) && util.IsSubscriberCert(c) +} + +func (l *qcNaturalPersonKUCorrectSetting) Execute(c *x509.Certificate) *lint.LintResult { + + if c.KeyUsage == x509.KeyUsageContentCommitment { // Type A + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == (x509.KeyUsageContentCommitment | x509.KeyUsageDigitalSignature) { // Type B + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageDigitalSignature { // Type C + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == (x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment) { //Type D, Option 1 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == (x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement) { //Type D, Option 2 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageKeyEncipherment { //Type E, Option 1 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageKeyAgreement { //Type E, Option 2 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == (x509.KeyUsageContentCommitment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment) { //Type F, Option 1 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == (x509.KeyUsageContentCommitment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement) { //Type F, Option 2 + return &lint.LintResult{Status: lint.Pass} + } + + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("KeyUsage %v (%08b) is not allowed for ETSI natural person certificates", util.GetKeyUsageStrings(c.KeyUsage), c.KeyUsage), + } + +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_ku_mandatory.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_ku_mandatory.go new file mode 100644 index 00000000000..ea2fb67112a --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_ku_mandatory.go @@ -0,0 +1,58 @@ +/* + * ZLint Copyright 2026 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package etsi + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcNaturalPersonKUMandatory struct{} + +/************************************************ +4.3.2 Key usage +The key usage extension shall be present and shall contain one (and only one) of the key usage settings defined in +table 1 (A, B, C, D, E or F). Type A, C or E should be used to avoid mixed usage of keys. +************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_etsi_natural_person_key_usage_mandatory", + Description: "The key usage extension shall be present for ETSI certificates issued to natural persons", + Citation: "ETSI EN 319 412-2 V2.2.1 (2020-07) / Section 4.3.2", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_412_2_V2_2_1_Date, + }, + Lint: NewQcNaturalPersonKUMandatory, + }) +} + +func NewQcNaturalPersonKUMandatory() lint.LintInterface { + return &qcNaturalPersonKUMandatory{} +} + +func (l *qcNaturalPersonKUMandatory) CheckApplies(c *x509.Certificate) bool { + return util.IsEtsiQcNaturalPerson(c) && util.IsSubscriberCert(c) +} + +func (l *qcNaturalPersonKUMandatory) Execute(c *x509.Certificate) *lint.LintResult { + if util.HasKeyUsageOID(c) { + return &lint.LintResult{Status: lint.Pass} + } + return &lint.LintResult{Status: lint.Error, Details: "ETSI natural person certificates does not have the key usage extension"} + +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_prefered_ku_setting.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_prefered_ku_setting.go new file mode 100644 index 00000000000..4ab25a1cb94 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qc_np_prefered_ku_setting.go @@ -0,0 +1,73 @@ +/* + * ZLint Copyright 2026 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package etsi + +import ( + "fmt" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcNaturalPersonKUPreferredSetting struct{} + +/************************************************ +4.3.2 Key usage +The key usage extension shall be present and shall contain one (and only one) of the key usage settings defined in +table 1 (A, B, C, D, E or F). Type A, C or E should be used to avoid mixed usage of keys. +************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "w_etsi_natural_person_key_usage_preferred_values", + Description: "The key usage extension should use specific key usage settings for ETSI certificates issued to natural persons", + Citation: "ETSI EN 319 412-2 V2.2.1 (2020-07) / Section 4.3.2", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_412_2_V2_2_1_Date, + }, + Lint: NewQcNaturalPersonKUPreferredSetting, + }) +} + +func NewQcNaturalPersonKUPreferredSetting() lint.LintInterface { + return &qcNaturalPersonKUPreferredSetting{} +} + +func (l *qcNaturalPersonKUPreferredSetting) CheckApplies(c *x509.Certificate) bool { + return util.IsEtsiQcNaturalPerson(c) && util.HasKeyUsageOID(c) && util.IsSubscriberCert(c) +} + +func (l *qcNaturalPersonKUPreferredSetting) Execute(c *x509.Certificate) *lint.LintResult { + + if c.KeyUsage == x509.KeyUsageContentCommitment { // Type A + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageDigitalSignature { // Type C + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageKeyEncipherment { //Type E, Option 1 + return &lint.LintResult{Status: lint.Pass} + } + if c.KeyUsage == x509.KeyUsageKeyAgreement { //Type E, Option 2 + return &lint.LintResult{Status: lint.Pass} + } + return &lint.LintResult{ + Status: lint.Warn, + Details: fmt.Sprintf("KeyUsage (%08b) should not be used for ETSI natural person certificates", c.KeyUsage), + } + +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qcpds_https_url.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qcpds_https_url.go new file mode 100644 index 00000000000..c8dae46288a --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qcpds_https_url.go @@ -0,0 +1,113 @@ +package etsi + +/* + * ZLint Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ( + "fmt" + "strings" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcStatemPdsHttpsOnly struct{} + +/************************************************************************ + +ETSI EN 319 412-5 V2.4.1 (2023-09) +https://www.etsi.org/deliver/etsi_en/319400_319499/31941205/02.04.01_60/en_31941205v020401p.pdf#%5B%7B%22num%22%3A30%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22FitH%22%7D%2C381%5D + +4.3.4 QCStatement regarding location of PKI Disclosure Statements (PDS) +This QCStatement holds URLs to PKI Disclosure Statements (PDS) in accordance with Annex A of ETSI EN 319 411-1 [i.10]. + +Syntax: + +esi4-qcStatement-5 QC-STATEMENT ::= { SYNTAX QcEuPDS IDENTIFIED +BY id-etsi-qcs-QcPDS } + +QcEuPDS ::= PdsLocations //nolint:dupword + +PdsLocations ::= SEQUENCE SIZE (1..MAX) OF PdsLocation //nolint:dupword // dup comes from the specification + +PdsLocation::= SEQUENCE { +url IA5String, +language PrintableString (SIZE(2))} --ISO 639-1 language code + +id-etsi-qcs-QcPDS OBJECT IDENTIFIER ::= { id-etsi-qcs 5 } + +QCS-4.3.4-01: The language shall be as defined in ISO 639-1 [1]. + +QCS-4.3.4-02: Referenced PKI Disclosure Statements should be structured according to Annex A of ETSI +EN 319 411-1 [i.10]. + +The signature of the certificate does not cover the content of the PDS and hence does not protect the integrity of the +PDS which can change over time. End users trust in the accuracy of a PDS is therefore based on the mechanisms used +to protect the authenticity of the PDS. + +QCS-4.3.4-03: As a minimum, a URL to a PDS provided in this statement shall use the "https" (https://) scheme, IETF +RFC 2818 [5] or later documents updating this specification + +*************************************************************************/ + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_qcstatem_pds_must_have_https_only", + Description: "Checks that a QC Statement of the type id-etsi-qcs-QcPDS contains a URL that uses the https scheme.", + Citation: "ETSI EN 319 412 - 5 V2.4.1 (2023 - 09) / Section 4.3.4", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_412_5_V2_4_1_Date, + }, + Lint: NewQcStatemPdsHasHTTPSOnly, + }) +} + +func NewQcStatemPdsHasHTTPSOnly() lint.LintInterface { + return &qcStatemPdsHttpsOnly{} +} + +func (l *qcStatemPdsHttpsOnly) CheckApplies(c *x509.Certificate) bool { + qcEuPDS := &util.IdEtsiQcsQcEuPDS + if !util.IsExtInCert(c, util.QcStateOid) { + return false + } + if util.ParseQcStatem(util.GetExtFromCert(c, util.QcStateOid).Value, *qcEuPDS).IsPresent() { + return true + } + return false +} + +func (l *qcStatemPdsHttpsOnly) Execute(c *x509.Certificate) *lint.LintResult { + + ext := util.GetExtFromCert(c, util.QcStateOid) + s := util.ParseQcStatem(ext.Value, util.IdEtsiQcsQcEuPDS) + + errString := s.GetErrorInfo() + + if len(errString) != 0 { + return &lint.LintResult{Status: lint.Error, Details: "Could not parse qcStatement with PDS: " + errString} + } + + pds := s.(util.EtsiQcPds) + for _, loc := range pds.PdsLocations { + if !strings.HasPrefix(loc.Url, "https://") { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("PDS URL %s does not use the https scheme", loc.Url)} + } + } + + return &lint.LintResult{Status: lint.Pass} + +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_oneonly.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_oneonly.go new file mode 100644 index 00000000000..c1d076b5466 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_oneonly.go @@ -0,0 +1,89 @@ +/* + * ZLint Copyright 2026 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package etsi + +import ( + "fmt" + + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcStatemQctypeValidOneOnly struct{} + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_qcstatem_qctype_oneonly", + Description: "Checks that a QC Statement of the type Id-etsi-qcs-QcType features exactly one of the allowed QcType OIDs", + Citation: "ETSI EN 319 412 - 5 V2.5.0 (2025 - 03) / Section 4.2.3", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_411_2_V2_5_0_Date, + }, + Lint: NewQcStatemQctypeValidOneOnly, + }) +} + +func NewQcStatemQctypeValidOneOnly() lint.LintInterface { + return &qcStatemQctypeValidOneOnly{} +} + +func (this *qcStatemQctypeValidOneOnly) getStatementOid() *asn1.ObjectIdentifier { + return &util.IdEtsiQcsQcType +} + +func (l *qcStatemQctypeValidOneOnly) CheckApplies(c *x509.Certificate) bool { + if !util.IsExtInCert(c, util.QcStateOid) { + return false + } + if util.ParseQcStatem(util.GetExtFromCert(c, util.QcStateOid).Value, *l.getStatementOid()).IsPresent() { + return true + } + return false +} + +func (l *qcStatemQctypeValidOneOnly) Execute(c *x509.Certificate) *lint.LintResult { + + errString := "" + ext := util.GetExtFromCert(c, util.QcStateOid) + s := util.ParseQcStatem(ext.Value, *l.getStatementOid()) + errString += s.GetErrorInfo() + if len(errString) == 0 { + qcType := s.(util.Etsi423QcType) + if len(qcType.TypeOids) == 0 { + errString += "no QcType present, sequence of OIDs is empty" + } + if len(qcType.TypeOids) > 1 { + errString += "more than one QcType present, sequence must have exactly size 1" + } + for _, t := range qcType.TypeOids { + + if !t.Equal(util.IdEtsiQcsQctEsign) && !t.Equal(util.IdEtsiQcsQctEseal) && !t.Equal(util.IdEtsiQcsQctWeb) { + if len(errString) > 0 { + errString += "; " + } + errString += fmt.Sprintf("encountered invalid ETSI QcType OID: %v", t) + } + } + } + + if len(errString) == 0 { + return &lint.LintResult{Status: lint.Pass} + } else { + return &lint.LintResult{Status: lint.Error, Details: errString} + } +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_valid.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_valid.go index 0add14df4ac..c2b4d26f135 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_valid.go +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_valid.go @@ -1,5 +1,5 @@ /* - * ZLint Copyright 2024 Regents of the University of Michigan + * ZLint Copyright 2026 Regents of the University of Michigan * * 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 @@ -28,11 +28,12 @@ type qcStatemQctypeValid struct{} func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ - Name: "e_qcstatem_qctype_valid", - Description: "Checks that a QC Statement of the type Id-etsi-qcs-QcType features a non-empty list of only the allowed QcType OIDs", - Citation: "ETSI EN 319 412 - 5 V2.2.1 (2017 - 11) / Section 4.2.3", - Source: lint.EtsiEsi, - EffectiveDate: util.EtsiEn319_412_5_V2_2_1_Date, + Name: "e_qcstatem_qctype_valid", + Description: "Checks that a QC Statement of the type Id-etsi-qcs-QcType features a non-empty list of only the allowed QcType OIDs", + Citation: "ETSI EN 319 412 - 5 V2.2.1 (2017 - 11) / Section 4.2.3", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiEn319_412_5_V2_2_1_Date, + IneffectiveDate: util.EtsiEn319_411_2_V2_5_0_Date, }, Lint: NewQcStatemQctypeValid, }) diff --git a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_web.go b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_web.go index 5a71ea093aa..38c76e5fdc9 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_web.go +++ b/vendor/github.com/zmap/zlint/v3/lints/etsi/lint_qcstatem_qctype_web.go @@ -49,7 +49,7 @@ func (l *qcStatemQctypeWeb) CheckApplies(c *x509.Certificate) bool { return false } if util.ParseQcStatem(util.GetExtFromCert(c, util.QcStateOid).Value, *l.getStatementOid()).IsPresent() { - return util.IsServerAuthCert(c) + return util.HasEKU(c, x509.ExtKeyUsageServerAuth) } return false } diff --git a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_pub_key_encoding_correct.go b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_pub_key_encoding_correct.go index ecf72f9cb33..c4523a2c912 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_pub_key_encoding_correct.go +++ b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_pub_key_encoding_correct.go @@ -29,14 +29,17 @@ type ecdsaPubKeyAidEncoding struct{} /************************************************ https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/policy/ -When ECDSA keys are encoded in a SubjectPublicKeyInfo structure, the algorithm field MUST be one of the following, as -specified by RFC 5480, Section 2.1.1: +When ECDSA keys are encoded in a SubjectPublicKeyInfo structure, the algorithm field MUST be one of the following, +as specified by RFC 5480, Section 2.1.1: The encoded AlgorithmIdentifier for a P-256 key MUST match the following hex-encoded -bytes: > 301306072a8648ce3d020106082a8648ce3d030107. +bytes: 301306072a8648ce3d020106082a8648ce3d030107; The encoded AlgorithmIdentifier for a P-384 key MUST match the following hex-encoded -bytes: > 301006072a8648ce3d020106052b81040022. +bytes: 301006072a8648ce3d020106052b81040022; + +The encoded AlgorithmIdentifier for a P-521 key MUST match the following hex-encoded +bytes: 301006072a8648ce3d020106052b81040023. The above encodings consist of an ecPublicKey OID (1.2.840.10045.2.1) with a named curve parameter of the corresponding curve OID. Certificates MUST NOT use the implicit or specified curve forms. @@ -50,7 +53,7 @@ func init() { Description: "The encoded algorithm identifiers for ECDSA public keys MUST match specific bytes", Citation: "Mozilla Root Store Policy / Section 5.1.2", Source: lint.MozillaRootStorePolicy, - EffectiveDate: util.MozillaPolicy27Date, + EffectiveDate: util.MozillaPolicy30Date, }, Lint: NewEcdsaPubKeyAidEncoding, }) @@ -60,11 +63,13 @@ func NewEcdsaPubKeyAidEncoding() lint.LintInterface { return &ecdsaPubKeyAidEncoding{} } -var acceptedAlgIDEncodingsDER = [2][]byte{ +var acceptedAlgIDEncodingsDER = [3][]byte{ // encoded AlgorithmIdentifier for a P-256 key {0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}, // encoded AlgorithmIdentifier for a P-384 key {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22}, + // encoded AlgorithmIdentifier for P-521 key + {0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23}, } func (l *ecdsaPubKeyAidEncoding) CheckApplies(c *x509.Certificate) bool { diff --git a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_signature_encoding_correct.go b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_signature_encoding_correct.go index 0a1c7db1f8d..8546ad72d0d 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_signature_encoding_correct.go +++ b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_ecdsa_signature_encoding_correct.go @@ -38,6 +38,9 @@ following hex-encoded bytes: 300a06082a8648ce3d040302. If the signing key is P-384, the signature MUST use ECDSA with SHA-384. The encoded AlgorithmIdentifier MUST match the following hex-encoded bytes: 300a06082a8648ce3d040303. +If the signing key is P-521, the signature MUST use ECDSA with SHA-512. When encoded, the AlgorithmIdentifier MUST +be byte-for-byte identical with the following hex-encoded bytes: 300a06082a8648ce3d040304. + The above encodings consist of the corresponding OID with the parameters field omitted, as specified by RFC 5758, Section 3.2. Certificates MUST NOT include a NULL parameter. Note this differs from RSASSA-PKCS1-v1_5, which includes an explicit NULL. @@ -51,7 +54,7 @@ func init() { Description: "The encoded algorithm identifiers for ECDSA signatures MUST match specific hex-encoded bytes", Citation: "Mozilla Root Store Policy / Section 5.1.2", Source: lint.MozillaRootStorePolicy, - EffectiveDate: util.MozillaPolicy27Date, + EffectiveDate: util.MozillaPolicy30Date, }, Lint: NewEcdsaSignatureAidEncoding, }) @@ -71,6 +74,7 @@ func (l *ecdsaSignatureAidEncoding) CheckApplies(c *x509.Certificate) bool { c.SignatureAlgorithmOID.Equal(util.OidSignatureSHA224withECDSA) } +//nolint:nestif func (l *ecdsaSignatureAidEncoding) Execute(c *x509.Certificate) *lint.LintResult { // We must check consistency of the issuer public key to the signature algorithm // (see for example: If the signing key is P-256, the signature MUST use ECDSA with SHA-256. @@ -94,6 +98,8 @@ func (l *ecdsaSignatureAidEncoding) Execute(c *x509.Certificate) *lint.LintResul const maxP256SigByteLen = 72 // len <= 2+2+2+49+49 (= 104) const maxP384SigByteLen = 104 + // len <= 2+2+2+67+67 (= 140) + const maxP521SigByteLen = 140 if signatureSize <= maxP256SigByteLen { expectedEncoding := []byte{0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02} @@ -115,6 +121,15 @@ func (l *ecdsaSignatureAidEncoding) Execute(c *x509.Certificate) *lint.LintResul Status: lint.Error, Details: "Encoding of signature algorithm does not match signing key on P-384 curve. Got the unsupported " + hex.EncodeToString(encoded), } + } else if signatureSize <= maxP521SigByteLen { + expectedEncoding := []byte{0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04} + if bytes.Equal(encoded, expectedEncoding) { + return &lint.LintResult{Status: lint.Pass} + } + return &lint.LintResult{ + Status: lint.Error, + Details: "Encoding of signature algorithm does not match signing key on P-521 curve. Got the unsupported " + hex.EncodeToString(encoded), + } } return &lint.LintResult{ Status: lint.Error, diff --git a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_exponent_cannot_be_one.go b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_exponent_cannot_be_one.go index 05e4fbc2384..f3c20707f78 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_exponent_cannot_be_one.go +++ b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_exponent_cannot_be_one.go @@ -15,8 +15,9 @@ package mozilla import ( - "crypto/rsa" + "math/big" + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" @@ -60,7 +61,7 @@ func (l *exponentCannotBeOne) Execute(c *x509.Certificate) *lint.LintResult { } } - if pubKey.E == 1 { + if pubKey.E.Cmp(big.NewInt(1)) == 0 { return &lint.LintResult{Status: lint.Error} } diff --git a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_2048_bits_or_more.go b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_2048_bits_or_more.go index 2a15354d4f2..e74077cb4c6 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_2048_bits_or_more.go +++ b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_2048_bits_or_more.go @@ -15,8 +15,7 @@ package mozilla import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_divisible_by_8.go b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_divisible_by_8.go index fea9f485790..61c8856617d 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_divisible_by_8.go +++ b/vendor/github.com/zmap/zlint/v3/lints/mozilla/lint_mp_modulus_must_be_divisible_by_8.go @@ -15,8 +15,7 @@ package mozilla import ( - "crypto/rsa" - + "github.com/zmap/zcrypto/rsa" "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_basic_constr_invalid_der.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_basic_constr_invalid_der.go new file mode 100644 index 00000000000..b0485294044 --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_basic_constr_invalid_der.go @@ -0,0 +1,77 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package rfc + +import ( + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_basic_constr_invalid_der", + Description: "Checks the correct DER encoding of the cA field in the BasicConstraints ext", + Citation: "RFC 3280 §4.1: the data that is to be signed is encoded using [DER]", + Source: lint.RFC5280, + EffectiveDate: util.RFC2459Date, + }, + Lint: NewBasicConstraintsInvalidDER, + }) +} + +// Use an ad hoc structure so to be able to detect the undue +// presence of the IsCA field with its DEFAULT value (FALSE) +type BasicConstraints struct { + IsCA asn1.RawValue `asn1:"optional"` + PathLen int `asn1:"optional"` +} + +type BasicConstraintsInvalidDER struct{} + +func NewBasicConstraintsInvalidDER() lint.LintInterface { + return &BasicConstraintsInvalidDER{} +} + +func (l *BasicConstraintsInvalidDER) CheckApplies(c *x509.Certificate) bool { + return util.IsExtInCert(c, util.BasicConstOID) +} + +func (l *BasicConstraintsInvalidDER) Execute(c *x509.Certificate) *lint.LintResult { + ext := util.GetExtFromCert(c, util.BasicConstOID) + + basicConstr := BasicConstraints{} + _, err := asn1.Unmarshal(ext.Value, &basicConstr) + if err != nil { + return &lint.LintResult{ + Status: lint.Fatal, + Details: "Could not parse the BasicConstraints extension", + } + } + + if basicConstr.IsCA.Tag == asn1.TagBoolean && // the cA field is present + len(basicConstr.IsCA.Bytes) > 0 && // it has an explicit value + basicConstr.IsCA.Bytes[0] == 0 { // the value is FALSE + return &lint.LintResult{ + Status: lint.Error, + Details: "The BasicConstraints extension has an invalid DER encoding; " + + "fields with DEFAULT values must be omitted.", + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_revocation_time_not_after_this_update.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_revocation_time_not_after_this_update.go new file mode 100644 index 00000000000..22427e2bc3f --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_revocation_time_not_after_this_update.go @@ -0,0 +1,68 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package rfc + +import ( + "fmt" + "time" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +/* + * The thisUpdate field in a CRL indicates the time at which the CRL was issued. + * For each entry in the revokedCertificates list within the CRL, there is a + * revocationDate field. This revocationDate signifies when the certificate was revoked. + * Logically, a certificate cannot be listed as revoked on a CRL with a revocationDate + * that is after the thisUpdate of that CRL, because thisUpdate marks the point + * in time that the information in the CRL is considered valid. If a revocation + * happened after the CRL was issued, it would appear on a subsequent CRL. + */ + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_revocation_time_after_this_update", + Description: "All revocation times for revoked certificates must be on or before the thisUpdate field of the CRL.", + Citation: "RFC 5280: Section 5.1.2.4 & 5.1.2.6", + Source: lint.RFC5280, + EffectiveDate: util.RFC5280Date, + }, + Lint: func() lint.RevocationListLintInterface { return &revocationTimeNotAfterThisUpdate{} }, + }) +} + +type revocationTimeNotAfterThisUpdate struct{} + +// CheckApplies returns true if the CRL has any revoked certificates. +func (l *revocationTimeNotAfterThisUpdate) CheckApplies(c *x509.RevocationList) bool { + return len(c.RevokedCertificates) > 0 +} + +// Execute checks that for each revoked certificate, the revocation time is not after the CRL's thisUpdate time. +func (l *revocationTimeNotAfterThisUpdate) Execute(c *x509.RevocationList) *lint.LintResult { + for _, rc := range c.RevokedCertificates { + if rc.RevocationTime.After(c.ThisUpdate) { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("revoked certificate with serial number %x has a revocation time (%s) after the CRL's thisUpdate time (%s)", + rc.SerialNumber, rc.RevocationTime.Format(time.RFC3339), c.ThisUpdate.Format(time.RFC3339)), + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_sigalgo_missing_null_params.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_sigalgo_missing_null_params.go new file mode 100644 index 00000000000..fddbff08e8e --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_crl_sigalgo_missing_null_params.go @@ -0,0 +1,128 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package rfc + +import ( + "bytes" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" +) + +/* + From RFC 4055 section-6: + + -- When the following OIDs are used in an AlgorithmIdentifier, the + -- parameters MUST be present and MUST be NULL. + + sha224WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 14 } + + sha256WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 11 } + + sha384WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 12 } + + sha512WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 13 } +*/ + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_crl_sigalgo_missing_null_params", + Description: "Checks for mandatory NULL parameters in the SignatureAlgorithm", + Citation: "RFC 4055 Section 6", + Source: lint.RFC5280, // RFC4055 is referenced in RFC 5280, Section 1 + EffectiveDate: util.RFC5280Date, + }, + Lint: NewCRLSigAlgoMissingNullParams, + }) +} + +type CRLSigAlgoMissingNullParams struct{} + +func NewCRLSigAlgoMissingNullParams() lint.RevocationListLintInterface { + return &CRLSigAlgoMissingNullParams{} +} + +func (l *CRLSigAlgoMissingNullParams) CheckApplies(c *x509.RevocationList) bool { + return true +} + +var ( + sha224WithRSAEncryption = []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xe} + sha256WithRSAEncryption = []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb} + sha384WithRSAEncryption = []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xc} + sha512WithRSAEncryption = []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xd} +) + +func (l *CRLSigAlgoMissingNullParams) Execute(c *x509.RevocationList) *lint.LintResult { + input := cryptobyte.String(c.Raw) + + // Read the outer CRL sequence (CertificateList) + var crlBytes cryptobyte.String + if !input.ReadASN1(&crlBytes, asn1.SEQUENCE) { + return &lint.LintResult{Status: lint.Fatal, Details: "Could not parse CRL"} + } + + // Skip the tbsCertList element + if !crlBytes.SkipASN1(asn1.SEQUENCE) { + return &lint.LintResult{Status: lint.Fatal, Details: "Could not parse CRL"} + } + + // Read the signatureAlgorithm element + var signatureAlgorithmBytes cryptobyte.String + if !crlBytes.ReadASN1(&signatureAlgorithmBytes, asn1.SEQUENCE) { + return &lint.LintResult{Status: lint.Fatal, Details: "Could not parse CRL"} + } + + // Read the algorithm element + var algoBytes cryptobyte.String + if !signatureAlgorithmBytes.ReadASN1(&algoBytes, asn1.OBJECT_IDENTIFIER) { + return &lint.LintResult{Status: lint.Fatal, Details: "Could not parse CRL"} + } + + if bytes.Equal(algoBytes, sha224WithRSAEncryption) || + bytes.Equal(algoBytes, sha256WithRSAEncryption) || + bytes.Equal(algoBytes, sha384WithRSAEncryption) || + bytes.Equal(algoBytes, sha512WithRSAEncryption) { + + // Attempt to read the parameters element + var nullBytes cryptobyte.String + var nullFound bool + if !signatureAlgorithmBytes.ReadOptionalASN1(&nullBytes, &nullFound, asn1.NULL) { + return &lint.LintResult{Status: lint.Fatal, Details: "Could not parse CRL"} + } + + if !nullFound { + return &lint.LintResult{ + Status: lint.Error, + Details: "Missing required NULL parameter in the SignatureAlgorithm element", + } + } + + // This should never happen, as invalid DER is caught upstream, + // but let's check it for good measure + if len(nullBytes) != 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: "Invalid DER encoding of NULL in the SignatureAlgorithm element", + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_cannot_be_empty_seq.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_cannot_be_empty_seq.go new file mode 100644 index 00000000000..b88715c19ed --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_cannot_be_empty_seq.go @@ -0,0 +1,92 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package rfc + +import ( + "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "fmt" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_ext_cannot_be_empty_sequence", + Description: "Extensions whose value is SEQUENCE SIZE (1..MAX) OF must have at least 1 element", + Citation: "All of RFC 5280", + Source: lint.RFC5280, + EffectiveDate: util.RFC2459Date, + }, + Lint: NewExtCannotBeEmptySequence, + }) +} + +type ExtCannotBeEmptySequence struct{} + +func NewExtCannotBeEmptySequence() lint.LintInterface { + return &ExtCannotBeEmptySequence{} +} + +/* + * According to RFC 5280, the value of all these extensions has an + * ASN.1 syntax "SEQUENCE SIZE (1..MAX) OF" something, which means + * a SEQUENCE containing at least one element. + */ +var targetExtensionsMap = map[string]string{ + util.CertPolicyOID.String(): "CertificatePolicies", + util.PolicyMapOID.String(): "PolicyMappings", + util.SubjectAlternateNameOID.String(): "SubjectAlternativeNames", + util.IssuerAlternateNameOID.String(): "IssuerAlternativeNames", + util.SubjectDirAttrOID.String(): "SubjectDirectoryAttributes", + util.EkuSynOid.String(): "ExtendedKeyUsage", + util.CrlDistOID.String(): "CRLDistributionPoints", + util.AiaOID.String(): "AuthorityInformationAccess", + util.SubjectInfoAccessOID.String(): "SubjectInformationAccess", + util.FreshCRLOID.String(): "FreshestCRL", +} + +func (l *ExtCannotBeEmptySequence) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *ExtCannotBeEmptySequence) Execute(c *x509.Certificate) *lint.LintResult { + + SequenceOfSomething := []asn1.RawValue{} + + for extOid := range targetExtensionsMap { + if ext, found := c.ExtensionsMap[extOid]; found { + _, err := asn1.Unmarshal(ext.Value, &SequenceOfSomething) + if err != nil { + return &lint.LintResult{ + Status: lint.Fatal, + Details: fmt.Sprintf("Cannot parse the %s extension: %v", + targetExtensionsMap[extOid], err), + } + } + if len(SequenceOfSomething) == 0 { + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("The %s extension, if present, MUST contain at least 1 element", + targetExtensionsMap[extOid]), + } + } + } + } + + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_freshest_crl_marked_critical.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_freshest_crl_marked_critical.go index 5f198ff8b97..f63f3b99521 100644 --- a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_freshest_crl_marked_critical.go +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_ext_freshest_crl_marked_critical.go @@ -16,7 +16,6 @@ package rfc import ( "github.com/zmap/zcrypto/x509" - "github.com/zmap/zcrypto/x509/pkix" "github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/util" ) @@ -49,7 +48,7 @@ func (l *ExtFreshestCrlMarkedCritical) CheckApplies(cert *x509.Certificate) bool } func (l *ExtFreshestCrlMarkedCritical) Execute(cert *x509.Certificate) *lint.LintResult { - var fCRL *pkix.Extension = util.GetExtFromCert(cert, util.FreshCRLOID) + var fCRL = util.GetExtFromCert(cert, util.FreshCRLOID) if fCRL != nil && fCRL.Critical { return &lint.LintResult{Status: lint.Error} } else if fCRL != nil && !fCRL.Critical { diff --git a/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_subj_email_not_in_san.go b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_subj_email_not_in_san.go new file mode 100644 index 00000000000..7086eecfdcd --- /dev/null +++ b/vendor/github.com/zmap/zlint/v3/lints/rfc/lint_subj_email_not_in_san.go @@ -0,0 +1,59 @@ +/* + * ZLint Copyright 2024 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package rfc + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" + + "slices" +) + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_subj_email_not_in_san", + Description: "Certificates with email addresses MUST include them in the SAN extension", + Citation: "RFC 5280 Section 4.1.2.6", + Source: lint.RFC5280, + EffectiveDate: util.RFC2459Date, + }, + Lint: NewSubjEmailAddrNotInSAN, + }) +} + +type SubjEmailAddrNotInSAN struct{} + +func NewSubjEmailAddrNotInSAN() lint.LintInterface { + return &SubjEmailAddrNotInSAN{} +} + +func (l *SubjEmailAddrNotInSAN) CheckApplies(c *x509.Certificate) bool { + return len(c.Subject.EmailAddress) > 0 +} + +func (l *SubjEmailAddrNotInSAN) Execute(c *x509.Certificate) *lint.LintResult { + + for _, emailAddr := range c.Subject.EmailAddress { + if !slices.Contains(c.EmailAddresses, emailAddr) { + return &lint.LintResult{ + Status: lint.Error, + Details: "At least one email address in the Subject does not appear in the SAN", + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/vendor/github.com/zmap/zlint/v3/util/fqdn.go b/vendor/github.com/zmap/zlint/v3/util/fqdn.go index ff4859da34e..d06f65d6131 100644 --- a/vendor/github.com/zmap/zlint/v3/util/fqdn.go +++ b/vendor/github.com/zmap/zlint/v3/util/fqdn.go @@ -127,5 +127,5 @@ func IsLDHLabel(label string) bool { !nonLDHCharacterRegex.MatchString(label) && !strings.HasPrefix(label, "-") && !strings.HasSuffix(label, "-") && - !(HasReservedLabelPrefix(label) && !HasXNLabelPrefix(label)) + (!HasReservedLabelPrefix(label) || HasXNLabelPrefix(label)) } diff --git a/vendor/github.com/zmap/zlint/v3/util/gtld_map.go b/vendor/github.com/zmap/zlint/v3/util/gtld_map.go index 6d250314fad..90eab80ff41 100644 --- a/vendor/github.com/zmap/zlint/v3/util/gtld_map.go +++ b/vendor/github.com/zmap/zlint/v3/util/gtld_map.go @@ -656,7 +656,7 @@ var tldMap = map[string]GTLDPeriod{ "bentley": { GTLD: "bentley", DelegationDate: "2015-07-09", - RemovalDate: "", + RemovalDate: "2025-04-23", }, "berlin": { GTLD: "berlin", @@ -1896,7 +1896,7 @@ var tldMap = map[string]GTLDPeriod{ "dunlop": { GTLD: "dunlop", DelegationDate: "2016-06-10", - RemovalDate: "", + RemovalDate: "2025-10-21", }, "duns": { GTLD: "duns", @@ -2666,7 +2666,7 @@ var tldMap = map[string]GTLDPeriod{ "goo": { GTLD: "goo", DelegationDate: "2015-03-03", - RemovalDate: "", + RemovalDate: "2026-02-06", }, "goodhands": { GTLD: "goodhands", @@ -3596,7 +3596,7 @@ var tldMap = map[string]GTLDPeriod{ "lancaster": { GTLD: "lancaster", DelegationDate: "2015-07-15", - RemovalDate: "", + RemovalDate: "2025-04-29", }, "lancia": { GTLD: "lancia", @@ -4093,6 +4093,11 @@ var tldMap = map[string]GTLDPeriod{ DelegationDate: "2015-10-29", RemovalDate: "2018-05-26", }, + "merck": { + GTLD: "merck", + DelegationDate: "2026-04-17", + RemovalDate: "", + }, "merckmsd": { GTLD: "merckmsd", DelegationDate: "2017-07-10", @@ -5046,7 +5051,7 @@ var tldMap = map[string]GTLDPeriod{ "pramerica": { GTLD: "pramerica", DelegationDate: "2016-07-28", - RemovalDate: "", + RemovalDate: "2025-05-16", }, "praxi": { GTLD: "praxi", @@ -5226,7 +5231,7 @@ var tldMap = map[string]GTLDPeriod{ "redstone": { GTLD: "redstone", DelegationDate: "2015-03-28", - RemovalDate: "", + RemovalDate: "2025-08-26", }, "redumbrella": { GTLD: "redumbrella", @@ -6901,7 +6906,7 @@ var tldMap = map[string]GTLDPeriod{ "wolterskluwer": { GTLD: "wolterskluwer", DelegationDate: "2016-02-11", - RemovalDate: "", + RemovalDate: "2026-02-13", }, "woodside": { GTLD: "woodside", diff --git a/vendor/github.com/zmap/zlint/v3/util/oid.go b/vendor/github.com/zmap/zlint/v3/util/oid.go index 78909a8e25f..f4932893e0d 100644 --- a/vendor/github.com/zmap/zlint/v3/util/oid.go +++ b/vendor/github.com/zmap/zlint/v3/util/oid.go @@ -55,6 +55,7 @@ var ( SubjectKeyIdentityOID = asn1.ObjectIdentifier{2, 5, 29, 14} // Subject Key Identifier ReasonCodeOID = asn1.ObjectIdentifier{2, 5, 29, 21} // CRL Reason Code CRLNumberOID = asn1.ObjectIdentifier{2, 5, 29, 20} // CRL Number + IssuingDistOID = asn1.ObjectIdentifier{2, 5, 29, 28} // Issuing Distribution Point // Extended Key Usage OIDs PreCertificateSigningCertificateEKU = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 4} // CA/B Reserved Certificate Policy Identifiers @@ -89,6 +90,8 @@ var ( BusinessOID = asn1.ObjectIdentifier{2, 5, 4, 15} PostalCodeOID = asn1.ObjectIdentifier{2, 5, 4, 17} GivenNameOID = asn1.ObjectIdentifier{2, 5, 4, 42} + PseudonameOID = asn1.ObjectIdentifier{2, 5, 4, 65} + EmailAddressOID = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1} // SAN otherNames OidIdOnSmtpUtf8Mailbox = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 9} // Hash algorithms - see https://golang.org/src/crypto/x509/x509.go @@ -117,6 +120,13 @@ var ( IdEtsiQcsQctEsign = asn1.ObjectIdentifier{0, 4, 0, 1862, 1, 6, 1} IdEtsiQcsQctEseal = asn1.ObjectIdentifier{0, 4, 0, 1862, 1, 6, 2} IdEtsiQcsQctWeb = asn1.ObjectIdentifier{0, 4, 0, 1862, 1, 6, 3} + QCPnPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 0} + QCPlPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 1} + QCPnqscdPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 2} + QCPlqscdPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 3} + QEVCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 4} + QNCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 5} + QNCPwgenPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 6} ) const ( diff --git a/vendor/github.com/zmap/zlint/v3/util/qc_stmt.go b/vendor/github.com/zmap/zlint/v3/util/qc_stmt.go index b258053d7aa..e073ba2bec7 100644 --- a/vendor/github.com/zmap/zlint/v3/util/qc_stmt.go +++ b/vendor/github.com/zmap/zlint/v3/util/qc_stmt.go @@ -20,6 +20,7 @@ import ( "reflect" "github.com/zmap/zcrypto/encoding/asn1" + "github.com/zmap/zcrypto/x509" ) type anyContent struct { @@ -162,7 +163,7 @@ func ParseQcStatem(extVal []byte, sought asn1.ObjectIdentifier) EtsiQcStmtIf { if len(statem.Any.FullBytes) != 0 { return etsiBase{errorInfo: "internal error, default optional content len is not zero"} } - } else if 0 != len(rest) { + } else if len(rest) != 0 { return etsiBase{errorInfo: parseErrorString, isPresent: false} } @@ -252,3 +253,43 @@ func ParseQcStatem(extVal []byte, sought asn1.ObjectIdentifier) EtsiQcStmtIf { return etsiBase{errorInfo: "", isPresent: false} } + +/************************************************ + +https://www.etsi.org/deliver/etsi_en/319400_319499/31941102/02.06.01_60/en_31941102v020601p.pdf + +3.3 Abbreviations +For the purposes of the present document, the abbreviations given in ETSI EN 319 401 [1], ETSI EN 319 411-1 [2] and +the following apply: +QCP-l Policy for EU Qualified Certificate issued to a legal person +QCP-l-qscd Policy for EU Qualified Certificate issued to a legal person where the private key and the related +certificate reside on a QSCD +QCP-n Policy for EU Qualified Certificate issued to a natural person +QCP-n-qscd Policy for EU Qualified Certificate issued to a natural person where the private key and the related +certificate reside on a QSCD +QEVCP-w Policy for EU qualified website certificate issued to a legal person and linking the website to that +person based on the EVCG +NOTE: Previous versions of the present document used the abbreviation QCP-w. +QNCP-w Policy for EU qualified website certificate issued to a natural or a legal person and linking the +website to that person based on the BRG +QNCP-w-gen Policy for EU qualified website certificate issued to a natural or a legal person and linking the +website to that person, applicable for general purpose certificate for qualified website +authentication +QSCD Qualified electronic Signature/Seal Creation Device +************************************************/ + +func IsEtsiQcNaturalPerson(cert *x509.Certificate) bool { + for _, policyIds := range cert.PolicyIdentifiers { + if policyIds.Equal(QCPnPolicyOID) { + return true + } + if policyIds.Equal(QCPnqscdPolicyOID) { + return true + } + if policyIds.Equal(QEVCPwPolicyOID) || policyIds.Equal(QNCPwPolicyOID) || policyIds.Equal(QNCPwgenPolicyOID) { + // a natural person has one of these properties, whereas a legal person does not have any of them + return TypeInName(&cert.Subject, GivenNameOID) || TypeInName(&cert.Subject, SurnameOID) || TypeInName(&cert.Subject, PseudonameOID) + } + } + return false +} diff --git a/vendor/github.com/zmap/zlint/v3/util/smime_policies.go b/vendor/github.com/zmap/zlint/v3/util/smime_policies.go index f0f4eb3be49..54fafe74e60 100644 --- a/vendor/github.com/zmap/zlint/v3/util/smime_policies.go +++ b/vendor/github.com/zmap/zlint/v3/util/smime_policies.go @@ -15,6 +15,7 @@ package util */ import ( + "github.com/zmap/zcrypto/encoding/asn1" "github.com/zmap/zcrypto/x509" ) @@ -91,3 +92,27 @@ func IsStrictSMIMECertificate(c *x509.Certificate) bool { return false } + +func ContainsExactlyOneSMIMEPolicy(policies []asn1.ObjectIdentifier) bool { + found := 0 + smimePolicies := map[string]bool{ + SMIMEBRMailboxValidatedLegacyOID.String(): true, + SMIMEBRMailboxValidatedMultipurposeOID.String(): true, + SMIMEBRMailboxValidatedStrictOID.String(): true, + SMIMEBROrganizationValidatedLegacyOID.String(): true, + SMIMEBROrganizationValidatedMultipurposeOID.String(): true, + SMIMEBROrganizationValidatedStrictOID.String(): true, + SMIMEBRSponsorValidatedLegacyOID.String(): true, + SMIMEBRSponsorValidatedMultipurposeOID.String(): true, + SMIMEBRSponsorValidatedStrictOID.String(): true, + SMIMEBRIndividualValidatedLegacyOID.String(): true, + SMIMEBRIndividualValidatedMultipurposeOID.String(): true, + SMIMEBRIndividualValidatedStrictOID.String(): true, + } + for _, oid := range policies { + if _, present := smimePolicies[oid.String()]; present { + found++ + } + } + return found == 1 +} diff --git a/vendor/github.com/zmap/zlint/v3/util/time.go b/vendor/github.com/zmap/zlint/v3/util/time.go index 1709402d001..f0a9fea5123 100644 --- a/vendor/github.com/zmap/zlint/v3/util/time.go +++ b/vendor/github.com/zmap/zlint/v3/util/time.go @@ -1,5 +1,5 @@ /* - * ZLint Copyright 2024 Regents of the University of Michigan + * ZLint Copyright 2025 Regents of the University of Michigan * * 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 @@ -15,6 +15,7 @@ package util import ( + "math" "time" "github.com/zmap/zcrypto/encoding/asn1" @@ -29,6 +30,7 @@ var ( ZeroDate = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC) RFC1035Date = time.Date(1987, time.January, 1, 0, 0, 0, 0, time.UTC) RFC2459Date = time.Date(1999, time.January, 1, 0, 0, 0, 0, time.UTC) + RFC3161Date = time.Date(2001, time.August, 1, 0, 0, 0, 0, time.UTC) RFC3279Date = time.Date(2002, time.April, 1, 0, 0, 0, 0, time.UTC) RFC3280Date = time.Date(2002, time.April, 1, 0, 0, 0, 0, time.UTC) RFC3490Date = time.Date(2003, time.March, 1, 0, 0, 0, 0, time.UTC) @@ -60,7 +62,12 @@ var ( SubCert39Month = time.Date(2016, time.July, 2, 0, 0, 0, 0, time.UTC) SubCert825Days = time.Date(2018, time.March, 2, 0, 0, 0, 0, time.UTC) CABV148Date = time.Date(2017, time.June, 8, 0, 0, 0, 0, time.UTC) + EtsiEn319_412_2_V2_2_1_Date = time.Date(2020, time.July, 1, 0, 0, 0, 0, time.UTC) + EtsiEn319_412_4_V1_3_0_Date = time.Date(2023, time.June, 1, 0, 0, 0, 0, time.UTC) EtsiEn319_412_5_V2_2_1_Date = time.Date(2017, time.November, 1, 0, 0, 0, 0, time.UTC) + EtsiEn319_412_5_V2_4_1_Date = time.Date(2023, time.September, 1, 0, 0, 0, 0, time.UTC) + EtsiEn319_412_5_V2_6_0_Date = time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC) + EtsiEn319_411_2_V2_5_0_Date = time.Date(2023, time.July, 1, 0, 0, 0, 0, time.UTC) OnionOnlyEVDate = time.Date(2015, time.May, 1, 0, 0, 0, 0, time.UTC) CABV201Date = time.Date(2017, time.July, 28, 0, 0, 0, 0, time.UTC) AppleCTPolicyDate = time.Date(2018, time.October, 15, 0, 0, 0, 0, time.UTC) @@ -68,6 +75,9 @@ var ( MozillaPolicy24Date = time.Date(2017, time.February, 28, 0, 0, 0, 0, time.UTC) MozillaPolicy241Date = time.Date(2017, time.March, 31, 0, 0, 0, 0, time.UTC) MozillaPolicy27Date = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC) + MozillaPolicy30Date = time.Date(2025, time.March, 15, 0, 0, 0, 0, time.UTC) + ChromePolicy18Date = time.Date(2026, time.February, 5, 0, 0, 0, 0, time.UTC) + ChromePolicyClientAuthDisallowedDate = time.Date(2027, time.March, 15, 0, 0, 0, 0, time.UTC) CABFBRs_1_6_2_UnderscorePermissibilitySunsetDate = time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC) CABFBRs_1_6_2_Date = time.Date(2018, time.December, 10, 0, 0, 0, 0, time.UTC) CABFBRs_1_2_1_Date = time.Date(2015, time.January, 16, 0, 0, 0, 0, time.UTC) @@ -90,19 +100,34 @@ var ( SC16EffectiveDate = time.Date(2019, time.April, 16, 0, 0, 0, 0, time.UTC) SC17EffectiveDate = time.Date(2019, time.June, 21, 0, 0, 0, 0, time.UTC) CABF_SMIME_BRs_1_0_0_Date = time.Date(2023, time.September, 1, 0, 0, 0, 0, time.UTC) + // With this version, PQC algorithms have been introduced + CABF_SMIME_BRs_1_0_11_Date = time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC) + // Date of deprecation of S/MIME legacy policies from Ballot SMC08 + SMC08EffectiveDate = time.Date(2025, time.July, 15, 0, 0, 0, 0, time.UTC) // Enforcement date of CRL reason codes from Ballot SC 061 CABFBRs_1_8_7_Date = time.Date(2023, time.July, 15, 0, 0, 0, 0, time.UTC) // Updates to the CABF BRs and EVGLs from Ballot SC 062 https://cabforum.org/2023/03/17/ballot-sc62v2-certificate-profiles-update/ SC62EffectiveDate = time.Date(2023, time.September, 15, 0, 0, 0, 0, time.UTC) + // Updates to the CABF BRs from Ballot SC 063 https://cabforum.org/2023/07/14/ballot-sc063v4-make-ocsp-optional-require-crls-and-incentivize-automation/ + SC63EffectiveDate = time.Date(2024, time.March, 15, 0, 0, 0, 0, time.UTC) // Date when section 9.2.8 of CABF EVG became effective - CABFEV_Sec9_2_8_Date = time.Date(2020, time.January, 31, 0, 0, 0, 0, time.UTC) - CABF_CS_BRs_1_2_Date = time.Date(2019, time.August, 13, 0, 0, 0, 0, time.UTC) + CABFEV_Sec9_2_8_Date = time.Date(2020, time.January, 31, 0, 0, 0, 0, time.UTC) + CABF_CS_BRs_1_2_Date = time.Date(2019, time.August, 13, 0, 0, 0, 0, time.UTC) + CABF_CS_CSC_31_Date = time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC) + CABF_SC081_FIRST_MILESTONE = time.Date(2026, time.March, 15, 0, 0, 0, 0, time.UTC) + CABF_SC081_SECOND_MILESTONE = time.Date(2027, time.March, 15, 0, 0, 0, 0, time.UTC) + CABF_SC081_THIRD_MILESTONE = time.Date(2029, time.March, 15, 0, 0, 0, 0, time.UTC) + CABF_SC086_EffectiveDate = time.Date(2026, time.March, 15, 0, 0, 0, 0, time.UTC) ) var ( CABFEV_9_8_2 = CABV170Date ) +var ( + DAY_LENGTH = 86400 * time.Second.Seconds() +) + func FindTimeType(firstDate, secondDate asn1.RawValue) (int, int) { return firstDate.Tag, secondDate.Tag } @@ -162,3 +187,18 @@ func BeforeOrOn(left, right time.Time) bool { func OnOrAfter(left, right time.Time) bool { return !left.Before(right) } + +func CertificateValidityInSeconds(cert *x509.Certificate) float64 { + return cert.NotAfter.Add(1 * time.Second).Sub(cert.NotBefore).Seconds() +} + +func CertificateValidityInDays(cert *x509.Certificate) float64 { + return math.Ceil(CertificateValidityInSeconds(cert) / DAY_LENGTH) +} + +// GreaterThan returns true if the validity of this cert in days is greater than +// this maxDaysAllowed, false otherwise +func GreaterThan(cert *x509.Certificate, maxDaysAllowed float64) bool { + maxValidity := maxDaysAllowed * DAY_LENGTH + return CertificateValidityInSeconds(cert) > maxValidity +} diff --git a/vendor/github.com/zmap/zlint/v3/zlint.go b/vendor/github.com/zmap/zlint/v3/zlint.go index 07cd33b2b54..6d1738549f6 100644 --- a/vendor/github.com/zmap/zlint/v3/zlint.go +++ b/vendor/github.com/zmap/zlint/v3/zlint.go @@ -26,6 +26,7 @@ import ( _ "github.com/zmap/zlint/v3/lints/cabf_cs_br" _ "github.com/zmap/zlint/v3/lints/cabf_ev" _ "github.com/zmap/zlint/v3/lints/cabf_smime_br" + _ "github.com/zmap/zlint/v3/lints/chrome" _ "github.com/zmap/zlint/v3/lints/community" _ "github.com/zmap/zlint/v3/lints/etsi" _ "github.com/zmap/zlint/v3/lints/mozilla" diff --git a/vendor/go.uber.org/atomic/.codecov.yml b/vendor/go.uber.org/atomic/.codecov.yml new file mode 100644 index 00000000000..571116cc39c --- /dev/null +++ b/vendor/go.uber.org/atomic/.codecov.yml @@ -0,0 +1,19 @@ +coverage: + range: 80..100 + round: down + precision: 2 + + status: + project: # measuring the overall project coverage + default: # context, you can create multiple ones with custom titles + enabled: yes # must be yes|true to enable this status + target: 100 # specify the target coverage for each commit status + # option: "auto" (must increase from parent commit or pull request base) + # option: "X%" a static target percentage to hit + if_not_found: success # if parent is not found report status as success, error, or failure + if_ci_failed: error # if ci fails report status as success, error, or failure + +# Also update COVER_IGNORE_PKGS in the Makefile. +ignore: + - /internal/gen-atomicint/ + - /internal/gen-valuewrapper/ diff --git a/vendor/go.uber.org/atomic/.gitignore b/vendor/go.uber.org/atomic/.gitignore new file mode 100644 index 00000000000..2e337a0ed52 --- /dev/null +++ b/vendor/go.uber.org/atomic/.gitignore @@ -0,0 +1,15 @@ +/bin +.DS_Store +/vendor +cover.html +cover.out +lint.log + +# Binaries +*.test + +# Profiling output +*.prof + +# Output of fossa analyzer +/fossa diff --git a/vendor/go.uber.org/atomic/CHANGELOG.md b/vendor/go.uber.org/atomic/CHANGELOG.md new file mode 100644 index 00000000000..6f87f33fa95 --- /dev/null +++ b/vendor/go.uber.org/atomic/CHANGELOG.md @@ -0,0 +1,127 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.11.0] - 2023-05-02 +### Fixed +- Fix initialization of `Value` wrappers. + +### Added +- Add `String` method to `atomic.Pointer[T]` type allowing users to safely print +underlying values of pointers. + +[1.11.0]: https://github.com/uber-go/atomic/compare/v1.10.0...v1.11.0 + +## [1.10.0] - 2022-08-11 +### Added +- Add `atomic.Float32` type for atomic operations on `float32`. +- Add `CompareAndSwap` and `Swap` methods to `atomic.String`, `atomic.Error`, + and `atomic.Value`. +- Add generic `atomic.Pointer[T]` type for atomic operations on pointers of any + type. This is present only for Go 1.18 or higher, and is a drop-in for + replacement for the standard library's `sync/atomic.Pointer` type. + +### Changed +- Deprecate `CAS` methods on all types in favor of corresponding + `CompareAndSwap` methods. + +Thanks to @eNV25 and @icpd for their contributions to this release. + +[1.10.0]: https://github.com/uber-go/atomic/compare/v1.9.0...v1.10.0 + +## [1.9.0] - 2021-07-15 +### Added +- Add `Float64.Swap` to match int atomic operations. +- Add `atomic.Time` type for atomic operations on `time.Time` values. + +[1.9.0]: https://github.com/uber-go/atomic/compare/v1.8.0...v1.9.0 + +## [1.8.0] - 2021-06-09 +### Added +- Add `atomic.Uintptr` type for atomic operations on `uintptr` values. +- Add `atomic.UnsafePointer` type for atomic operations on `unsafe.Pointer` values. + +[1.8.0]: https://github.com/uber-go/atomic/compare/v1.7.0...v1.8.0 + +## [1.7.0] - 2020-09-14 +### Added +- Support JSON serialization and deserialization of primitive atomic types. +- Support Text marshalling and unmarshalling for string atomics. + +### Changed +- Disallow incorrect comparison of atomic values in a non-atomic way. + +### Removed +- Remove dependency on `golang.org/x/{lint, tools}`. + +[1.7.0]: https://github.com/uber-go/atomic/compare/v1.6.0...v1.7.0 + +## [1.6.0] - 2020-02-24 +### Changed +- Drop library dependency on `golang.org/x/{lint, tools}`. + +[1.6.0]: https://github.com/uber-go/atomic/compare/v1.5.1...v1.6.0 + +## [1.5.1] - 2019-11-19 +- Fix bug where `Bool.CAS` and `Bool.Toggle` do work correctly together + causing `CAS` to fail even though the old value matches. + +[1.5.1]: https://github.com/uber-go/atomic/compare/v1.5.0...v1.5.1 + +## [1.5.0] - 2019-10-29 +### Changed +- With Go modules, only the `go.uber.org/atomic` import path is supported now. + If you need to use the old import path, please add a `replace` directive to + your `go.mod`. + +[1.5.0]: https://github.com/uber-go/atomic/compare/v1.4.0...v1.5.0 + +## [1.4.0] - 2019-05-01 +### Added + - Add `atomic.Error` type for atomic operations on `error` values. + +[1.4.0]: https://github.com/uber-go/atomic/compare/v1.3.2...v1.4.0 + +## [1.3.2] - 2018-05-02 +### Added +- Add `atomic.Duration` type for atomic operations on `time.Duration` values. + +[1.3.2]: https://github.com/uber-go/atomic/compare/v1.3.1...v1.3.2 + +## [1.3.1] - 2017-11-14 +### Fixed +- Revert optimization for `atomic.String.Store("")` which caused data races. + +[1.3.1]: https://github.com/uber-go/atomic/compare/v1.3.0...v1.3.1 + +## [1.3.0] - 2017-11-13 +### Added +- Add `atomic.Bool.CAS` for compare-and-swap semantics on bools. + +### Changed +- Optimize `atomic.String.Store("")` by avoiding an allocation. + +[1.3.0]: https://github.com/uber-go/atomic/compare/v1.2.0...v1.3.0 + +## [1.2.0] - 2017-04-12 +### Added +- Shadow `atomic.Value` from `sync/atomic`. + +[1.2.0]: https://github.com/uber-go/atomic/compare/v1.1.0...v1.2.0 + +## [1.1.0] - 2017-03-10 +### Added +- Add atomic `Float64` type. + +### Changed +- Support new `go.uber.org/atomic` import path. + +[1.1.0]: https://github.com/uber-go/atomic/compare/v1.0.0...v1.1.0 + +## [1.0.0] - 2016-07-18 + +- Initial release. + +[1.0.0]: https://github.com/uber-go/atomic/releases/tag/v1.0.0 diff --git a/vendor/github.com/dgryski/go-rendezvous/LICENSE b/vendor/go.uber.org/atomic/LICENSE.txt similarity index 92% rename from vendor/github.com/dgryski/go-rendezvous/LICENSE rename to vendor/go.uber.org/atomic/LICENSE.txt index 22080f736a4..8765c9fbc61 100644 --- a/vendor/github.com/dgryski/go-rendezvous/LICENSE +++ b/vendor/go.uber.org/atomic/LICENSE.txt @@ -1,6 +1,4 @@ -The MIT License (MIT) - -Copyright (c) 2017-2020 Damian Gryski +Copyright (c) 2016 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/go.uber.org/atomic/Makefile b/vendor/go.uber.org/atomic/Makefile new file mode 100644 index 00000000000..46c945b32be --- /dev/null +++ b/vendor/go.uber.org/atomic/Makefile @@ -0,0 +1,79 @@ +# Directory to place `go install`ed binaries into. +export GOBIN ?= $(shell pwd)/bin + +GOLINT = $(GOBIN)/golint +GEN_ATOMICINT = $(GOBIN)/gen-atomicint +GEN_ATOMICWRAPPER = $(GOBIN)/gen-atomicwrapper +STATICCHECK = $(GOBIN)/staticcheck + +GO_FILES ?= $(shell find . '(' -path .git -o -path vendor ')' -prune -o -name '*.go' -print) + +# Also update ignore section in .codecov.yml. +COVER_IGNORE_PKGS = \ + go.uber.org/atomic/internal/gen-atomicint \ + go.uber.org/atomic/internal/gen-atomicwrapper + +.PHONY: build +build: + go build ./... + +.PHONY: test +test: + go test -race ./... + +.PHONY: gofmt +gofmt: + $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) + gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true + @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" && cat $(FMT_LOG) && false) + +$(GOLINT): + cd tools && go install golang.org/x/lint/golint + +$(STATICCHECK): + cd tools && go install honnef.co/go/tools/cmd/staticcheck + +$(GEN_ATOMICWRAPPER): $(wildcard ./internal/gen-atomicwrapper/*) + go build -o $@ ./internal/gen-atomicwrapper + +$(GEN_ATOMICINT): $(wildcard ./internal/gen-atomicint/*) + go build -o $@ ./internal/gen-atomicint + +.PHONY: golint +golint: $(GOLINT) + $(GOLINT) ./... + +.PHONY: staticcheck +staticcheck: $(STATICCHECK) + $(STATICCHECK) ./... + +.PHONY: lint +lint: gofmt golint staticcheck generatenodirty + +# comma separated list of packages to consider for code coverage. +COVER_PKG = $(shell \ + go list -find ./... | \ + grep -v $(foreach pkg,$(COVER_IGNORE_PKGS),-e "^$(pkg)$$") | \ + paste -sd, -) + +.PHONY: cover +cover: + go test -coverprofile=cover.out -coverpkg $(COVER_PKG) -v ./... + go tool cover -html=cover.out -o cover.html + +.PHONY: generate +generate: $(GEN_ATOMICINT) $(GEN_ATOMICWRAPPER) + go generate ./... + +.PHONY: generatenodirty +generatenodirty: + @[ -z "$$(git status --porcelain)" ] || ( \ + echo "Working tree is dirty. Commit your changes first."; \ + git status; \ + exit 1 ) + @make generate + @status=$$(git status --porcelain); \ + [ -z "$$status" ] || ( \ + echo "Working tree is dirty after `make generate`:"; \ + echo "$$status"; \ + echo "Please ensure that the generated code is up-to-date." ) diff --git a/vendor/go.uber.org/atomic/README.md b/vendor/go.uber.org/atomic/README.md new file mode 100644 index 00000000000..96b47a1f12d --- /dev/null +++ b/vendor/go.uber.org/atomic/README.md @@ -0,0 +1,63 @@ +# atomic [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Go Report Card][reportcard-img]][reportcard] + +Simple wrappers for primitive types to enforce atomic access. + +## Installation + +```shell +$ go get -u go.uber.org/atomic@v1 +``` + +### Legacy Import Path + +As of v1.5.0, the import path `go.uber.org/atomic` is the only supported way +of using this package. If you are using Go modules, this package will fail to +compile with the legacy import path path `github.com/uber-go/atomic`. + +We recommend migrating your code to the new import path but if you're unable +to do so, or if your dependencies are still using the old import path, you +will have to add a `replace` directive to your `go.mod` file downgrading the +legacy import path to an older version. + +``` +replace github.com/uber-go/atomic => github.com/uber-go/atomic v1.4.0 +``` + +You can do so automatically by running the following command. + +```shell +$ go mod edit -replace github.com/uber-go/atomic=github.com/uber-go/atomic@v1.4.0 +``` + +## Usage + +The standard library's `sync/atomic` is powerful, but it's easy to forget which +variables must be accessed atomically. `go.uber.org/atomic` preserves all the +functionality of the standard library, but wraps the primitive types to +provide a safer, more convenient API. + +```go +var atom atomic.Uint32 +atom.Store(42) +atom.Sub(2) +atom.CAS(40, 11) +``` + +See the [documentation][doc] for a complete API specification. + +## Development Status + +Stable. + +--- + +Released under the [MIT License](LICENSE.txt). + +[doc-img]: https://godoc.org/github.com/uber-go/atomic?status.svg +[doc]: https://godoc.org/go.uber.org/atomic +[ci-img]: https://github.com/uber-go/atomic/actions/workflows/go.yml/badge.svg +[ci]: https://github.com/uber-go/atomic/actions/workflows/go.yml +[cov-img]: https://codecov.io/gh/uber-go/atomic/branch/master/graph/badge.svg +[cov]: https://codecov.io/gh/uber-go/atomic +[reportcard-img]: https://goreportcard.com/badge/go.uber.org/atomic +[reportcard]: https://goreportcard.com/report/go.uber.org/atomic diff --git a/vendor/go.uber.org/atomic/bool.go b/vendor/go.uber.org/atomic/bool.go new file mode 100644 index 00000000000..f0a2ddd148c --- /dev/null +++ b/vendor/go.uber.org/atomic/bool.go @@ -0,0 +1,88 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" +) + +// Bool is an atomic type-safe wrapper for bool values. +type Bool struct { + _ nocmp // disallow non-atomic comparison + + v Uint32 +} + +var _zeroBool bool + +// NewBool creates a new Bool. +func NewBool(val bool) *Bool { + x := &Bool{} + if val != _zeroBool { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped bool. +func (x *Bool) Load() bool { + return truthy(x.v.Load()) +} + +// Store atomically stores the passed bool. +func (x *Bool) Store(val bool) { + x.v.Store(boolToInt(val)) +} + +// CAS is an atomic compare-and-swap for bool values. +// +// Deprecated: Use CompareAndSwap. +func (x *Bool) CAS(old, new bool) (swapped bool) { + return x.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for bool values. +func (x *Bool) CompareAndSwap(old, new bool) (swapped bool) { + return x.v.CompareAndSwap(boolToInt(old), boolToInt(new)) +} + +// Swap atomically stores the given bool and returns the old +// value. +func (x *Bool) Swap(val bool) (old bool) { + return truthy(x.v.Swap(boolToInt(val))) +} + +// MarshalJSON encodes the wrapped bool into JSON. +func (x *Bool) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a bool from JSON. +func (x *Bool) UnmarshalJSON(b []byte) error { + var v bool + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/bool_ext.go b/vendor/go.uber.org/atomic/bool_ext.go new file mode 100644 index 00000000000..a2e60e98739 --- /dev/null +++ b/vendor/go.uber.org/atomic/bool_ext.go @@ -0,0 +1,53 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "strconv" +) + +//go:generate bin/gen-atomicwrapper -name=Bool -type=bool -wrapped=Uint32 -pack=boolToInt -unpack=truthy -cas -swap -json -file=bool.go + +func truthy(n uint32) bool { + return n == 1 +} + +func boolToInt(b bool) uint32 { + if b { + return 1 + } + return 0 +} + +// Toggle atomically negates the Boolean and returns the previous value. +func (b *Bool) Toggle() (old bool) { + for { + old := b.Load() + if b.CAS(old, !old) { + return old + } + } +} + +// String encodes the wrapped value as a string. +func (b *Bool) String() string { + return strconv.FormatBool(b.Load()) +} diff --git a/vendor/go.uber.org/atomic/doc.go b/vendor/go.uber.org/atomic/doc.go new file mode 100644 index 00000000000..ae7390ee688 --- /dev/null +++ b/vendor/go.uber.org/atomic/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package atomic provides simple wrappers around numerics to enforce atomic +// access. +package atomic diff --git a/vendor/go.uber.org/atomic/duration.go b/vendor/go.uber.org/atomic/duration.go new file mode 100644 index 00000000000..7c23868fc87 --- /dev/null +++ b/vendor/go.uber.org/atomic/duration.go @@ -0,0 +1,89 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "time" +) + +// Duration is an atomic type-safe wrapper for time.Duration values. +type Duration struct { + _ nocmp // disallow non-atomic comparison + + v Int64 +} + +var _zeroDuration time.Duration + +// NewDuration creates a new Duration. +func NewDuration(val time.Duration) *Duration { + x := &Duration{} + if val != _zeroDuration { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped time.Duration. +func (x *Duration) Load() time.Duration { + return time.Duration(x.v.Load()) +} + +// Store atomically stores the passed time.Duration. +func (x *Duration) Store(val time.Duration) { + x.v.Store(int64(val)) +} + +// CAS is an atomic compare-and-swap for time.Duration values. +// +// Deprecated: Use CompareAndSwap. +func (x *Duration) CAS(old, new time.Duration) (swapped bool) { + return x.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for time.Duration values. +func (x *Duration) CompareAndSwap(old, new time.Duration) (swapped bool) { + return x.v.CompareAndSwap(int64(old), int64(new)) +} + +// Swap atomically stores the given time.Duration and returns the old +// value. +func (x *Duration) Swap(val time.Duration) (old time.Duration) { + return time.Duration(x.v.Swap(int64(val))) +} + +// MarshalJSON encodes the wrapped time.Duration into JSON. +func (x *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a time.Duration from JSON. +func (x *Duration) UnmarshalJSON(b []byte) error { + var v time.Duration + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/duration_ext.go b/vendor/go.uber.org/atomic/duration_ext.go new file mode 100644 index 00000000000..4c18b0a9ed4 --- /dev/null +++ b/vendor/go.uber.org/atomic/duration_ext.go @@ -0,0 +1,40 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "time" + +//go:generate bin/gen-atomicwrapper -name=Duration -type=time.Duration -wrapped=Int64 -pack=int64 -unpack=time.Duration -cas -swap -json -imports time -file=duration.go + +// Add atomically adds to the wrapped time.Duration and returns the new value. +func (d *Duration) Add(delta time.Duration) time.Duration { + return time.Duration(d.v.Add(int64(delta))) +} + +// Sub atomically subtracts from the wrapped time.Duration and returns the new value. +func (d *Duration) Sub(delta time.Duration) time.Duration { + return time.Duration(d.v.Sub(int64(delta))) +} + +// String encodes the wrapped value as a string. +func (d *Duration) String() string { + return d.Load().String() +} diff --git a/vendor/go.uber.org/atomic/error.go b/vendor/go.uber.org/atomic/error.go new file mode 100644 index 00000000000..b7e3f1291a3 --- /dev/null +++ b/vendor/go.uber.org/atomic/error.go @@ -0,0 +1,72 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// Error is an atomic type-safe wrapper for error values. +type Error struct { + _ nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroError error + +// NewError creates a new Error. +func NewError(val error) *Error { + x := &Error{} + if val != _zeroError { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped error. +func (x *Error) Load() error { + return unpackError(x.v.Load()) +} + +// Store atomically stores the passed error. +func (x *Error) Store(val error) { + x.v.Store(packError(val)) +} + +// CompareAndSwap is an atomic compare-and-swap for error values. +func (x *Error) CompareAndSwap(old, new error) (swapped bool) { + if x.v.CompareAndSwap(packError(old), packError(new)) { + return true + } + + if old == _zeroError { + // If the old value is the empty value, then it's possible the + // underlying Value hasn't been set and is nil, so retry with nil. + return x.v.CompareAndSwap(nil, packError(new)) + } + + return false +} + +// Swap atomically stores the given error and returns the old +// value. +func (x *Error) Swap(val error) (old error) { + return unpackError(x.v.Swap(packError(val))) +} diff --git a/vendor/go.uber.org/atomic/error_ext.go b/vendor/go.uber.org/atomic/error_ext.go new file mode 100644 index 00000000000..d31fb633bb6 --- /dev/null +++ b/vendor/go.uber.org/atomic/error_ext.go @@ -0,0 +1,39 @@ +// Copyright (c) 2020-2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// atomic.Value panics on nil inputs, or if the underlying type changes. +// Stabilize by always storing a custom struct that we control. + +//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -compareandswap -swap -file=error.go + +type packedError struct{ Value error } + +func packError(v error) interface{} { + return packedError{v} +} + +func unpackError(v interface{}) error { + if err, ok := v.(packedError); ok { + return err.Value + } + return nil +} diff --git a/vendor/go.uber.org/atomic/float32.go b/vendor/go.uber.org/atomic/float32.go new file mode 100644 index 00000000000..62c36334fd5 --- /dev/null +++ b/vendor/go.uber.org/atomic/float32.go @@ -0,0 +1,77 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "math" +) + +// Float32 is an atomic type-safe wrapper for float32 values. +type Float32 struct { + _ nocmp // disallow non-atomic comparison + + v Uint32 +} + +var _zeroFloat32 float32 + +// NewFloat32 creates a new Float32. +func NewFloat32(val float32) *Float32 { + x := &Float32{} + if val != _zeroFloat32 { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped float32. +func (x *Float32) Load() float32 { + return math.Float32frombits(x.v.Load()) +} + +// Store atomically stores the passed float32. +func (x *Float32) Store(val float32) { + x.v.Store(math.Float32bits(val)) +} + +// Swap atomically stores the given float32 and returns the old +// value. +func (x *Float32) Swap(val float32) (old float32) { + return math.Float32frombits(x.v.Swap(math.Float32bits(val))) +} + +// MarshalJSON encodes the wrapped float32 into JSON. +func (x *Float32) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a float32 from JSON. +func (x *Float32) UnmarshalJSON(b []byte) error { + var v float32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/float32_ext.go b/vendor/go.uber.org/atomic/float32_ext.go new file mode 100644 index 00000000000..b0cd8d9c820 --- /dev/null +++ b/vendor/go.uber.org/atomic/float32_ext.go @@ -0,0 +1,76 @@ +// Copyright (c) 2020-2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "math" + "strconv" +) + +//go:generate bin/gen-atomicwrapper -name=Float32 -type=float32 -wrapped=Uint32 -pack=math.Float32bits -unpack=math.Float32frombits -swap -json -imports math -file=float32.go + +// Add atomically adds to the wrapped float32 and returns the new value. +func (f *Float32) Add(delta float32) float32 { + for { + old := f.Load() + new := old + delta + if f.CAS(old, new) { + return new + } + } +} + +// Sub atomically subtracts from the wrapped float32 and returns the new value. +func (f *Float32) Sub(delta float32) float32 { + return f.Add(-delta) +} + +// CAS is an atomic compare-and-swap for float32 values. +// +// Deprecated: Use CompareAndSwap +func (f *Float32) CAS(old, new float32) (swapped bool) { + return f.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for float32 values. +// +// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators +// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN. +// This avoids typical CompareAndSwap loops from blocking forever, e.g., +// +// for { +// old := atom.Load() +// new = f(old) +// if atom.CompareAndSwap(old, new) { +// break +// } +// } +// +// If CompareAndSwap did not match NaN to match, then the above would loop forever. +func (f *Float32) CompareAndSwap(old, new float32) (swapped bool) { + return f.v.CompareAndSwap(math.Float32bits(old), math.Float32bits(new)) +} + +// String encodes the wrapped value as a string. +func (f *Float32) String() string { + // 'g' is the behavior for floats with %v. + return strconv.FormatFloat(float64(f.Load()), 'g', -1, 32) +} diff --git a/vendor/go.uber.org/atomic/float64.go b/vendor/go.uber.org/atomic/float64.go new file mode 100644 index 00000000000..5bc11caabe2 --- /dev/null +++ b/vendor/go.uber.org/atomic/float64.go @@ -0,0 +1,77 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "math" +) + +// Float64 is an atomic type-safe wrapper for float64 values. +type Float64 struct { + _ nocmp // disallow non-atomic comparison + + v Uint64 +} + +var _zeroFloat64 float64 + +// NewFloat64 creates a new Float64. +func NewFloat64(val float64) *Float64 { + x := &Float64{} + if val != _zeroFloat64 { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped float64. +func (x *Float64) Load() float64 { + return math.Float64frombits(x.v.Load()) +} + +// Store atomically stores the passed float64. +func (x *Float64) Store(val float64) { + x.v.Store(math.Float64bits(val)) +} + +// Swap atomically stores the given float64 and returns the old +// value. +func (x *Float64) Swap(val float64) (old float64) { + return math.Float64frombits(x.v.Swap(math.Float64bits(val))) +} + +// MarshalJSON encodes the wrapped float64 into JSON. +func (x *Float64) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a float64 from JSON. +func (x *Float64) UnmarshalJSON(b []byte) error { + var v float64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/float64_ext.go b/vendor/go.uber.org/atomic/float64_ext.go new file mode 100644 index 00000000000..48c52b0abf6 --- /dev/null +++ b/vendor/go.uber.org/atomic/float64_ext.go @@ -0,0 +1,76 @@ +// Copyright (c) 2020-2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "math" + "strconv" +) + +//go:generate bin/gen-atomicwrapper -name=Float64 -type=float64 -wrapped=Uint64 -pack=math.Float64bits -unpack=math.Float64frombits -swap -json -imports math -file=float64.go + +// Add atomically adds to the wrapped float64 and returns the new value. +func (f *Float64) Add(delta float64) float64 { + for { + old := f.Load() + new := old + delta + if f.CAS(old, new) { + return new + } + } +} + +// Sub atomically subtracts from the wrapped float64 and returns the new value. +func (f *Float64) Sub(delta float64) float64 { + return f.Add(-delta) +} + +// CAS is an atomic compare-and-swap for float64 values. +// +// Deprecated: Use CompareAndSwap +func (f *Float64) CAS(old, new float64) (swapped bool) { + return f.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for float64 values. +// +// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators +// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN. +// This avoids typical CompareAndSwap loops from blocking forever, e.g., +// +// for { +// old := atom.Load() +// new = f(old) +// if atom.CompareAndSwap(old, new) { +// break +// } +// } +// +// If CompareAndSwap did not match NaN to match, then the above would loop forever. +func (f *Float64) CompareAndSwap(old, new float64) (swapped bool) { + return f.v.CompareAndSwap(math.Float64bits(old), math.Float64bits(new)) +} + +// String encodes the wrapped value as a string. +func (f *Float64) String() string { + // 'g' is the behavior for floats with %v. + return strconv.FormatFloat(f.Load(), 'g', -1, 64) +} diff --git a/vendor/go.uber.org/atomic/gen.go b/vendor/go.uber.org/atomic/gen.go new file mode 100644 index 00000000000..1e9ef4f879c --- /dev/null +++ b/vendor/go.uber.org/atomic/gen.go @@ -0,0 +1,27 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +//go:generate bin/gen-atomicint -name=Int32 -wrapped=int32 -file=int32.go +//go:generate bin/gen-atomicint -name=Int64 -wrapped=int64 -file=int64.go +//go:generate bin/gen-atomicint -name=Uint32 -wrapped=uint32 -unsigned -file=uint32.go +//go:generate bin/gen-atomicint -name=Uint64 -wrapped=uint64 -unsigned -file=uint64.go +//go:generate bin/gen-atomicint -name=Uintptr -wrapped=uintptr -unsigned -file=uintptr.go diff --git a/vendor/go.uber.org/atomic/int32.go b/vendor/go.uber.org/atomic/int32.go new file mode 100644 index 00000000000..5320eac10f1 --- /dev/null +++ b/vendor/go.uber.org/atomic/int32.go @@ -0,0 +1,109 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Int32 is an atomic wrapper around int32. +type Int32 struct { + _ nocmp // disallow non-atomic comparison + + v int32 +} + +// NewInt32 creates a new Int32. +func NewInt32(val int32) *Int32 { + return &Int32{v: val} +} + +// Load atomically loads the wrapped value. +func (i *Int32) Load() int32 { + return atomic.LoadInt32(&i.v) +} + +// Add atomically adds to the wrapped int32 and returns the new value. +func (i *Int32) Add(delta int32) int32 { + return atomic.AddInt32(&i.v, delta) +} + +// Sub atomically subtracts from the wrapped int32 and returns the new value. +func (i *Int32) Sub(delta int32) int32 { + return atomic.AddInt32(&i.v, -delta) +} + +// Inc atomically increments the wrapped int32 and returns the new value. +func (i *Int32) Inc() int32 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped int32 and returns the new value. +func (i *Int32) Dec() int32 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. +func (i *Int32) CAS(old, new int32) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Int32) CompareAndSwap(old, new int32) (swapped bool) { + return atomic.CompareAndSwapInt32(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Int32) Store(val int32) { + atomic.StoreInt32(&i.v, val) +} + +// Swap atomically swaps the wrapped int32 and returns the old value. +func (i *Int32) Swap(val int32) (old int32) { + return atomic.SwapInt32(&i.v, val) +} + +// MarshalJSON encodes the wrapped int32 into JSON. +func (i *Int32) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped int32. +func (i *Int32) UnmarshalJSON(b []byte) error { + var v int32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Int32) String() string { + v := i.Load() + return strconv.FormatInt(int64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/int64.go b/vendor/go.uber.org/atomic/int64.go new file mode 100644 index 00000000000..460821d009d --- /dev/null +++ b/vendor/go.uber.org/atomic/int64.go @@ -0,0 +1,109 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Int64 is an atomic wrapper around int64. +type Int64 struct { + _ nocmp // disallow non-atomic comparison + + v int64 +} + +// NewInt64 creates a new Int64. +func NewInt64(val int64) *Int64 { + return &Int64{v: val} +} + +// Load atomically loads the wrapped value. +func (i *Int64) Load() int64 { + return atomic.LoadInt64(&i.v) +} + +// Add atomically adds to the wrapped int64 and returns the new value. +func (i *Int64) Add(delta int64) int64 { + return atomic.AddInt64(&i.v, delta) +} + +// Sub atomically subtracts from the wrapped int64 and returns the new value. +func (i *Int64) Sub(delta int64) int64 { + return atomic.AddInt64(&i.v, -delta) +} + +// Inc atomically increments the wrapped int64 and returns the new value. +func (i *Int64) Inc() int64 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped int64 and returns the new value. +func (i *Int64) Dec() int64 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. +func (i *Int64) CAS(old, new int64) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Int64) CompareAndSwap(old, new int64) (swapped bool) { + return atomic.CompareAndSwapInt64(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Int64) Store(val int64) { + atomic.StoreInt64(&i.v, val) +} + +// Swap atomically swaps the wrapped int64 and returns the old value. +func (i *Int64) Swap(val int64) (old int64) { + return atomic.SwapInt64(&i.v, val) +} + +// MarshalJSON encodes the wrapped int64 into JSON. +func (i *Int64) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped int64. +func (i *Int64) UnmarshalJSON(b []byte) error { + var v int64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Int64) String() string { + v := i.Load() + return strconv.FormatInt(int64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/nocmp.go b/vendor/go.uber.org/atomic/nocmp.go new file mode 100644 index 00000000000..54b74174abd --- /dev/null +++ b/vendor/go.uber.org/atomic/nocmp.go @@ -0,0 +1,35 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// nocmp is an uncomparable struct. Embed this inside another struct to make +// it uncomparable. +// +// type Foo struct { +// nocmp +// // ... +// } +// +// This DOES NOT: +// +// - Disallow shallow copies of structs +// - Disallow comparison of pointers to uncomparable structs +type nocmp [0]func() diff --git a/vendor/go.uber.org/atomic/pointer_go118.go b/vendor/go.uber.org/atomic/pointer_go118.go new file mode 100644 index 00000000000..1fb6c03b261 --- /dev/null +++ b/vendor/go.uber.org/atomic/pointer_go118.go @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//go:build go1.18 +// +build go1.18 + +package atomic + +import "fmt" + +// String returns a human readable representation of a Pointer's underlying value. +func (p *Pointer[T]) String() string { + return fmt.Sprint(p.Load()) +} diff --git a/vendor/go.uber.org/atomic/pointer_go118_pre119.go b/vendor/go.uber.org/atomic/pointer_go118_pre119.go new file mode 100644 index 00000000000..e0f47dba468 --- /dev/null +++ b/vendor/go.uber.org/atomic/pointer_go118_pre119.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//go:build go1.18 && !go1.19 +// +build go1.18,!go1.19 + +package atomic + +import "unsafe" + +type Pointer[T any] struct { + _ nocmp // disallow non-atomic comparison + p UnsafePointer +} + +// NewPointer creates a new Pointer. +func NewPointer[T any](v *T) *Pointer[T] { + var p Pointer[T] + if v != nil { + p.p.Store(unsafe.Pointer(v)) + } + return &p +} + +// Load atomically loads the wrapped value. +func (p *Pointer[T]) Load() *T { + return (*T)(p.p.Load()) +} + +// Store atomically stores the passed value. +func (p *Pointer[T]) Store(val *T) { + p.p.Store(unsafe.Pointer(val)) +} + +// Swap atomically swaps the wrapped pointer and returns the old value. +func (p *Pointer[T]) Swap(val *T) (old *T) { + return (*T)(p.p.Swap(unsafe.Pointer(val))) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (p *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) { + return p.p.CompareAndSwap(unsafe.Pointer(old), unsafe.Pointer(new)) +} diff --git a/vendor/go.uber.org/atomic/pointer_go119.go b/vendor/go.uber.org/atomic/pointer_go119.go new file mode 100644 index 00000000000..6726f17ad64 --- /dev/null +++ b/vendor/go.uber.org/atomic/pointer_go119.go @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//go:build go1.19 +// +build go1.19 + +package atomic + +import "sync/atomic" + +// Pointer is an atomic pointer of type *T. +type Pointer[T any] struct { + _ nocmp // disallow non-atomic comparison + p atomic.Pointer[T] +} + +// NewPointer creates a new Pointer. +func NewPointer[T any](v *T) *Pointer[T] { + var p Pointer[T] + if v != nil { + p.p.Store(v) + } + return &p +} + +// Load atomically loads the wrapped value. +func (p *Pointer[T]) Load() *T { + return p.p.Load() +} + +// Store atomically stores the passed value. +func (p *Pointer[T]) Store(val *T) { + p.p.Store(val) +} + +// Swap atomically swaps the wrapped pointer and returns the old value. +func (p *Pointer[T]) Swap(val *T) (old *T) { + return p.p.Swap(val) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (p *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) { + return p.p.CompareAndSwap(old, new) +} diff --git a/vendor/go.uber.org/atomic/string.go b/vendor/go.uber.org/atomic/string.go new file mode 100644 index 00000000000..061466c5bde --- /dev/null +++ b/vendor/go.uber.org/atomic/string.go @@ -0,0 +1,72 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// String is an atomic type-safe wrapper for string values. +type String struct { + _ nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroString string + +// NewString creates a new String. +func NewString(val string) *String { + x := &String{} + if val != _zeroString { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped string. +func (x *String) Load() string { + return unpackString(x.v.Load()) +} + +// Store atomically stores the passed string. +func (x *String) Store(val string) { + x.v.Store(packString(val)) +} + +// CompareAndSwap is an atomic compare-and-swap for string values. +func (x *String) CompareAndSwap(old, new string) (swapped bool) { + if x.v.CompareAndSwap(packString(old), packString(new)) { + return true + } + + if old == _zeroString { + // If the old value is the empty value, then it's possible the + // underlying Value hasn't been set and is nil, so retry with nil. + return x.v.CompareAndSwap(nil, packString(new)) + } + + return false +} + +// Swap atomically stores the given string and returns the old +// value. +func (x *String) Swap(val string) (old string) { + return unpackString(x.v.Swap(packString(val))) +} diff --git a/vendor/go.uber.org/atomic/string_ext.go b/vendor/go.uber.org/atomic/string_ext.go new file mode 100644 index 00000000000..019109c86ba --- /dev/null +++ b/vendor/go.uber.org/atomic/string_ext.go @@ -0,0 +1,54 @@ +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped Value -pack packString -unpack unpackString -compareandswap -swap -file=string.go + +func packString(s string) interface{} { + return s +} + +func unpackString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +// String returns the wrapped value. +func (s *String) String() string { + return s.Load() +} + +// MarshalText encodes the wrapped string into a textual form. +// +// This makes it encodable as JSON, YAML, XML, and more. +func (s *String) MarshalText() ([]byte, error) { + return []byte(s.Load()), nil +} + +// UnmarshalText decodes text and replaces the wrapped string with it. +// +// This makes it decodable from JSON, YAML, XML, and more. +func (s *String) UnmarshalText(b []byte) error { + s.Store(string(b)) + return nil +} diff --git a/vendor/go.uber.org/atomic/time.go b/vendor/go.uber.org/atomic/time.go new file mode 100644 index 00000000000..cc2a230c001 --- /dev/null +++ b/vendor/go.uber.org/atomic/time.go @@ -0,0 +1,55 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "time" +) + +// Time is an atomic type-safe wrapper for time.Time values. +type Time struct { + _ nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroTime time.Time + +// NewTime creates a new Time. +func NewTime(val time.Time) *Time { + x := &Time{} + if val != _zeroTime { + x.Store(val) + } + return x +} + +// Load atomically loads the wrapped time.Time. +func (x *Time) Load() time.Time { + return unpackTime(x.v.Load()) +} + +// Store atomically stores the passed time.Time. +func (x *Time) Store(val time.Time) { + x.v.Store(packTime(val)) +} diff --git a/vendor/go.uber.org/atomic/time_ext.go b/vendor/go.uber.org/atomic/time_ext.go new file mode 100644 index 00000000000..1e3dc978aa5 --- /dev/null +++ b/vendor/go.uber.org/atomic/time_ext.go @@ -0,0 +1,36 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "time" + +//go:generate bin/gen-atomicwrapper -name=Time -type=time.Time -wrapped=Value -pack=packTime -unpack=unpackTime -imports time -file=time.go + +func packTime(t time.Time) interface{} { + return t +} + +func unpackTime(v interface{}) time.Time { + if t, ok := v.(time.Time); ok { + return t + } + return time.Time{} +} diff --git a/vendor/go.uber.org/atomic/uint32.go b/vendor/go.uber.org/atomic/uint32.go new file mode 100644 index 00000000000..4adc294ac2a --- /dev/null +++ b/vendor/go.uber.org/atomic/uint32.go @@ -0,0 +1,109 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Uint32 is an atomic wrapper around uint32. +type Uint32 struct { + _ nocmp // disallow non-atomic comparison + + v uint32 +} + +// NewUint32 creates a new Uint32. +func NewUint32(val uint32) *Uint32 { + return &Uint32{v: val} +} + +// Load atomically loads the wrapped value. +func (i *Uint32) Load() uint32 { + return atomic.LoadUint32(&i.v) +} + +// Add atomically adds to the wrapped uint32 and returns the new value. +func (i *Uint32) Add(delta uint32) uint32 { + return atomic.AddUint32(&i.v, delta) +} + +// Sub atomically subtracts from the wrapped uint32 and returns the new value. +func (i *Uint32) Sub(delta uint32) uint32 { + return atomic.AddUint32(&i.v, ^(delta - 1)) +} + +// Inc atomically increments the wrapped uint32 and returns the new value. +func (i *Uint32) Inc() uint32 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped uint32 and returns the new value. +func (i *Uint32) Dec() uint32 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. +func (i *Uint32) CAS(old, new uint32) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uint32) CompareAndSwap(old, new uint32) (swapped bool) { + return atomic.CompareAndSwapUint32(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Uint32) Store(val uint32) { + atomic.StoreUint32(&i.v, val) +} + +// Swap atomically swaps the wrapped uint32 and returns the old value. +func (i *Uint32) Swap(val uint32) (old uint32) { + return atomic.SwapUint32(&i.v, val) +} + +// MarshalJSON encodes the wrapped uint32 into JSON. +func (i *Uint32) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped uint32. +func (i *Uint32) UnmarshalJSON(b []byte) error { + var v uint32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Uint32) String() string { + v := i.Load() + return strconv.FormatUint(uint64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/uint64.go b/vendor/go.uber.org/atomic/uint64.go new file mode 100644 index 00000000000..0e2eddb3038 --- /dev/null +++ b/vendor/go.uber.org/atomic/uint64.go @@ -0,0 +1,109 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Uint64 is an atomic wrapper around uint64. +type Uint64 struct { + _ nocmp // disallow non-atomic comparison + + v uint64 +} + +// NewUint64 creates a new Uint64. +func NewUint64(val uint64) *Uint64 { + return &Uint64{v: val} +} + +// Load atomically loads the wrapped value. +func (i *Uint64) Load() uint64 { + return atomic.LoadUint64(&i.v) +} + +// Add atomically adds to the wrapped uint64 and returns the new value. +func (i *Uint64) Add(delta uint64) uint64 { + return atomic.AddUint64(&i.v, delta) +} + +// Sub atomically subtracts from the wrapped uint64 and returns the new value. +func (i *Uint64) Sub(delta uint64) uint64 { + return atomic.AddUint64(&i.v, ^(delta - 1)) +} + +// Inc atomically increments the wrapped uint64 and returns the new value. +func (i *Uint64) Inc() uint64 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped uint64 and returns the new value. +func (i *Uint64) Dec() uint64 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. +func (i *Uint64) CAS(old, new uint64) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uint64) CompareAndSwap(old, new uint64) (swapped bool) { + return atomic.CompareAndSwapUint64(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Uint64) Store(val uint64) { + atomic.StoreUint64(&i.v, val) +} + +// Swap atomically swaps the wrapped uint64 and returns the old value. +func (i *Uint64) Swap(val uint64) (old uint64) { + return atomic.SwapUint64(&i.v, val) +} + +// MarshalJSON encodes the wrapped uint64 into JSON. +func (i *Uint64) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped uint64. +func (i *Uint64) UnmarshalJSON(b []byte) error { + var v uint64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Uint64) String() string { + v := i.Load() + return strconv.FormatUint(uint64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/uintptr.go b/vendor/go.uber.org/atomic/uintptr.go new file mode 100644 index 00000000000..7d5b000d610 --- /dev/null +++ b/vendor/go.uber.org/atomic/uintptr.go @@ -0,0 +1,109 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020-2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Uintptr is an atomic wrapper around uintptr. +type Uintptr struct { + _ nocmp // disallow non-atomic comparison + + v uintptr +} + +// NewUintptr creates a new Uintptr. +func NewUintptr(val uintptr) *Uintptr { + return &Uintptr{v: val} +} + +// Load atomically loads the wrapped value. +func (i *Uintptr) Load() uintptr { + return atomic.LoadUintptr(&i.v) +} + +// Add atomically adds to the wrapped uintptr and returns the new value. +func (i *Uintptr) Add(delta uintptr) uintptr { + return atomic.AddUintptr(&i.v, delta) +} + +// Sub atomically subtracts from the wrapped uintptr and returns the new value. +func (i *Uintptr) Sub(delta uintptr) uintptr { + return atomic.AddUintptr(&i.v, ^(delta - 1)) +} + +// Inc atomically increments the wrapped uintptr and returns the new value. +func (i *Uintptr) Inc() uintptr { + return i.Add(1) +} + +// Dec atomically decrements the wrapped uintptr and returns the new value. +func (i *Uintptr) Dec() uintptr { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. +func (i *Uintptr) CAS(old, new uintptr) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uintptr) CompareAndSwap(old, new uintptr) (swapped bool) { + return atomic.CompareAndSwapUintptr(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Uintptr) Store(val uintptr) { + atomic.StoreUintptr(&i.v, val) +} + +// Swap atomically swaps the wrapped uintptr and returns the old value. +func (i *Uintptr) Swap(val uintptr) (old uintptr) { + return atomic.SwapUintptr(&i.v, val) +} + +// MarshalJSON encodes the wrapped uintptr into JSON. +func (i *Uintptr) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped uintptr. +func (i *Uintptr) UnmarshalJSON(b []byte) error { + var v uintptr + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Uintptr) String() string { + v := i.Load() + return strconv.FormatUint(uint64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/unsafe_pointer.go b/vendor/go.uber.org/atomic/unsafe_pointer.go new file mode 100644 index 00000000000..34868baf6a8 --- /dev/null +++ b/vendor/go.uber.org/atomic/unsafe_pointer.go @@ -0,0 +1,65 @@ +// Copyright (c) 2021-2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "sync/atomic" + "unsafe" +) + +// UnsafePointer is an atomic wrapper around unsafe.Pointer. +type UnsafePointer struct { + _ nocmp // disallow non-atomic comparison + + v unsafe.Pointer +} + +// NewUnsafePointer creates a new UnsafePointer. +func NewUnsafePointer(val unsafe.Pointer) *UnsafePointer { + return &UnsafePointer{v: val} +} + +// Load atomically loads the wrapped value. +func (p *UnsafePointer) Load() unsafe.Pointer { + return atomic.LoadPointer(&p.v) +} + +// Store atomically stores the passed value. +func (p *UnsafePointer) Store(val unsafe.Pointer) { + atomic.StorePointer(&p.v, val) +} + +// Swap atomically swaps the wrapped unsafe.Pointer and returns the old value. +func (p *UnsafePointer) Swap(val unsafe.Pointer) (old unsafe.Pointer) { + return atomic.SwapPointer(&p.v, val) +} + +// CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap +func (p *UnsafePointer) CAS(old, new unsafe.Pointer) (swapped bool) { + return p.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (p *UnsafePointer) CompareAndSwap(old, new unsafe.Pointer) (swapped bool) { + return atomic.CompareAndSwapPointer(&p.v, old, new) +} diff --git a/vendor/go.uber.org/atomic/value.go b/vendor/go.uber.org/atomic/value.go new file mode 100644 index 00000000000..52caedb9a58 --- /dev/null +++ b/vendor/go.uber.org/atomic/value.go @@ -0,0 +1,31 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "sync/atomic" + +// Value shadows the type of the same name from sync/atomic +// https://godoc.org/sync/atomic#Value +type Value struct { + _ nocmp // disallow non-atomic comparison + + atomic.Value +} diff --git a/vendor/golang.org/x/sys/plan9/asm.s b/vendor/golang.org/x/sys/plan9/asm.s deleted file mode 100644 index 06449ebfa9e..00000000000 --- a/vendor/golang.org/x/sys/plan9/asm.s +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2014 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 file. - -#include "textflag.h" - -TEXT ·use(SB),NOSPLIT,$0 - RET diff --git a/vendor/golang.org/x/sys/plan9/asm_plan9_386.s b/vendor/golang.org/x/sys/plan9/asm_plan9_386.s deleted file mode 100644 index bc5cab1f347..00000000000 --- a/vendor/golang.org/x/sys/plan9/asm_plan9_386.s +++ /dev/null @@ -1,30 +0,0 @@ -// 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 file. - -#include "textflag.h" - -// -// System call support for 386, Plan 9 -// - -// Just jump to package syscall's implementation for all these functions. -// The runtime may know about them. - -TEXT ·Syscall(SB),NOSPLIT,$0-32 - JMP syscall·Syscall(SB) - -TEXT ·Syscall6(SB),NOSPLIT,$0-44 - JMP syscall·Syscall6(SB) - -TEXT ·RawSyscall(SB),NOSPLIT,$0-28 - JMP syscall·RawSyscall(SB) - -TEXT ·RawSyscall6(SB),NOSPLIT,$0-40 - JMP syscall·RawSyscall6(SB) - -TEXT ·seek(SB),NOSPLIT,$0-36 - JMP syscall·seek(SB) - -TEXT ·exit(SB),NOSPLIT,$4-4 - JMP syscall·exit(SB) diff --git a/vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s b/vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s deleted file mode 100644 index d3448e6750b..00000000000 --- a/vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s +++ /dev/null @@ -1,30 +0,0 @@ -// 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 file. - -#include "textflag.h" - -// -// System call support for amd64, Plan 9 -// - -// Just jump to package syscall's implementation for all these functions. -// The runtime may know about them. - -TEXT ·Syscall(SB),NOSPLIT,$0-64 - JMP syscall·Syscall(SB) - -TEXT ·Syscall6(SB),NOSPLIT,$0-88 - JMP syscall·Syscall6(SB) - -TEXT ·RawSyscall(SB),NOSPLIT,$0-56 - JMP syscall·RawSyscall(SB) - -TEXT ·RawSyscall6(SB),NOSPLIT,$0-80 - JMP syscall·RawSyscall6(SB) - -TEXT ·seek(SB),NOSPLIT,$0-56 - JMP syscall·seek(SB) - -TEXT ·exit(SB),NOSPLIT,$8-8 - JMP syscall·exit(SB) diff --git a/vendor/golang.org/x/sys/plan9/asm_plan9_arm.s b/vendor/golang.org/x/sys/plan9/asm_plan9_arm.s deleted file mode 100644 index afb7c0a9b90..00000000000 --- a/vendor/golang.org/x/sys/plan9/asm_plan9_arm.s +++ /dev/null @@ -1,25 +0,0 @@ -// 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 file. - -#include "textflag.h" - -// System call support for plan9 on arm - -// Just jump to package syscall's implementation for all these functions. -// The runtime may know about them. - -TEXT ·Syscall(SB),NOSPLIT,$0-32 - JMP syscall·Syscall(SB) - -TEXT ·Syscall6(SB),NOSPLIT,$0-44 - JMP syscall·Syscall6(SB) - -TEXT ·RawSyscall(SB),NOSPLIT,$0-28 - JMP syscall·RawSyscall(SB) - -TEXT ·RawSyscall6(SB),NOSPLIT,$0-40 - JMP syscall·RawSyscall6(SB) - -TEXT ·seek(SB),NOSPLIT,$0-36 - JMP syscall·exit(SB) diff --git a/vendor/golang.org/x/sys/plan9/const_plan9.go b/vendor/golang.org/x/sys/plan9/const_plan9.go deleted file mode 100644 index b4e85a3a9d3..00000000000 --- a/vendor/golang.org/x/sys/plan9/const_plan9.go +++ /dev/null @@ -1,70 +0,0 @@ -package plan9 - -// Plan 9 Constants - -// Open modes -const ( - O_RDONLY = 0 - O_WRONLY = 1 - O_RDWR = 2 - O_TRUNC = 16 - O_CLOEXEC = 32 - O_EXCL = 0x1000 -) - -// Rfork flags -const ( - RFNAMEG = 1 << 0 - RFENVG = 1 << 1 - RFFDG = 1 << 2 - RFNOTEG = 1 << 3 - RFPROC = 1 << 4 - RFMEM = 1 << 5 - RFNOWAIT = 1 << 6 - RFCNAMEG = 1 << 10 - RFCENVG = 1 << 11 - RFCFDG = 1 << 12 - RFREND = 1 << 13 - RFNOMNT = 1 << 14 -) - -// Qid.Type bits -const ( - QTDIR = 0x80 - QTAPPEND = 0x40 - QTEXCL = 0x20 - QTMOUNT = 0x10 - QTAUTH = 0x08 - QTTMP = 0x04 - QTFILE = 0x00 -) - -// Dir.Mode bits -const ( - DMDIR = 0x80000000 - DMAPPEND = 0x40000000 - DMEXCL = 0x20000000 - DMMOUNT = 0x10000000 - DMAUTH = 0x08000000 - DMTMP = 0x04000000 - DMREAD = 0x4 - DMWRITE = 0x2 - DMEXEC = 0x1 -) - -const ( - STATMAX = 65535 - ERRMAX = 128 - STATFIXLEN = 49 -) - -// Mount and bind flags -const ( - MREPL = 0x0000 - MBEFORE = 0x0001 - MAFTER = 0x0002 - MORDER = 0x0003 - MCREATE = 0x0004 - MCACHE = 0x0010 - MMASK = 0x0017 -) diff --git a/vendor/golang.org/x/sys/plan9/dir_plan9.go b/vendor/golang.org/x/sys/plan9/dir_plan9.go deleted file mode 100644 index 0955e0c53e0..00000000000 --- a/vendor/golang.org/x/sys/plan9/dir_plan9.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2012 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 file. - -// Plan 9 directory marshalling. See intro(5). - -package plan9 - -import "errors" - -var ( - ErrShortStat = errors.New("stat buffer too short") - ErrBadStat = errors.New("malformed stat buffer") - ErrBadName = errors.New("bad character in file name") -) - -// A Qid represents a 9P server's unique identification for a file. -type Qid struct { - Path uint64 // the file server's unique identification for the file - Vers uint32 // version number for given Path - Type uint8 // the type of the file (plan9.QTDIR for example) -} - -// A Dir contains the metadata for a file. -type Dir struct { - // system-modified data - Type uint16 // server type - Dev uint32 // server subtype - - // file data - Qid Qid // unique id from server - Mode uint32 // permissions - Atime uint32 // last read time - Mtime uint32 // last write time - Length int64 // file length - Name string // last element of path - Uid string // owner name - Gid string // group name - Muid string // last modifier name -} - -var nullDir = Dir{ - Type: ^uint16(0), - Dev: ^uint32(0), - Qid: Qid{ - Path: ^uint64(0), - Vers: ^uint32(0), - Type: ^uint8(0), - }, - Mode: ^uint32(0), - Atime: ^uint32(0), - Mtime: ^uint32(0), - Length: ^int64(0), -} - -// Null assigns special "don't touch" values to members of d to -// avoid modifying them during plan9.Wstat. -func (d *Dir) Null() { *d = nullDir } - -// Marshal encodes a 9P stat message corresponding to d into b -// -// If there isn't enough space in b for a stat message, ErrShortStat is returned. -func (d *Dir) Marshal(b []byte) (n int, err error) { - n = STATFIXLEN + len(d.Name) + len(d.Uid) + len(d.Gid) + len(d.Muid) - if n > len(b) { - return n, ErrShortStat - } - - for _, c := range d.Name { - if c == '/' { - return n, ErrBadName - } - } - - b = pbit16(b, uint16(n)-2) - b = pbit16(b, d.Type) - b = pbit32(b, d.Dev) - b = pbit8(b, d.Qid.Type) - b = pbit32(b, d.Qid.Vers) - b = pbit64(b, d.Qid.Path) - b = pbit32(b, d.Mode) - b = pbit32(b, d.Atime) - b = pbit32(b, d.Mtime) - b = pbit64(b, uint64(d.Length)) - b = pstring(b, d.Name) - b = pstring(b, d.Uid) - b = pstring(b, d.Gid) - b = pstring(b, d.Muid) - - return n, nil -} - -// UnmarshalDir decodes a single 9P stat message from b and returns the resulting Dir. -// -// If b is too small to hold a valid stat message, ErrShortStat is returned. -// -// If the stat message itself is invalid, ErrBadStat is returned. -func UnmarshalDir(b []byte) (*Dir, error) { - if len(b) < STATFIXLEN { - return nil, ErrShortStat - } - size, buf := gbit16(b) - if len(b) != int(size)+2 { - return nil, ErrBadStat - } - b = buf - - var d Dir - d.Type, b = gbit16(b) - d.Dev, b = gbit32(b) - d.Qid.Type, b = gbit8(b) - d.Qid.Vers, b = gbit32(b) - d.Qid.Path, b = gbit64(b) - d.Mode, b = gbit32(b) - d.Atime, b = gbit32(b) - d.Mtime, b = gbit32(b) - - n, b := gbit64(b) - d.Length = int64(n) - - var ok bool - if d.Name, b, ok = gstring(b); !ok { - return nil, ErrBadStat - } - if d.Uid, b, ok = gstring(b); !ok { - return nil, ErrBadStat - } - if d.Gid, b, ok = gstring(b); !ok { - return nil, ErrBadStat - } - if d.Muid, b, ok = gstring(b); !ok { - return nil, ErrBadStat - } - - return &d, nil -} - -// pbit8 copies the 8-bit number v to b and returns the remaining slice of b. -func pbit8(b []byte, v uint8) []byte { - b[0] = byte(v) - return b[1:] -} - -// pbit16 copies the 16-bit number v to b in little-endian order and returns the remaining slice of b. -func pbit16(b []byte, v uint16) []byte { - b[0] = byte(v) - b[1] = byte(v >> 8) - return b[2:] -} - -// pbit32 copies the 32-bit number v to b in little-endian order and returns the remaining slice of b. -func pbit32(b []byte, v uint32) []byte { - b[0] = byte(v) - b[1] = byte(v >> 8) - b[2] = byte(v >> 16) - b[3] = byte(v >> 24) - return b[4:] -} - -// pbit64 copies the 64-bit number v to b in little-endian order and returns the remaining slice of b. -func pbit64(b []byte, v uint64) []byte { - b[0] = byte(v) - b[1] = byte(v >> 8) - b[2] = byte(v >> 16) - b[3] = byte(v >> 24) - b[4] = byte(v >> 32) - b[5] = byte(v >> 40) - b[6] = byte(v >> 48) - b[7] = byte(v >> 56) - return b[8:] -} - -// pstring copies the string s to b, prepending it with a 16-bit length in little-endian order, and -// returning the remaining slice of b.. -func pstring(b []byte, s string) []byte { - b = pbit16(b, uint16(len(s))) - n := copy(b, s) - return b[n:] -} - -// gbit8 reads an 8-bit number from b and returns it with the remaining slice of b. -func gbit8(b []byte) (uint8, []byte) { - return uint8(b[0]), b[1:] -} - -// gbit16 reads a 16-bit number in little-endian order from b and returns it with the remaining slice of b. -func gbit16(b []byte) (uint16, []byte) { - return uint16(b[0]) | uint16(b[1])<<8, b[2:] -} - -// gbit32 reads a 32-bit number in little-endian order from b and returns it with the remaining slice of b. -func gbit32(b []byte) (uint32, []byte) { - return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24, b[4:] -} - -// gbit64 reads a 64-bit number in little-endian order from b and returns it with the remaining slice of b. -func gbit64(b []byte) (uint64, []byte) { - lo := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 - hi := uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24 - return uint64(lo) | uint64(hi)<<32, b[8:] -} - -// gstring reads a string from b, prefixed with a 16-bit length in little-endian order. -// It returns the string with the remaining slice of b and a boolean. If the length is -// greater than the number of bytes in b, the boolean will be false. -func gstring(b []byte) (string, []byte, bool) { - n, b := gbit16(b) - if int(n) > len(b) { - return "", b, false - } - return string(b[:n]), b[n:], true -} diff --git a/vendor/golang.org/x/sys/plan9/env_plan9.go b/vendor/golang.org/x/sys/plan9/env_plan9.go deleted file mode 100644 index 8f1918004ff..00000000000 --- a/vendor/golang.org/x/sys/plan9/env_plan9.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2011 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 file. - -// Plan 9 environment variables. - -package plan9 - -import ( - "syscall" -) - -func Getenv(key string) (value string, found bool) { - return syscall.Getenv(key) -} - -func Setenv(key, value string) error { - return syscall.Setenv(key, value) -} - -func Clearenv() { - syscall.Clearenv() -} - -func Environ() []string { - return syscall.Environ() -} - -func Unsetenv(key string) error { - return syscall.Unsetenv(key) -} diff --git a/vendor/golang.org/x/sys/plan9/errors_plan9.go b/vendor/golang.org/x/sys/plan9/errors_plan9.go deleted file mode 100644 index 65fe74d3efb..00000000000 --- a/vendor/golang.org/x/sys/plan9/errors_plan9.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2011 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 file. - -package plan9 - -import "syscall" - -// Constants -const ( - // Invented values to support what package os expects. - O_CREAT = 0x02000 - O_APPEND = 0x00400 - O_NOCTTY = 0x00000 - O_NONBLOCK = 0x00000 - O_SYNC = 0x00000 - O_ASYNC = 0x00000 - - S_IFMT = 0x1f000 - S_IFIFO = 0x1000 - S_IFCHR = 0x2000 - S_IFDIR = 0x4000 - S_IFBLK = 0x6000 - S_IFREG = 0x8000 - S_IFLNK = 0xa000 - S_IFSOCK = 0xc000 -) - -// Errors -var ( - EINVAL = syscall.NewError("bad arg in system call") - ENOTDIR = syscall.NewError("not a directory") - EISDIR = syscall.NewError("file is a directory") - ENOENT = syscall.NewError("file does not exist") - EEXIST = syscall.NewError("file already exists") - EMFILE = syscall.NewError("no free file descriptors") - EIO = syscall.NewError("i/o error") - ENAMETOOLONG = syscall.NewError("file name too long") - EINTR = syscall.NewError("interrupted") - EPERM = syscall.NewError("permission denied") - EBUSY = syscall.NewError("no free devices") - ETIMEDOUT = syscall.NewError("connection timed out") - EPLAN9 = syscall.NewError("not supported by plan 9") - - // The following errors do not correspond to any - // Plan 9 system messages. Invented to support - // what package os and others expect. - EACCES = syscall.NewError("access permission denied") - EAFNOSUPPORT = syscall.NewError("address family not supported by protocol") -) diff --git a/vendor/golang.org/x/sys/plan9/mkall.sh b/vendor/golang.org/x/sys/plan9/mkall.sh deleted file mode 100644 index 1650fbcc745..00000000000 --- a/vendor/golang.org/x/sys/plan9/mkall.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -# 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 file. - -# The plan9 package provides access to the raw system call -# interface of the underlying operating system. Porting Go to -# a new architecture/operating system combination requires -# some manual effort, though there are tools that automate -# much of the process. The auto-generated files have names -# beginning with z. -# -# This script runs or (given -n) prints suggested commands to generate z files -# for the current system. Running those commands is not automatic. -# This script is documentation more than anything else. -# -# * asm_${GOOS}_${GOARCH}.s -# -# This hand-written assembly file implements system call dispatch. -# There are three entry points: -# -# func Syscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr); -# func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr); -# func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr); -# -# The first and second are the standard ones; they differ only in -# how many arguments can be passed to the kernel. -# The third is for low-level use by the ForkExec wrapper; -# unlike the first two, it does not call into the scheduler to -# let it know that a system call is running. -# -# * syscall_${GOOS}.go -# -# This hand-written Go file implements system calls that need -# special handling and lists "//sys" comments giving prototypes -# for ones that can be auto-generated. Mksyscall reads those -# comments to generate the stubs. -# -# * syscall_${GOOS}_${GOARCH}.go -# -# Same as syscall_${GOOS}.go except that it contains code specific -# to ${GOOS} on one particular architecture. -# -# * types_${GOOS}.c -# -# This hand-written C file includes standard C headers and then -# creates typedef or enum names beginning with a dollar sign -# (use of $ in variable names is a gcc extension). The hardest -# part about preparing this file is figuring out which headers to -# include and which symbols need to be #defined to get the -# actual data structures that pass through to the kernel system calls. -# Some C libraries present alternate versions for binary compatibility -# and translate them on the way in and out of system calls, but -# there is almost always a #define that can get the real ones. -# See types_darwin.c and types_linux.c for examples. -# -# * zerror_${GOOS}_${GOARCH}.go -# -# This machine-generated file defines the system's error numbers, -# error strings, and signal numbers. The generator is "mkerrors.sh". -# Usually no arguments are needed, but mkerrors.sh will pass its -# arguments on to godefs. -# -# * zsyscall_${GOOS}_${GOARCH}.go -# -# Generated by mksyscall.pl; see syscall_${GOOS}.go above. -# -# * zsysnum_${GOOS}_${GOARCH}.go -# -# Generated by mksysnum_${GOOS}. -# -# * ztypes_${GOOS}_${GOARCH}.go -# -# Generated by godefs; see types_${GOOS}.c above. - -GOOSARCH="${GOOS}_${GOARCH}" - -# defaults -mksyscall="go run mksyscall.go" -mkerrors="./mkerrors.sh" -zerrors="zerrors_$GOOSARCH.go" -mksysctl="" -zsysctl="zsysctl_$GOOSARCH.go" -mksysnum= -mktypes= -run="sh" - -case "$1" in --syscalls) - for i in zsyscall*go - do - sed 1q $i | sed 's;^// ;;' | sh > _$i && gofmt < _$i > $i - rm _$i - done - exit 0 - ;; --n) - run="cat" - shift -esac - -case "$#" in -0) - ;; -*) - echo 'usage: mkall.sh [-n]' 1>&2 - exit 2 -esac - -case "$GOOSARCH" in -_* | *_ | _) - echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2 - exit 1 - ;; -plan9_386) - mkerrors= - mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,386" - mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h" - mktypes="XXX" - ;; -plan9_amd64) - mkerrors= - mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,amd64" - mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h" - mktypes="XXX" - ;; -plan9_arm) - mkerrors= - mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,arm" - mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h" - mktypes="XXX" - ;; -*) - echo 'unrecognized $GOOS_$GOARCH: ' "$GOOSARCH" 1>&2 - exit 1 - ;; -esac - -( - if [ -n "$mkerrors" ]; then echo "$mkerrors |gofmt >$zerrors"; fi - case "$GOOS" in - plan9) - syscall_goos="syscall_$GOOS.go" - if [ -n "$mksyscall" ]; then echo "$mksyscall $syscall_goos |gofmt >zsyscall_$GOOSARCH.go"; fi - ;; - esac - if [ -n "$mksysctl" ]; then echo "$mksysctl |gofmt >$zsysctl"; fi - if [ -n "$mksysnum" ]; then echo "$mksysnum |gofmt >zsysnum_$GOOSARCH.go"; fi - if [ -n "$mktypes" ]; then echo "$mktypes types_$GOOS.go |gofmt >ztypes_$GOOSARCH.go"; fi -) | $run diff --git a/vendor/golang.org/x/sys/plan9/mkerrors.sh b/vendor/golang.org/x/sys/plan9/mkerrors.sh deleted file mode 100644 index 526d04ab68c..00000000000 --- a/vendor/golang.org/x/sys/plan9/mkerrors.sh +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env bash -# 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 file. - -# Generate Go code listing errors and other #defined constant -# values (ENAMETOOLONG etc.), by asking the preprocessor -# about the definitions. - -unset LANG -export LC_ALL=C -export LC_CTYPE=C - -CC=${CC:-gcc} - -uname=$(uname) - -includes=' -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -' - -ccflags="$@" - -# Write go tool cgo -godefs input. -( - echo package plan9 - echo - echo '/*' - indirect="includes_$(uname)" - echo "${!indirect} $includes" - echo '*/' - echo 'import "C"' - echo - echo 'const (' - - # The gcc command line prints all the #defines - # it encounters while processing the input - echo "${!indirect} $includes" | $CC -x c - -E -dM $ccflags | - awk ' - $1 != "#define" || $2 ~ /\(/ || $3 == "" {next} - - $2 ~ /^E([ABCD]X|[BIS]P|[SD]I|S|FL)$/ {next} # 386 registers - $2 ~ /^(SIGEV_|SIGSTKSZ|SIGRT(MIN|MAX))/ {next} - $2 ~ /^(SCM_SRCRT)$/ {next} - $2 ~ /^(MAP_FAILED)$/ {next} - - $2 !~ /^ETH_/ && - $2 !~ /^EPROC_/ && - $2 !~ /^EQUIV_/ && - $2 !~ /^EXPR_/ && - $2 ~ /^E[A-Z0-9_]+$/ || - $2 ~ /^B[0-9_]+$/ || - $2 ~ /^V[A-Z0-9]+$/ || - $2 ~ /^CS[A-Z0-9]/ || - $2 ~ /^I(SIG|CANON|CRNL|EXTEN|MAXBEL|STRIP|UTF8)$/ || - $2 ~ /^IGN/ || - $2 ~ /^IX(ON|ANY|OFF)$/ || - $2 ~ /^IN(LCR|PCK)$/ || - $2 ~ /(^FLU?SH)|(FLU?SH$)/ || - $2 ~ /^C(LOCAL|READ)$/ || - $2 == "BRKINT" || - $2 == "HUPCL" || - $2 == "PENDIN" || - $2 == "TOSTOP" || - $2 ~ /^PAR/ || - $2 ~ /^SIG[^_]/ || - $2 ~ /^O[CNPFP][A-Z]+[^_][A-Z]+$/ || - $2 ~ /^IN_/ || - $2 ~ /^LOCK_(SH|EX|NB|UN)$/ || - $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|ICMP6|TCP|EVFILT|NOTE|EV|SHUT|PROT|MAP|PACKET|MSG|SCM|MCL|DT|MADV|PR)_/ || - $2 == "ICMPV6_FILTER" || - $2 == "SOMAXCONN" || - $2 == "NAME_MAX" || - $2 == "IFNAMSIZ" || - $2 ~ /^CTL_(MAXNAME|NET|QUERY)$/ || - $2 ~ /^SYSCTL_VERS/ || - $2 ~ /^(MS|MNT)_/ || - $2 ~ /^TUN(SET|GET|ATTACH|DETACH)/ || - $2 ~ /^(O|F|FD|NAME|S|PTRACE|PT)_/ || - $2 ~ /^LINUX_REBOOT_CMD_/ || - $2 ~ /^LINUX_REBOOT_MAGIC[12]$/ || - $2 !~ "NLA_TYPE_MASK" && - $2 ~ /^(NETLINK|NLM|NLMSG|NLA|IFA|IFAN|RT|RTCF|RTN|RTPROT|RTNH|ARPHRD|ETH_P)_/ || - $2 ~ /^SIOC/ || - $2 ~ /^TIOC/ || - $2 !~ "RTF_BITS" && - $2 ~ /^(IFF|IFT|NET_RT|RTM|RTF|RTV|RTA|RTAX)_/ || - $2 ~ /^BIOC/ || - $2 ~ /^RUSAGE_(SELF|CHILDREN|THREAD)/ || - $2 ~ /^RLIMIT_(AS|CORE|CPU|DATA|FSIZE|NOFILE|STACK)|RLIM_INFINITY/ || - $2 ~ /^PRIO_(PROCESS|PGRP|USER)/ || - $2 ~ /^CLONE_[A-Z_]+/ || - $2 !~ /^(BPF_TIMEVAL)$/ && - $2 ~ /^(BPF|DLT)_/ || - $2 !~ "WMESGLEN" && - $2 ~ /^W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", $2, $2)} - $2 ~ /^__WCOREFLAG$/ {next} - $2 ~ /^__W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", substr($2,3), $2)} - - {next} - ' | sort - - echo ')' -) >_const.go - -# Pull out the error names for later. -errors=$( - echo '#include ' | $CC -x c - -E -dM $ccflags | - awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print $2 }' | - sort -) - -# Pull out the signal names for later. -signals=$( - echo '#include ' | $CC -x c - -E -dM $ccflags | - awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print $2 }' | - grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' | - sort -) - -# Again, writing regexps to a file. -echo '#include ' | $CC -x c - -E -dM $ccflags | - awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print "^\t" $2 "[ \t]*=" }' | - sort >_error.grep -echo '#include ' | $CC -x c - -E -dM $ccflags | - awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print "^\t" $2 "[ \t]*=" }' | - grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' | - sort >_signal.grep - -echo '// mkerrors.sh' "$@" -echo '// Code generated by the command above; DO NOT EDIT.' -echo -go tool cgo -godefs -- "$@" _const.go >_error.out -cat _error.out | grep -vf _error.grep | grep -vf _signal.grep -echo -echo '// Errors' -echo 'const (' -cat _error.out | grep -f _error.grep | sed 's/=\(.*\)/= Errno(\1)/' -echo ')' - -echo -echo '// Signals' -echo 'const (' -cat _error.out | grep -f _signal.grep | sed 's/=\(.*\)/= Signal(\1)/' -echo ')' - -# Run C program to print error and syscall strings. -( - echo -E " -#include -#include -#include -#include -#include -#include - -#define nelem(x) (sizeof(x)/sizeof((x)[0])) - -enum { A = 'A', Z = 'Z', a = 'a', z = 'z' }; // avoid need for single quotes below - -int errors[] = { -" - for i in $errors - do - echo -E ' '$i, - done - - echo -E " -}; - -int signals[] = { -" - for i in $signals - do - echo -E ' '$i, - done - - # Use -E because on some systems bash builtin interprets \n itself. - echo -E ' -}; - -static int -intcmp(const void *a, const void *b) -{ - return *(int*)a - *(int*)b; -} - -int -main(void) -{ - int i, j, e; - char buf[1024], *p; - - printf("\n\n// Error table\n"); - printf("var errors = [...]string {\n"); - qsort(errors, nelem(errors), sizeof errors[0], intcmp); - for(i=0; i 0 && errors[i-1] == e) - continue; - strcpy(buf, strerror(e)); - // lowercase first letter: Bad -> bad, but STREAM -> STREAM. - if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z) - buf[0] += a - A; - printf("\t%d: \"%s\",\n", e, buf); - } - printf("}\n\n"); - - printf("\n\n// Signal table\n"); - printf("var signals = [...]string {\n"); - qsort(signals, nelem(signals), sizeof signals[0], intcmp); - for(i=0; i 0 && signals[i-1] == e) - continue; - strcpy(buf, strsignal(e)); - // lowercase first letter: Bad -> bad, but STREAM -> STREAM. - if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z) - buf[0] += a - A; - // cut trailing : number. - p = strrchr(buf, ":"[0]); - if(p) - *p = '\0'; - printf("\t%d: \"%s\",\n", e, buf); - } - printf("}\n\n"); - - return 0; -} - -' -) >_errors.c - -$CC $ccflags -o _errors _errors.c && $GORUN ./_errors && rm -f _errors.c _errors _const.go _error.grep _signal.grep _error.out diff --git a/vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh b/vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh deleted file mode 100644 index 3c3ab05810e..00000000000 --- a/vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# 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 file. - -COMMAND="mksysnum_plan9.sh $@" - -cat <= 10 { - buf[i] = byte(val%10 + '0') - i-- - val /= 10 - } - buf[i] = byte(val + '0') - return string(buf[i:]) -} diff --git a/vendor/golang.org/x/sys/plan9/syscall.go b/vendor/golang.org/x/sys/plan9/syscall.go deleted file mode 100644 index d631fd664a7..00000000000 --- a/vendor/golang.org/x/sys/plan9/syscall.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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 file. - -//go:build plan9 - -// Package plan9 contains an interface to the low-level operating system -// primitives. OS details vary depending on the underlying system, and -// by default, godoc will display the OS-specific documentation for the current -// system. If you want godoc to display documentation for another -// system, set $GOOS and $GOARCH to the desired system. For example, if -// you want to view documentation for freebsd/arm on linux/amd64, set $GOOS -// to freebsd and $GOARCH to arm. -// -// The primary use of this package is inside other packages that provide a more -// portable interface to the system, such as "os", "time" and "net". Use -// those packages rather than this one if you can. -// -// For details of the functions and data types in this package consult -// the manuals for the appropriate operating system. -// -// These calls return err == nil to indicate success; otherwise -// err represents an operating system error describing the failure and -// holds a value of type syscall.ErrorString. -package plan9 // import "golang.org/x/sys/plan9" - -import ( - "bytes" - "strings" - "unsafe" -) - -// ByteSliceFromString returns a NUL-terminated slice of bytes -// containing the text of s. If s contains a NUL byte at any -// location, it returns (nil, EINVAL). -func ByteSliceFromString(s string) ([]byte, error) { - if strings.IndexByte(s, 0) != -1 { - return nil, EINVAL - } - a := make([]byte, len(s)+1) - copy(a, s) - return a, nil -} - -// BytePtrFromString returns a pointer to a NUL-terminated array of -// bytes containing the text of s. If s contains a NUL byte at any -// location, it returns (nil, EINVAL). -func BytePtrFromString(s string) (*byte, error) { - a, err := ByteSliceFromString(s) - if err != nil { - return nil, err - } - return &a[0], nil -} - -// ByteSliceToString returns a string form of the text represented by the slice s, with a terminating NUL and any -// bytes after the NUL removed. -func ByteSliceToString(s []byte) string { - if i := bytes.IndexByte(s, 0); i != -1 { - s = s[:i] - } - return string(s) -} - -// BytePtrToString takes a pointer to a sequence of text and returns the corresponding string. -// If the pointer is nil, it returns the empty string. It assumes that the text sequence is terminated -// at a zero byte; if the zero byte is not present, the program may crash. -func BytePtrToString(p *byte) string { - if p == nil { - return "" - } - if *p == 0 { - return "" - } - - // Find NUL terminator. - n := 0 - for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ { - ptr = unsafe.Pointer(uintptr(ptr) + 1) - } - - return string(unsafe.Slice(p, n)) -} - -// Single-word zero for use when we need a valid pointer to 0 bytes. -// See mksyscall.pl. -var _zero uintptr - -func (ts *Timespec) Unix() (sec int64, nsec int64) { - return int64(ts.Sec), int64(ts.Nsec) -} - -func (tv *Timeval) Unix() (sec int64, nsec int64) { - return int64(tv.Sec), int64(tv.Usec) * 1000 -} - -func (ts *Timespec) Nano() int64 { - return int64(ts.Sec)*1e9 + int64(ts.Nsec) -} - -func (tv *Timeval) Nano() int64 { - return int64(tv.Sec)*1e9 + int64(tv.Usec)*1000 -} - -// use is a no-op, but the compiler cannot see that it is. -// Calling use(p) ensures that p is kept live until that point. -// -//go:noescape -func use(p unsafe.Pointer) diff --git a/vendor/golang.org/x/sys/plan9/syscall_plan9.go b/vendor/golang.org/x/sys/plan9/syscall_plan9.go deleted file mode 100644 index 761912237fa..00000000000 --- a/vendor/golang.org/x/sys/plan9/syscall_plan9.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2011 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 file. - -// Plan 9 system calls. -// This file is compiled as ordinary Go code, -// but it is also input to mksyscall, -// which parses the //sys lines and generates system call stubs. -// Note that sometimes we use a lowercase //sys name and -// wrap it in our own nicer implementation. - -package plan9 - -import ( - "bytes" - "syscall" - "unsafe" -) - -// A Note is a string describing a process note. -// It implements the os.Signal interface. -type Note = syscall.Note - -var ( - Stdin = 0 - Stdout = 1 - Stderr = 2 -) - -// For testing: clients can set this flag to force -// creation of IPv6 sockets to return EAFNOSUPPORT. -var SocketDisableIPv6 bool - -func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.ErrorString) -func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.ErrorString) -func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr) -func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) - -func atoi(b []byte) (n uint) { - n = 0 - for i := 0; i < len(b); i++ { - n = n*10 + uint(b[i]-'0') - } - return -} - -func cstring(s []byte) string { - i := bytes.IndexByte(s, 0) - if i == -1 { - i = len(s) - } - return string(s[:i]) -} - -func errstr() string { - var buf [ERRMAX]byte - - RawSyscall(SYS_ERRSTR, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0) - - buf[len(buf)-1] = 0 - return cstring(buf[:]) -} - -// Implemented in assembly to import from runtime. -func exit(code int) - -func Exit(code int) { exit(code) } - -func readnum(path string) (uint, error) { - var b [12]byte - - fd, e := Open(path, O_RDONLY) - if e != nil { - return 0, e - } - defer Close(fd) - - n, e := Pread(fd, b[:], 0) - - if e != nil { - return 0, e - } - - m := 0 - for ; m < n && b[m] == ' '; m++ { - } - - return atoi(b[m : n-1]), nil -} - -func Getpid() (pid int) { - n, _ := readnum("#c/pid") - return int(n) -} - -func Getppid() (ppid int) { - n, _ := readnum("#c/ppid") - return int(n) -} - -func Read(fd int, p []byte) (n int, err error) { - return Pread(fd, p, -1) -} - -func Write(fd int, p []byte) (n int, err error) { - return Pwrite(fd, p, -1) -} - -var ioSync int64 - -//sys fd2path(fd int, buf []byte) (err error) - -func Fd2path(fd int) (path string, err error) { - var buf [512]byte - - e := fd2path(fd, buf[:]) - if e != nil { - return "", e - } - return cstring(buf[:]), nil -} - -//sys pipe(p *[2]int32) (err error) - -func Pipe(p []int) (err error) { - if len(p) != 2 { - return syscall.ErrorString("bad arg in system call") - } - var pp [2]int32 - err = pipe(&pp) - if err == nil { - p[0] = int(pp[0]) - p[1] = int(pp[1]) - } - return -} - -// Underlying system call writes to newoffset via pointer. -// Implemented in assembly to avoid allocation. -func seek(placeholder uintptr, fd int, offset int64, whence int) (newoffset int64, err string) - -func Seek(fd int, offset int64, whence int) (newoffset int64, err error) { - newoffset, e := seek(0, fd, offset, whence) - - if newoffset == -1 { - err = syscall.ErrorString(e) - } - return -} - -func Mkdir(path string, mode uint32) (err error) { - fd, err := Create(path, O_RDONLY, DMDIR|mode) - - if fd != -1 { - Close(fd) - } - - return -} - -type Waitmsg struct { - Pid int - Time [3]uint32 - Msg string -} - -func (w Waitmsg) Exited() bool { return true } -func (w Waitmsg) Signaled() bool { return false } - -func (w Waitmsg) ExitStatus() int { - if len(w.Msg) == 0 { - // a normal exit returns no message - return 0 - } - return 1 -} - -//sys await(s []byte) (n int, err error) - -func Await(w *Waitmsg) (err error) { - var buf [512]byte - var f [5][]byte - - n, err := await(buf[:]) - - if err != nil || w == nil { - return - } - - nf := 0 - p := 0 - for i := 0; i < n && nf < len(f)-1; i++ { - if buf[i] == ' ' { - f[nf] = buf[p:i] - p = i + 1 - nf++ - } - } - f[nf] = buf[p:] - nf++ - - if nf != len(f) { - return syscall.ErrorString("invalid wait message") - } - w.Pid = int(atoi(f[0])) - w.Time[0] = uint32(atoi(f[1])) - w.Time[1] = uint32(atoi(f[2])) - w.Time[2] = uint32(atoi(f[3])) - w.Msg = cstring(f[4]) - if w.Msg == "''" { - // await() returns '' for no error - w.Msg = "" - } - return -} - -func Unmount(name, old string) (err error) { - fixwd() - oldp, err := BytePtrFromString(old) - if err != nil { - return err - } - oldptr := uintptr(unsafe.Pointer(oldp)) - - var r0 uintptr - var e syscall.ErrorString - - // bind(2) man page: If name is zero, everything bound or mounted upon old is unbound or unmounted. - if name == "" { - r0, _, e = Syscall(SYS_UNMOUNT, _zero, oldptr, 0) - } else { - namep, err := BytePtrFromString(name) - if err != nil { - return err - } - r0, _, e = Syscall(SYS_UNMOUNT, uintptr(unsafe.Pointer(namep)), oldptr, 0) - } - - if int32(r0) == -1 { - err = e - } - return -} - -func Fchdir(fd int) (err error) { - path, err := Fd2path(fd) - - if err != nil { - return - } - - return Chdir(path) -} - -type Timespec struct { - Sec int32 - Nsec int32 -} - -type Timeval struct { - Sec int32 - Usec int32 -} - -func NsecToTimeval(nsec int64) (tv Timeval) { - nsec += 999 // round up to microsecond - tv.Usec = int32(nsec % 1e9 / 1e3) - tv.Sec = int32(nsec / 1e9) - return -} - -func nsec() int64 { - var scratch int64 - - r0, _, _ := Syscall(SYS_NSEC, uintptr(unsafe.Pointer(&scratch)), 0, 0) - // TODO(aram): remove hack after I fix _nsec in the pc64 kernel. - if r0 == 0 { - return scratch - } - return int64(r0) -} - -func Gettimeofday(tv *Timeval) error { - nsec := nsec() - *tv = NsecToTimeval(nsec) - return nil -} - -func Getpagesize() int { return 0x1000 } - -func Getegid() (egid int) { return -1 } -func Geteuid() (euid int) { return -1 } -func Getgid() (gid int) { return -1 } -func Getuid() (uid int) { return -1 } - -func Getgroups() (gids []int, err error) { - return make([]int, 0), nil -} - -//sys open(path string, mode int) (fd int, err error) - -func Open(path string, mode int) (fd int, err error) { - fixwd() - return open(path, mode) -} - -//sys create(path string, mode int, perm uint32) (fd int, err error) - -func Create(path string, mode int, perm uint32) (fd int, err error) { - fixwd() - return create(path, mode, perm) -} - -//sys remove(path string) (err error) - -func Remove(path string) error { - fixwd() - return remove(path) -} - -//sys stat(path string, edir []byte) (n int, err error) - -func Stat(path string, edir []byte) (n int, err error) { - fixwd() - return stat(path, edir) -} - -//sys bind(name string, old string, flag int) (err error) - -func Bind(name string, old string, flag int) (err error) { - fixwd() - return bind(name, old, flag) -} - -//sys mount(fd int, afd int, old string, flag int, aname string) (err error) - -func Mount(fd int, afd int, old string, flag int, aname string) (err error) { - fixwd() - return mount(fd, afd, old, flag, aname) -} - -//sys wstat(path string, edir []byte) (err error) - -func Wstat(path string, edir []byte) (err error) { - fixwd() - return wstat(path, edir) -} - -//sys chdir(path string) (err error) -//sys Dup(oldfd int, newfd int) (fd int, err error) -//sys Pread(fd int, p []byte, offset int64) (n int, err error) -//sys Pwrite(fd int, p []byte, offset int64) (n int, err error) -//sys Close(fd int) (err error) -//sys Fstat(fd int, edir []byte) (n int, err error) -//sys Fwstat(fd int, edir []byte) (err error) diff --git a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go b/vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go deleted file mode 100644 index f780d5c8079..00000000000 --- a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go +++ /dev/null @@ -1,284 +0,0 @@ -// go run mksyscall.go -l32 -plan9 -tags plan9,386 syscall_plan9.go -// Code generated by the command above; see README.md. DO NOT EDIT. - -//go:build plan9 && 386 - -package plan9 - -import "unsafe" - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func fd2path(fd int, buf []byte) (err error) { - var _p0 unsafe.Pointer - if len(buf) > 0 { - _p0 = unsafe.Pointer(&buf[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func pipe(p *[2]int32) (err error) { - r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func await(s []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(s) > 0 { - _p0 = unsafe.Pointer(&s[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func open(path string, mode int) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func create(path string, mode int, perm uint32) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm)) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func remove(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func stat(path string, edir []byte) (n int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func bind(name string, old string, flag int) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(name) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(old) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag)) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func mount(fd int, afd int, old string, flag int, aname string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(old) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(aname) - if err != nil { - return - } - r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func wstat(path string, edir []byte) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func chdir(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Dup(oldfd int, newfd int) (fd int, err error) { - r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pread(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pwrite(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Close(fd int) (err error) { - r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fstat(fd int, edir []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fwstat(fd int, edir []byte) (err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} diff --git a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go b/vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go deleted file mode 100644 index 7de61065f65..00000000000 --- a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go +++ /dev/null @@ -1,284 +0,0 @@ -// go run mksyscall.go -l32 -plan9 -tags plan9,amd64 syscall_plan9.go -// Code generated by the command above; see README.md. DO NOT EDIT. - -//go:build plan9 && amd64 - -package plan9 - -import "unsafe" - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func fd2path(fd int, buf []byte) (err error) { - var _p0 unsafe.Pointer - if len(buf) > 0 { - _p0 = unsafe.Pointer(&buf[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func pipe(p *[2]int32) (err error) { - r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func await(s []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(s) > 0 { - _p0 = unsafe.Pointer(&s[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func open(path string, mode int) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func create(path string, mode int, perm uint32) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm)) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func remove(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func stat(path string, edir []byte) (n int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func bind(name string, old string, flag int) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(name) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(old) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag)) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func mount(fd int, afd int, old string, flag int, aname string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(old) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(aname) - if err != nil { - return - } - r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func wstat(path string, edir []byte) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func chdir(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Dup(oldfd int, newfd int) (fd int, err error) { - r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pread(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pwrite(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Close(fd int) (err error) { - r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fstat(fd int, edir []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fwstat(fd int, edir []byte) (err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} diff --git a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go b/vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go deleted file mode 100644 index ea85780f03e..00000000000 --- a/vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go +++ /dev/null @@ -1,284 +0,0 @@ -// go run mksyscall.go -l32 -plan9 -tags plan9,arm syscall_plan9.go -// Code generated by the command above; see README.md. DO NOT EDIT. - -//go:build plan9 && arm - -package plan9 - -import "unsafe" - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func fd2path(fd int, buf []byte) (err error) { - var _p0 unsafe.Pointer - if len(buf) > 0 { - _p0 = unsafe.Pointer(&buf[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func pipe(p *[2]int32) (err error) { - r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func await(s []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(s) > 0 { - _p0 = unsafe.Pointer(&s[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func open(path string, mode int) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func create(path string, mode int, perm uint32) (fd int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm)) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func remove(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func stat(path string, edir []byte) (n int, err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func bind(name string, old string, flag int) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(name) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(old) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag)) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func mount(fd int, afd int, old string, flag int, aname string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(old) - if err != nil { - return - } - var _p1 *byte - _p1, err = BytePtrFromString(aname) - if err != nil { - return - } - r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func wstat(path string, edir []byte) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - var _p1 unsafe.Pointer - if len(edir) > 0 { - _p1 = unsafe.Pointer(&edir[0]) - } else { - _p1 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func chdir(path string) (err error) { - var _p0 *byte - _p0, err = BytePtrFromString(path) - if err != nil { - return - } - r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Dup(oldfd int, newfd int) (fd int, err error) { - r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0) - fd = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pread(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Pwrite(fd int, p []byte, offset int64) (n int, err error) { - var _p0 unsafe.Pointer - if len(p) > 0 { - _p0 = unsafe.Pointer(&p[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Close(fd int) (err error) { - r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fstat(fd int, edir []byte) (n int, err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - n = int(r0) - if int32(r0) == -1 { - err = e1 - } - return -} - -// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT - -func Fwstat(fd int, edir []byte) (err error) { - var _p0 unsafe.Pointer - if len(edir) > 0 { - _p0 = unsafe.Pointer(&edir[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir))) - if int32(r0) == -1 { - err = e1 - } - return -} diff --git a/vendor/golang.org/x/sys/plan9/zsysnum_plan9.go b/vendor/golang.org/x/sys/plan9/zsysnum_plan9.go deleted file mode 100644 index 22e8abd43d8..00000000000 --- a/vendor/golang.org/x/sys/plan9/zsysnum_plan9.go +++ /dev/null @@ -1,49 +0,0 @@ -// mksysnum_plan9.sh /opt/plan9/sys/src/libc/9syscall/sys.h -// MACHINE GENERATED BY THE ABOVE COMMAND; DO NOT EDIT - -package plan9 - -const ( - SYS_SYSR1 = 0 - SYS_BIND = 2 - SYS_CHDIR = 3 - SYS_CLOSE = 4 - SYS_DUP = 5 - SYS_ALARM = 6 - SYS_EXEC = 7 - SYS_EXITS = 8 - SYS_FAUTH = 10 - SYS_SEGBRK = 12 - SYS_OPEN = 14 - SYS_OSEEK = 16 - SYS_SLEEP = 17 - SYS_RFORK = 19 - SYS_PIPE = 21 - SYS_CREATE = 22 - SYS_FD2PATH = 23 - SYS_BRK_ = 24 - SYS_REMOVE = 25 - SYS_NOTIFY = 28 - SYS_NOTED = 29 - SYS_SEGATTACH = 30 - SYS_SEGDETACH = 31 - SYS_SEGFREE = 32 - SYS_SEGFLUSH = 33 - SYS_RENDEZVOUS = 34 - SYS_UNMOUNT = 35 - SYS_SEMACQUIRE = 37 - SYS_SEMRELEASE = 38 - SYS_SEEK = 39 - SYS_FVERSION = 40 - SYS_ERRSTR = 41 - SYS_STAT = 42 - SYS_FSTAT = 43 - SYS_WSTAT = 44 - SYS_FWSTAT = 45 - SYS_MOUNT = 46 - SYS_AWAIT = 47 - SYS_PREAD = 50 - SYS_PWRITE = 51 - SYS_TSEMACQUIRE = 52 - SYS_NSEC = 53 -) diff --git a/vendor/golang.org/x/term/CONTRIBUTING.md b/vendor/golang.org/x/term/CONTRIBUTING.md deleted file mode 100644 index d0485e887a2..00000000000 --- a/vendor/golang.org/x/term/CONTRIBUTING.md +++ /dev/null @@ -1,26 +0,0 @@ -# Contributing to Go - -Go is an open source project. - -It is the work of hundreds of contributors. We appreciate your help! - -## Filing issues - -When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions: - -1. What version of Go are you using (`go version`)? -2. What operating system and processor architecture are you using? -3. What did you do? -4. What did you expect to see? -5. What did you see instead? - -General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. -The gophers there will answer or ask you to file an issue if you've tripped over a bug. - -## Contributing code - -Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) -before sending patches. - -Unless otherwise noted, the Go source files are distributed under -the BSD-style license found in the LICENSE file. diff --git a/vendor/golang.org/x/term/LICENSE b/vendor/golang.org/x/term/LICENSE deleted file mode 100644 index 2a7cf70da6e..00000000000 --- a/vendor/golang.org/x/term/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright 2009 The Go Authors. - -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 LLC 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. diff --git a/vendor/golang.org/x/term/PATENTS b/vendor/golang.org/x/term/PATENTS deleted file mode 100644 index 733099041f8..00000000000 --- a/vendor/golang.org/x/term/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the Go project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of Go, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of Go. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of Go or any code incorporated within this -implementation of Go constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of Go -shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/term/README.md b/vendor/golang.org/x/term/README.md deleted file mode 100644 index 05ff623f94f..00000000000 --- a/vendor/golang.org/x/term/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Go terminal/console support - -[![Go Reference](https://pkg.go.dev/badge/golang.org/x/term.svg)](https://pkg.go.dev/golang.org/x/term) - -This repository provides Go terminal and console support packages. - -## Report Issues / Send Patches - -This repository uses Gerrit for code changes. To learn how to submit changes to -this repository, see https://go.dev/doc/contribute. - -The git repository is https://go.googlesource.com/term. - -The main issue tracker for the term repository is located at -https://go.dev/issues. Prefix your issue with "x/term:" in the -subject line, so it is easy to find. diff --git a/vendor/golang.org/x/term/codereview.cfg b/vendor/golang.org/x/term/codereview.cfg deleted file mode 100644 index 3f8b14b64e8..00000000000 --- a/vendor/golang.org/x/term/codereview.cfg +++ /dev/null @@ -1 +0,0 @@ -issuerepo: golang/go diff --git a/vendor/golang.org/x/term/term.go b/vendor/golang.org/x/term/term.go deleted file mode 100644 index 1a40d101256..00000000000 --- a/vendor/golang.org/x/term/term.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2019 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 file. - -// Package term provides support functions for dealing with terminals, as -// commonly found on UNIX systems. -// -// Putting a terminal into raw mode is the most common requirement: -// -// oldState, err := term.MakeRaw(int(os.Stdin.Fd())) -// if err != nil { -// panic(err) -// } -// defer term.Restore(int(os.Stdin.Fd()), oldState) -// -// Note that on non-Unix systems os.Stdin.Fd() may not be 0. -package term - -// State contains the state of a terminal. -type State struct { - state -} - -// IsTerminal returns whether the given file descriptor is a terminal. -func IsTerminal(fd int) bool { - return isTerminal(fd) -} - -// MakeRaw puts the terminal connected to the given file descriptor into raw -// mode and returns the previous state of the terminal so that it can be -// restored. -func MakeRaw(fd int) (*State, error) { - return makeRaw(fd) -} - -// GetState returns the current state of a terminal which may be useful to -// restore the terminal after a signal. -func GetState(fd int) (*State, error) { - return getState(fd) -} - -// Restore restores the terminal connected to the given file descriptor to a -// previous state. -func Restore(fd int, oldState *State) error { - return restore(fd, oldState) -} - -// GetSize returns the visible dimensions of the given terminal. -// -// These dimensions don't include any scrollback buffer height. -func GetSize(fd int) (width, height int, err error) { - return getSize(fd) -} - -// ReadPassword reads a line of input from a terminal without local echo. This -// is commonly used for inputting passwords and other sensitive data. The slice -// returned does not include the \n. -func ReadPassword(fd int) ([]byte, error) { - return readPassword(fd) -} diff --git a/vendor/golang.org/x/term/term_plan9.go b/vendor/golang.org/x/term/term_plan9.go deleted file mode 100644 index 21afa55cdb8..00000000000 --- a/vendor/golang.org/x/term/term_plan9.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2019 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 file. - -package term - -import ( - "fmt" - "runtime" - - "golang.org/x/sys/plan9" -) - -type state struct{} - -func isTerminal(fd int) bool { - path, err := plan9.Fd2path(fd) - if err != nil { - return false - } - return path == "/dev/cons" || path == "/mnt/term/dev/cons" -} - -func makeRaw(fd int) (*State, error) { - return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func getState(fd int) (*State, error) { - return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func restore(fd int, state *State) error { - return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func getSize(fd int) (width, height int, err error) { - return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func readPassword(fd int) ([]byte, error) { - return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} diff --git a/vendor/golang.org/x/term/term_unix.go b/vendor/golang.org/x/term/term_unix.go deleted file mode 100644 index 1ad0ddfe30d..00000000000 --- a/vendor/golang.org/x/term/term_unix.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2019 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 file. - -//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos - -package term - -import ( - "golang.org/x/sys/unix" -) - -type state struct { - termios unix.Termios -} - -func isTerminal(fd int) bool { - _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - return err == nil -} - -func makeRaw(fd int) (*State, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - oldState := State{state{termios: *termios}} - - // This attempts to replicate the behaviour documented for cfmakeraw in - // the termios(3) manpage. - termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON - termios.Oflag &^= unix.OPOST - termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN - termios.Cflag &^= unix.CSIZE | unix.PARENB - termios.Cflag |= unix.CS8 - termios.Cc[unix.VMIN] = 1 - termios.Cc[unix.VTIME] = 0 - if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { - return nil, err - } - - return &oldState, nil -} - -func getState(fd int) (*State, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - return &State{state{termios: *termios}}, nil -} - -func restore(fd int, state *State) error { - return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) -} - -func getSize(fd int) (width, height int, err error) { - ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) - if err != nil { - return 0, 0, err - } - return int(ws.Col), int(ws.Row), nil -} - -// passwordReader is an io.Reader that reads from a specific file descriptor. -type passwordReader int - -func (r passwordReader) Read(buf []byte) (int, error) { - return unix.Read(int(r), buf) -} - -func readPassword(fd int) ([]byte, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - newState := *termios - newState.Lflag &^= unix.ECHO - newState.Lflag |= unix.ICANON | unix.ISIG - newState.Iflag |= unix.ICRNL - if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { - return nil, err - } - - defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) - - return readPasswordLine(passwordReader(fd)) -} diff --git a/vendor/golang.org/x/term/term_unix_bsd.go b/vendor/golang.org/x/term/term_unix_bsd.go deleted file mode 100644 index 9dbf546298d..00000000000 --- a/vendor/golang.org/x/term/term_unix_bsd.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2013 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 file. - -//go:build darwin || dragonfly || freebsd || netbsd || openbsd - -package term - -import "golang.org/x/sys/unix" - -const ioctlReadTermios = unix.TIOCGETA -const ioctlWriteTermios = unix.TIOCSETA diff --git a/vendor/golang.org/x/term/term_unix_other.go b/vendor/golang.org/x/term/term_unix_other.go deleted file mode 100644 index 1b36de799a2..00000000000 --- a/vendor/golang.org/x/term/term_unix_other.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2021 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 file. - -//go:build aix || linux || solaris || zos - -package term - -import "golang.org/x/sys/unix" - -const ioctlReadTermios = unix.TCGETS -const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/term/term_unsupported.go b/vendor/golang.org/x/term/term_unsupported.go deleted file mode 100644 index 3c409e5885e..00000000000 --- a/vendor/golang.org/x/term/term_unsupported.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 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 file. - -//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9 - -package term - -import ( - "fmt" - "runtime" -) - -type state struct{} - -func isTerminal(fd int) bool { - return false -} - -func makeRaw(fd int) (*State, error) { - return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func getState(fd int) (*State, error) { - return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func restore(fd int, state *State) error { - return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func getSize(fd int) (width, height int, err error) { - return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} - -func readPassword(fd int) ([]byte, error) { - return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) -} diff --git a/vendor/golang.org/x/term/term_windows.go b/vendor/golang.org/x/term/term_windows.go deleted file mode 100644 index 0ddd81c02a6..00000000000 --- a/vendor/golang.org/x/term/term_windows.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 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 file. - -package term - -import ( - "os" - - "golang.org/x/sys/windows" -) - -type state struct { - mode uint32 -} - -func isTerminal(fd int) bool { - var st uint32 - err := windows.GetConsoleMode(windows.Handle(fd), &st) - return err == nil -} - -// This is intended to be used on a console input handle. -// See https://learn.microsoft.com/en-us/windows/console/setconsolemode -func makeRaw(fd int) (*State, error) { - var st uint32 - if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { - return nil, err - } - raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT) - raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT - if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { - return nil, err - } - return &State{state{st}}, nil -} - -func getState(fd int) (*State, error) { - var st uint32 - if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { - return nil, err - } - return &State{state{st}}, nil -} - -func restore(fd int, state *State) error { - return windows.SetConsoleMode(windows.Handle(fd), state.mode) -} - -func getSize(fd int) (width, height int, err error) { - var info windows.ConsoleScreenBufferInfo - if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { - return 0, 0, err - } - return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil -} - -func readPassword(fd int) ([]byte, error) { - var st uint32 - if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { - return nil, err - } - old := st - - st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) - st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) - if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { - return nil, err - } - - defer windows.SetConsoleMode(windows.Handle(fd), old) - - var h windows.Handle - p, _ := windows.GetCurrentProcess() - if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { - return nil, err - } - - f := os.NewFile(uintptr(h), "stdin") - defer f.Close() - return readPasswordLine(f) -} diff --git a/vendor/golang.org/x/term/terminal.go b/vendor/golang.org/x/term/terminal.go deleted file mode 100644 index 6ec537cdc1a..00000000000 --- a/vendor/golang.org/x/term/terminal.go +++ /dev/null @@ -1,1074 +0,0 @@ -// Copyright 2011 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 file. - -package term - -import ( - "bytes" - "fmt" - "io" - "runtime" - "strconv" - "sync" - "unicode/utf8" -) - -// EscapeCodes contains escape sequences that can be written to the terminal in -// order to achieve different styles of text. -type EscapeCodes struct { - // Foreground colors - Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte - - // Reset all attributes - Reset []byte -} - -var vt100EscapeCodes = EscapeCodes{ - Black: []byte{keyEscape, '[', '3', '0', 'm'}, - Red: []byte{keyEscape, '[', '3', '1', 'm'}, - Green: []byte{keyEscape, '[', '3', '2', 'm'}, - Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, - Blue: []byte{keyEscape, '[', '3', '4', 'm'}, - Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, - Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, - White: []byte{keyEscape, '[', '3', '7', 'm'}, - - Reset: []byte{keyEscape, '[', '0', 'm'}, -} - -// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine]. -type History interface { - // Add will be called by [Terminal.ReadLine] to add - // a new, most recent entry to the history. - // It is allowed to drop any entry, including - // the entry being added (e.g., if it's deemed an invalid entry), - // the least-recent entry (e.g., to keep the history bounded), - // or any other entry. - Add(entry string) - - // Len returns the number of entries in the history. - Len() int - - // At returns an entry from the history. - // Index 0 is the most-recently added entry and - // index Len()-1 is the least-recently added entry. - // If index is < 0 or >= Len(), it panics. - At(idx int) string -} - -// Terminal contains the state for running a VT100 terminal that is capable of -// reading lines of input. -type Terminal struct { - // AutoCompleteCallback, if non-null, is called for each keypress with - // the full input line and the current position of the cursor (in - // bytes, as an index into |line|). If it returns ok=false, the key - // press is processed normally. Otherwise it returns a replacement line - // and the new cursor position. - // - // This will be disabled during ReadPassword. - AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) - - // Escape contains a pointer to the escape codes for this terminal. - // It's always a valid pointer, although the escape codes themselves - // may be empty if the terminal doesn't support them. - Escape *EscapeCodes - - // lock protects the terminal and the state in this object from - // concurrent processing of a key press and a Write() call. - lock sync.Mutex - - c io.ReadWriter - prompt []rune - - // line is the current line being entered. - line []rune - // pos is the logical position of the cursor in line - pos int - // echo is true if local echo is enabled - echo bool - // pasteActive is true iff there is a bracketed paste operation in - // progress. - pasteActive bool - - // cursorX contains the current X value of the cursor where the left - // edge is 0. cursorY contains the row number where the first row of - // the current line is 0. - cursorX, cursorY int - // maxLine is the greatest value of cursorY so far. - maxLine int - - termWidth, termHeight int - - // outBuf contains the terminal data to be sent. - outBuf []byte - // remainder contains the remainder of any partial key sequences after - // a read. It aliases into inBuf. - remainder []byte - inBuf [256]byte - - // History records and retrieves lines of input read by [ReadLine] which - // a user can retrieve and navigate using the up and down arrow keys. - // - // It is not safe to call ReadLine concurrently with any methods on History. - // - // [NewTerminal] sets this to a default implementation that records the - // last 100 lines of input. - History History - // historyIndex stores the currently accessed history entry, where zero - // means the immediately previous entry. - historyIndex int - // When navigating up and down the history it's possible to return to - // the incomplete, initial line. That value is stored in - // historyPending. - historyPending string -} - -// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is -// a local terminal, that terminal must first have been put into raw mode. -// prompt is a string that is written at the start of each input line (i.e. -// "> "). -func NewTerminal(c io.ReadWriter, prompt string) *Terminal { - return &Terminal{ - Escape: &vt100EscapeCodes, - c: c, - prompt: []rune(prompt), - termWidth: 80, - termHeight: 24, - echo: true, - historyIndex: -1, - History: &stRingBuffer{}, - } -} - -const ( - keyCtrlC = 3 - keyCtrlD = 4 - keyCtrlU = 21 - keyEnter = '\r' - keyLF = '\n' - keyEscape = 27 - keyBackspace = 127 - keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota - keyUp - keyDown - keyLeft - keyRight - keyAltLeft - keyAltRight - keyHome - keyEnd - keyDeleteWord - keyDeleteLine - keyDelete - keyClearScreen - keyTranspose - keyPasteStart - keyPasteEnd -) - -var ( - crlf = []byte{'\r', '\n'} - pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} - pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} -) - -// bytesToKey tries to parse a key sequence from b. If successful, it returns -// the key and the remainder of the input. Otherwise it returns utf8.RuneError. -func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { - if len(b) == 0 { - return utf8.RuneError, nil - } - - if !pasteActive { - switch b[0] { - case 1: // ^A - return keyHome, b[1:] - case 2: // ^B - return keyLeft, b[1:] - case 5: // ^E - return keyEnd, b[1:] - case 6: // ^F - return keyRight, b[1:] - case 8: // ^H - return keyBackspace, b[1:] - case 11: // ^K - return keyDeleteLine, b[1:] - case 12: // ^L - return keyClearScreen, b[1:] - case 20: // ^T - return keyTranspose, b[1:] - case 23: // ^W - return keyDeleteWord, b[1:] - case 14: // ^N - return keyDown, b[1:] - case 16: // ^P - return keyUp, b[1:] - } - } - - if b[0] != keyEscape { - if !utf8.FullRune(b) { - return utf8.RuneError, b - } - r, l := utf8.DecodeRune(b) - return r, b[l:] - } - - if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { - switch b[2] { - case 'A': - return keyUp, b[3:] - case 'B': - return keyDown, b[3:] - case 'C': - return keyRight, b[3:] - case 'D': - return keyLeft, b[3:] - case 'H': - return keyHome, b[3:] - case 'F': - return keyEnd, b[3:] - } - } - - if !pasteActive && len(b) >= 4 && b[0] == keyEscape && b[1] == '[' && b[2] == '3' && b[3] == '~' { - return keyDelete, b[4:] - } - - if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { - switch b[5] { - case 'C': - return keyAltRight, b[6:] - case 'D': - return keyAltLeft, b[6:] - } - } - - if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { - return keyPasteStart, b[6:] - } - - if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { - return keyPasteEnd, b[6:] - } - - // If we get here then we have a key that we don't recognise, or a - // partial sequence. It's not clear how one should find the end of a - // sequence without knowing them all, but it seems that [a-zA-Z~] only - // appears at the end of a sequence. - for i, c := range b[0:] { - if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { - return keyUnknown, b[i+1:] - } - } - - return utf8.RuneError, b -} - -// queue appends data to the end of t.outBuf -func (t *Terminal) queue(data []rune) { - t.outBuf = append(t.outBuf, []byte(string(data))...) -} - -var space = []rune{' '} - -func isPrintable(key rune) bool { - isInSurrogateArea := key >= 0xd800 && key <= 0xdbff - return key >= 32 && !isInSurrogateArea -} - -// moveCursorToPos appends data to t.outBuf which will move the cursor to the -// given, logical position in the text. -func (t *Terminal) moveCursorToPos(pos int) { - if !t.echo { - return - } - - x := visualLength(t.prompt) + pos - y := x / t.termWidth - x = x % t.termWidth - - up := 0 - if y < t.cursorY { - up = t.cursorY - y - } - - down := 0 - if y > t.cursorY { - down = y - t.cursorY - } - - left := 0 - if x < t.cursorX { - left = t.cursorX - x - } - - right := 0 - if x > t.cursorX { - right = x - t.cursorX - } - - t.cursorX = x - t.cursorY = y - t.move(up, down, left, right) -} - -func (t *Terminal) move(up, down, left, right int) { - m := []rune{} - - // 1 unit up can be expressed as ^[[A or ^[A - // 5 units up can be expressed as ^[[5A - - if up == 1 { - m = append(m, keyEscape, '[', 'A') - } else if up > 1 { - m = append(m, keyEscape, '[') - m = append(m, []rune(strconv.Itoa(up))...) - m = append(m, 'A') - } - - if down == 1 { - m = append(m, keyEscape, '[', 'B') - } else if down > 1 { - m = append(m, keyEscape, '[') - m = append(m, []rune(strconv.Itoa(down))...) - m = append(m, 'B') - } - - if right == 1 { - m = append(m, keyEscape, '[', 'C') - } else if right > 1 { - m = append(m, keyEscape, '[') - m = append(m, []rune(strconv.Itoa(right))...) - m = append(m, 'C') - } - - if left == 1 { - m = append(m, keyEscape, '[', 'D') - } else if left > 1 { - m = append(m, keyEscape, '[') - m = append(m, []rune(strconv.Itoa(left))...) - m = append(m, 'D') - } - - t.queue(m) -} - -func (t *Terminal) clearLineToRight() { - op := []rune{keyEscape, '[', 'K'} - t.queue(op) -} - -const maxLineLength = 4096 - -func (t *Terminal) setLine(newLine []rune, newPos int) { - if t.echo { - t.moveCursorToPos(0) - t.writeLine(newLine) - for i := len(newLine); i < len(t.line); i++ { - t.writeLine(space) - } - t.moveCursorToPos(newPos) - } - t.line = newLine - t.pos = newPos -} - -func (t *Terminal) advanceCursor(places int) { - t.cursorX += places - t.cursorY += t.cursorX / t.termWidth - if t.cursorY > t.maxLine { - t.maxLine = t.cursorY - } - t.cursorX = t.cursorX % t.termWidth - - if places > 0 && t.cursorX == 0 { - // Normally terminals will advance the current position - // when writing a character. But that doesn't happen - // for the last character in a line. However, when - // writing a character (except a new line) that causes - // a line wrap, the position will be advanced two - // places. - // - // So, if we are stopping at the end of a line, we - // need to write a newline so that our cursor can be - // advanced to the next line. - t.outBuf = append(t.outBuf, '\r', '\n') - } -} - -func (t *Terminal) eraseNPreviousChars(n int) { - if n == 0 { - return - } - - if t.pos < n { - n = t.pos - } - t.pos -= n - t.moveCursorToPos(t.pos) - - copy(t.line[t.pos:], t.line[n+t.pos:]) - t.line = t.line[:len(t.line)-n] - if t.echo { - t.writeLine(t.line[t.pos:]) - for i := 0; i < n; i++ { - t.queue(space) - } - t.advanceCursor(n) - t.moveCursorToPos(t.pos) - } -} - -// countToLeftWord returns the number of characters from the cursor to the -// start of the previous word. -func (t *Terminal) countToLeftWord() int { - if t.pos == 0 { - return 0 - } - - pos := t.pos - 1 - for pos > 0 { - if t.line[pos] != ' ' { - break - } - pos-- - } - for pos > 0 { - if t.line[pos] == ' ' { - pos++ - break - } - pos-- - } - - return t.pos - pos -} - -// countToRightWord returns the number of characters from the cursor to the -// start of the next word. -func (t *Terminal) countToRightWord() int { - pos := t.pos - for pos < len(t.line) { - if t.line[pos] == ' ' { - break - } - pos++ - } - for pos < len(t.line) { - if t.line[pos] != ' ' { - break - } - pos++ - } - return pos - t.pos -} - -// visualLength returns the number of visible glyphs in s. -func visualLength(runes []rune) int { - inEscapeSeq := false - length := 0 - - for _, r := range runes { - switch { - case inEscapeSeq: - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { - inEscapeSeq = false - } - case r == '\x1b': - inEscapeSeq = true - default: - length++ - } - } - - return length -} - -// historyAt unlocks the terminal and relocks it while calling History.At. -func (t *Terminal) historyAt(idx int) (string, bool) { - t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer. - defer t.lock.Lock() // panic in At (or Len) protection. - if idx < 0 || idx >= t.History.Len() { - return "", false - } - return t.History.At(idx), true -} - -// historyAdd unlocks the terminal and relocks it while calling History.Add. -func (t *Terminal) historyAdd(entry string) { - t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer. - defer t.lock.Lock() // panic in Add protection. - t.History.Add(entry) -} - -// handleKey processes the given key and, optionally, returns a line of text -// that the user has entered. -func (t *Terminal) handleKey(key rune) (line string, ok bool) { - if t.pasteActive && key != keyEnter && key != keyLF { - t.addKeyToLine(key) - return - } - - switch key { - case keyBackspace: - if t.pos == 0 { - return - } - t.eraseNPreviousChars(1) - case keyAltLeft: - // move left by a word. - t.pos -= t.countToLeftWord() - t.moveCursorToPos(t.pos) - case keyAltRight: - // move right by a word. - t.pos += t.countToRightWord() - t.moveCursorToPos(t.pos) - case keyLeft: - if t.pos == 0 { - return - } - t.pos-- - t.moveCursorToPos(t.pos) - case keyRight: - if t.pos == len(t.line) { - return - } - t.pos++ - t.moveCursorToPos(t.pos) - case keyHome: - if t.pos == 0 { - return - } - t.pos = 0 - t.moveCursorToPos(t.pos) - case keyEnd: - if t.pos == len(t.line) { - return - } - t.pos = len(t.line) - t.moveCursorToPos(t.pos) - case keyUp: - entry, ok := t.historyAt(t.historyIndex + 1) - if !ok { - return "", false - } - if t.historyIndex == -1 { - t.historyPending = string(t.line) - } - t.historyIndex++ - runes := []rune(entry) - t.setLine(runes, len(runes)) - case keyDown: - switch t.historyIndex { - case -1: - return - case 0: - runes := []rune(t.historyPending) - t.setLine(runes, len(runes)) - t.historyIndex-- - default: - entry, ok := t.historyAt(t.historyIndex - 1) - if ok { - t.historyIndex-- - runes := []rune(entry) - t.setLine(runes, len(runes)) - } - } - case keyEnter, keyLF: - t.moveCursorToPos(len(t.line)) - t.queue([]rune("\r\n")) - line = string(t.line) - ok = true - t.line = t.line[:0] - t.pos = 0 - t.cursorX = 0 - t.cursorY = 0 - t.maxLine = 0 - case keyDeleteWord: - // Delete zero or more spaces and then one or more characters. - t.eraseNPreviousChars(t.countToLeftWord()) - case keyDeleteLine: - // Delete everything from the current cursor position to the - // end of line. - for i := t.pos; i < len(t.line); i++ { - t.queue(space) - t.advanceCursor(1) - } - t.line = t.line[:t.pos] - t.moveCursorToPos(t.pos) - case keyCtrlD, keyDelete: - // Erase the character under the current position. - // The EOF case when the line is empty is handled in - // readLine(). - if t.pos < len(t.line) { - t.pos++ - t.eraseNPreviousChars(1) - } - case keyCtrlU: - t.eraseNPreviousChars(t.pos) - case keyTranspose: - // This transposes the two characters around the cursor and advances the cursor. Best-effort. - if len(t.line) < 2 || t.pos < 1 { - return - } - swap := t.pos - if swap == len(t.line) { - swap-- // special: at end of line, swap previous two chars - } - t.line[swap-1], t.line[swap] = t.line[swap], t.line[swap-1] - if t.pos < len(t.line) { - t.pos++ - } - if t.echo { - t.moveCursorToPos(swap - 1) - t.writeLine(t.line[swap-1:]) - t.moveCursorToPos(t.pos) - } - case keyClearScreen: - // Erases the screen and moves the cursor to the home position. - t.queue([]rune("\x1b[2J\x1b[H")) - t.queue(t.prompt) - t.cursorX, t.cursorY = 0, 0 - t.advanceCursor(visualLength(t.prompt)) - t.setLine(t.line, t.pos) - default: - if t.AutoCompleteCallback != nil { - prefix := string(t.line[:t.pos]) - suffix := string(t.line[t.pos:]) - - t.lock.Unlock() - newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) - t.lock.Lock() - - if completeOk { - t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) - return - } - } - if !isPrintable(key) { - return - } - if len(t.line) == maxLineLength { - return - } - t.addKeyToLine(key) - } - return -} - -// addKeyToLine inserts the given key at the current position in the current -// line. -func (t *Terminal) addKeyToLine(key rune) { - if len(t.line) == cap(t.line) { - newLine := make([]rune, len(t.line), 2*(1+len(t.line))) - copy(newLine, t.line) - t.line = newLine - } - t.line = t.line[:len(t.line)+1] - copy(t.line[t.pos+1:], t.line[t.pos:]) - t.line[t.pos] = key - if t.echo { - t.writeLine(t.line[t.pos:]) - } - t.pos++ - t.moveCursorToPos(t.pos) -} - -func (t *Terminal) writeLine(line []rune) { - for len(line) != 0 { - remainingOnLine := t.termWidth - t.cursorX - todo := len(line) - if todo > remainingOnLine { - todo = remainingOnLine - } - t.queue(line[:todo]) - t.advanceCursor(visualLength(line[:todo])) - line = line[todo:] - } -} - -// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. -func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { - for len(buf) > 0 { - i := bytes.IndexByte(buf, '\n') - todo := len(buf) - if i >= 0 { - todo = i - } - - var nn int - nn, err = w.Write(buf[:todo]) - n += nn - if err != nil { - return n, err - } - buf = buf[todo:] - - if i >= 0 { - if _, err = w.Write(crlf); err != nil { - return n, err - } - n++ - buf = buf[1:] - } - } - - return n, nil -} - -func (t *Terminal) Write(buf []byte) (n int, err error) { - t.lock.Lock() - defer t.lock.Unlock() - - if t.cursorX == 0 && t.cursorY == 0 { - // This is the easy case: there's nothing on the screen that we - // have to move out of the way. - return writeWithCRLF(t.c, buf) - } - - // We have a prompt and possibly user input on the screen. We - // have to clear it first. - t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) - t.cursorX = 0 - t.clearLineToRight() - - for t.cursorY > 0 { - t.move(1 /* up */, 0, 0, 0) - t.cursorY-- - t.clearLineToRight() - } - - if _, err = t.c.Write(t.outBuf); err != nil { - return - } - t.outBuf = t.outBuf[:0] - - if n, err = writeWithCRLF(t.c, buf); err != nil { - return - } - - t.writeLine(t.prompt) - if t.echo { - t.writeLine(t.line) - } - - t.moveCursorToPos(t.pos) - - if _, err = t.c.Write(t.outBuf); err != nil { - return - } - t.outBuf = t.outBuf[:0] - return -} - -// ReadPassword temporarily changes the prompt and reads a password, without -// echo, from the terminal. -// -// The AutoCompleteCallback is disabled during this call. -func (t *Terminal) ReadPassword(prompt string) (line string, err error) { - t.lock.Lock() - defer t.lock.Unlock() - - oldPrompt := t.prompt - t.prompt = []rune(prompt) - t.echo = false - oldAutoCompleteCallback := t.AutoCompleteCallback - t.AutoCompleteCallback = nil - defer func() { - t.AutoCompleteCallback = oldAutoCompleteCallback - }() - - line, err = t.readLine() - - t.prompt = oldPrompt - t.echo = true - - return -} - -// ReadLine returns a line of input from the terminal. -func (t *Terminal) ReadLine() (line string, err error) { - t.lock.Lock() - defer t.lock.Unlock() - - return t.readLine() -} - -func (t *Terminal) readLine() (line string, err error) { - // t.lock must be held at this point - - if t.cursorX == 0 && t.cursorY == 0 { - t.writeLine(t.prompt) - t.c.Write(t.outBuf) - t.outBuf = t.outBuf[:0] - } - - lineIsPasted := t.pasteActive - - for { - rest := t.remainder - lineOk := false - for !lineOk { - var key rune - key, rest = bytesToKey(rest, t.pasteActive) - if key == utf8.RuneError { - break - } - if !t.pasteActive { - if key == keyCtrlD { - if len(t.line) == 0 { - return "", io.EOF - } - } - if key == keyCtrlC { - return "", io.EOF - } - if key == keyPasteStart { - t.pasteActive = true - if len(t.line) == 0 { - lineIsPasted = true - } - continue - } - } else if key == keyPasteEnd { - t.pasteActive = false - continue - } - if !t.pasteActive { - lineIsPasted = false - } - // If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line. - if key == keyEnter && len(rest) > 0 && rest[0] == keyLF { - rest = rest[1:] - } - line, lineOk = t.handleKey(key) - } - if len(rest) > 0 { - n := copy(t.inBuf[:], rest) - t.remainder = t.inBuf[:n] - } else { - t.remainder = nil - } - t.c.Write(t.outBuf) - t.outBuf = t.outBuf[:0] - if lineOk { - if t.echo { - t.historyIndex = -1 - t.historyAdd(line) - } - if lineIsPasted { - err = ErrPasteIndicator - } - return - } - - // t.remainder is a slice at the beginning of t.inBuf - // containing a partial key sequence - readBuf := t.inBuf[len(t.remainder):] - var n int - - t.lock.Unlock() - n, err = t.c.Read(readBuf) - t.lock.Lock() - - if err != nil { - return - } - - t.remainder = t.inBuf[:n+len(t.remainder)] - } -} - -// SetPrompt sets the prompt to be used when reading subsequent lines. -func (t *Terminal) SetPrompt(prompt string) { - t.lock.Lock() - defer t.lock.Unlock() - - t.prompt = []rune(prompt) -} - -func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { - // Move cursor to column zero at the start of the line. - t.move(t.cursorY, 0, t.cursorX, 0) - t.cursorX, t.cursorY = 0, 0 - t.clearLineToRight() - for t.cursorY < numPrevLines { - // Move down a line - t.move(0, 1, 0, 0) - t.cursorY++ - t.clearLineToRight() - } - // Move back to beginning. - t.move(t.cursorY, 0, 0, 0) - t.cursorX, t.cursorY = 0, 0 - - t.queue(t.prompt) - t.advanceCursor(visualLength(t.prompt)) - t.writeLine(t.line) - t.moveCursorToPos(t.pos) -} - -func (t *Terminal) SetSize(width, height int) error { - t.lock.Lock() - defer t.lock.Unlock() - - if width == 0 { - width = 1 - } - - oldWidth := t.termWidth - t.termWidth, t.termHeight = width, height - - switch { - case width == oldWidth: - // If the width didn't change then nothing else needs to be - // done. - return nil - case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: - // If there is nothing on current line and no prompt printed, - // just do nothing - return nil - case width < oldWidth: - // Some terminals (e.g. xterm) will truncate lines that were - // too long when shinking. Others, (e.g. gnome-terminal) will - // attempt to wrap them. For the former, repainting t.maxLine - // works great, but that behaviour goes badly wrong in the case - // of the latter because they have doubled every full line. - - // We assume that we are working on a terminal that wraps lines - // and adjust the cursor position based on every previous line - // wrapping and turning into two. This causes the prompt on - // xterms to move upwards, which isn't great, but it avoids a - // huge mess with gnome-terminal. - if t.cursorX >= t.termWidth { - t.cursorX = t.termWidth - 1 - } - t.cursorY *= 2 - t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) - case width > oldWidth: - // If the terminal expands then our position calculations will - // be wrong in the future because we think the cursor is - // |t.pos| chars into the string, but there will be a gap at - // the end of any wrapped line. - // - // But the position will actually be correct until we move, so - // we can move back to the beginning and repaint everything. - t.clearAndRepaintLinePlusNPrevious(t.maxLine) - } - - _, err := t.c.Write(t.outBuf) - t.outBuf = t.outBuf[:0] - return err -} - -type pasteIndicatorError struct{} - -func (pasteIndicatorError) Error() string { - return "terminal: ErrPasteIndicator not correctly handled" -} - -// ErrPasteIndicator may be returned from ReadLine as the error, in addition -// to valid line data. It indicates that bracketed paste mode is enabled and -// that the returned line consists only of pasted data. Programs may wish to -// interpret pasted data more literally than typed data. -var ErrPasteIndicator = pasteIndicatorError{} - -// SetBracketedPasteMode requests that the terminal bracket paste operations -// with markers. Not all terminals support this but, if it is supported, then -// enabling this mode will stop any autocomplete callback from running due to -// pastes. Additionally, any lines that are completely pasted will be returned -// from ReadLine with the error set to ErrPasteIndicator. -func (t *Terminal) SetBracketedPasteMode(on bool) { - if on { - io.WriteString(t.c, "\x1b[?2004h") - } else { - io.WriteString(t.c, "\x1b[?2004l") - } -} - -// stRingBuffer is a ring buffer of strings. -type stRingBuffer struct { - // entries contains max elements. - entries []string - max int - // head contains the index of the element most recently added to the ring. - head int - // size contains the number of elements in the ring. - size int -} - -func (s *stRingBuffer) Add(a string) { - if s.entries == nil { - const defaultNumEntries = 100 - s.entries = make([]string, defaultNumEntries) - s.max = defaultNumEntries - } - - s.head = (s.head + 1) % s.max - s.entries[s.head] = a - if s.size < s.max { - s.size++ - } -} - -func (s *stRingBuffer) Len() int { - return s.size -} - -// At returns the value passed to the nth previous call to Add. -// If n is zero then the immediately prior value is returned, if one, then the -// next most recent, and so on. If such an element doesn't exist then ok is -// false. -func (s *stRingBuffer) At(n int) string { - if n < 0 || n >= s.size { - panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size)) - } - index := s.head - n - if index < 0 { - index += s.max - } - return s.entries[index] -} - -// readPasswordLine reads from reader until it finds \n or io.EOF. -// The slice returned does not include the \n. -// readPasswordLine also ignores any \r it finds. -// Windows uses \r as end of line. So, on Windows, readPasswordLine -// reads until it finds \r and ignores any \n it finds during processing. -func readPasswordLine(reader io.Reader) ([]byte, error) { - var buf [1]byte - var ret []byte - - for { - n, err := reader.Read(buf[:]) - if n > 0 { - switch buf[0] { - case '\b': - if len(ret) > 0 { - ret = ret[:len(ret)-1] - } - case '\n': - if runtime.GOOS != "windows" { - return ret, nil - } - // otherwise ignore \n - case '\r': - if runtime.GOOS == "windows" { - return ret, nil - } - // otherwise ignore \r - default: - ret = append(ret, buf[0]) - } - continue - } - if err != nil { - if err == io.EOF && len(ret) > 0 { - return ret, nil - } - return ret, err - } - } -} diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go index 794b2e32bfa..563270c1549 100644 --- a/vendor/golang.org/x/time/rate/rate.go +++ b/vendor/golang.org/x/time/rate/rate.go @@ -195,7 +195,7 @@ func (r *Reservation) CancelAt(t time.Time) { // update state r.lim.last = t r.lim.tokens = tokens - if r.timeToAct == r.lim.lastEvent { + if r.timeToAct.Equal(r.lim.lastEvent) { prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) if !prevEvent.Before(t) { r.lim.lastEvent = prevEvent diff --git a/vendor/golang.org/x/time/rate/sometimes.go b/vendor/golang.org/x/time/rate/sometimes.go index 6ba99ddb67b..9b83932692f 100644 --- a/vendor/golang.org/x/time/rate/sometimes.go +++ b/vendor/golang.org/x/time/rate/sometimes.go @@ -61,7 +61,9 @@ func (s *Sometimes) Do(f func()) { (s.Every > 0 && s.count%s.Every == 0) || (s.Interval > 0 && time.Since(s.last) >= s.Interval) { f() - s.last = time.Now() + if s.Interval > 0 { + s.last = time.Now() + } } s.count++ } diff --git a/vendor/modules.txt b/vendor/modules.txt index dd9a2887df9..b5d027d0968 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,7 +2,7 @@ ## explicit; go 1.20 filippo.io/edwards25519 filippo.io/edwards25519/field -# github.com/aws/aws-sdk-go-v2 v1.41.6 +# github.com/aws/aws-sdk-go-v2 v1.41.7 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/aws github.com/aws/aws-sdk-go-v2/aws/arn @@ -28,15 +28,15 @@ github.com/aws/aws-sdk-go-v2/internal/shareddefaults github.com/aws/aws-sdk-go-v2/internal/strings github.com/aws/aws-sdk-go-v2/internal/sync/singleflight github.com/aws/aws-sdk-go-v2/internal/timeconv -# github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 +# github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi -# github.com/aws/aws-sdk-go-v2/config v1.32.16 +# github.com/aws/aws-sdk-go-v2/config v1.32.17 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/config github.com/aws/aws-sdk-go-v2/config/internal/ini -# github.com/aws/aws-sdk-go-v2/credentials v1.19.15 +# github.com/aws/aws-sdk-go-v2/credentials v1.19.16 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/credentials github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds @@ -46,63 +46,63 @@ github.com/aws/aws-sdk-go-v2/credentials/logincreds github.com/aws/aws-sdk-go-v2/credentials/processcreds github.com/aws/aws-sdk-go-v2/credentials/ssocreds github.com/aws/aws-sdk-go-v2/credentials/stscreds -# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 +# github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/feature/ec2/imds github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config -# github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 +# github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/configsources -# github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 +# github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 -# github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 +# github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/internal/v4a github.com/aws/aws-sdk-go-v2/internal/v4a/internal/crypto github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4 -# github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 +# github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding -# github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 +# github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/checksum -# github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 +# github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url -# github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 +# github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/internal/s3shared github.com/aws/aws-sdk-go-v2/service/internal/s3shared/arn github.com/aws/aws-sdk-go-v2/service/internal/s3shared/config -# github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 +# github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/s3 github.com/aws/aws-sdk-go-v2/service/s3/internal/arn github.com/aws/aws-sdk-go-v2/service/s3/internal/customizations github.com/aws/aws-sdk-go-v2/service/s3/internal/endpoints github.com/aws/aws-sdk-go-v2/service/s3/types -# github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 +# github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/signin github.com/aws/aws-sdk-go-v2/service/signin/internal/endpoints github.com/aws/aws-sdk-go-v2/service/signin/types -# github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 +# github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/sso github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints github.com/aws/aws-sdk-go-v2/service/sso/types -# github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 +# github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/ssooidc github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints github.com/aws/aws-sdk-go-v2/service/ssooidc/types -# github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 +# github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/service/sts github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints github.com/aws/aws-sdk-go-v2/service/sts/types -# github.com/aws/smithy-go v1.25.0 +# github.com/aws/smithy-go v1.25.1 ## explicit; go 1.24 github.com/aws/smithy-go github.com/aws/smithy-go/auth @@ -140,9 +140,6 @@ github.com/cenkalti/backoff/v5 # github.com/cespare/xxhash/v2 v2.3.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2 -# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f -## explicit -github.com/dgryski/go-rendezvous # github.com/eggsampler/acme/v3 v3.8.1 ## explicit; go 1.11 github.com/eggsampler/acme/v3 @@ -252,6 +249,7 @@ github.com/prometheus/client_golang/prometheus/internal github.com/prometheus/client_golang/prometheus/promauto github.com/prometheus/client_golang/prometheus/promhttp github.com/prometheus/client_golang/prometheus/promhttp/internal +github.com/prometheus/client_golang/prometheus/push # github.com/prometheus/client_model v0.6.1 ## explicit; go 1.19 github.com/prometheus/client_model/go @@ -271,37 +269,44 @@ github.com/redis/go-redis/extra/rediscmd/v9 # github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 ## explicit; go 1.19 github.com/redis/go-redis/extra/redisotel/v9 -# github.com/redis/go-redis/v9 v9.10.0 -## explicit; go 1.18 +# github.com/redis/go-redis/v9 v9.20.1 +## explicit; go 1.24 github.com/redis/go-redis/v9 github.com/redis/go-redis/v9/auth github.com/redis/go-redis/v9/internal +github.com/redis/go-redis/v9/internal/auth/streaming github.com/redis/go-redis/v9/internal/hashtag github.com/redis/go-redis/v9/internal/hscan +github.com/redis/go-redis/v9/internal/interfaces +github.com/redis/go-redis/v9/internal/maintnotifications/logs +github.com/redis/go-redis/v9/internal/otel github.com/redis/go-redis/v9/internal/pool github.com/redis/go-redis/v9/internal/proto -github.com/redis/go-redis/v9/internal/rand +github.com/redis/go-redis/v9/internal/routing github.com/redis/go-redis/v9/internal/util +github.com/redis/go-redis/v9/maintnotifications +github.com/redis/go-redis/v9/push # github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 ## explicit github.com/titanous/rocacheck -# github.com/weppos/publicsuffix-go v0.50.3 +# github.com/weppos/publicsuffix-go v0.50.4-0.20260507075217-1bd47f85b3da ## explicit; go 1.24.0 github.com/weppos/publicsuffix-go/publicsuffix -# github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 -## explicit; go 1.16 +# github.com/zmap/zcrypto v0.0.0-20260514033604-a1159eb3cad9 +## explicit; go 1.25.0 github.com/zmap/zcrypto/cryptobyte github.com/zmap/zcrypto/cryptobyte/asn1 github.com/zmap/zcrypto/dsa github.com/zmap/zcrypto/encoding/asn1 github.com/zmap/zcrypto/internal/randutil github.com/zmap/zcrypto/json +github.com/zmap/zcrypto/rsa github.com/zmap/zcrypto/util github.com/zmap/zcrypto/x509 github.com/zmap/zcrypto/x509/ct github.com/zmap/zcrypto/x509/pkix -# github.com/zmap/zlint/v3 v3.6.6 -## explicit; go 1.23.0 +# github.com/zmap/zlint/v3 v3.7.2-0.20260531191521-b88ecfaefc52 +## explicit; go 1.25.0 github.com/zmap/zlint/v3 github.com/zmap/zlint/v3/lint github.com/zmap/zlint/v3/lints/apple @@ -309,6 +314,7 @@ github.com/zmap/zlint/v3/lints/cabf_br github.com/zmap/zlint/v3/lints/cabf_cs_br github.com/zmap/zlint/v3/lints/cabf_ev github.com/zmap/zlint/v3/lints/cabf_smime_br +github.com/zmap/zlint/v3/lints/chrome github.com/zmap/zlint/v3/lints/community github.com/zmap/zlint/v3/lints/etsi github.com/zmap/zlint/v3/lints/mozilla @@ -384,6 +390,9 @@ go.opentelemetry.io/proto/otlp/collector/trace/v1 go.opentelemetry.io/proto/otlp/common/v1 go.opentelemetry.io/proto/otlp/resource/v1 go.opentelemetry.io/proto/otlp/trace/v1 +# go.uber.org/atomic v1.11.0 +## explicit; go 1.18 +go.uber.org/atomic # go.yaml.in/yaml/v3 v3.0.4 ## explicit; go 1.16 go.yaml.in/yaml/v3 @@ -416,21 +425,17 @@ golang.org/x/net/trace golang.org/x/sync/errgroup # golang.org/x/sys v0.45.0 ## explicit; go 1.25.0 -golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows golang.org/x/sys/windows/registry -# golang.org/x/term v0.43.0 -## explicit; go 1.25.0 -golang.org/x/term # golang.org/x/text v0.37.0 ## explicit; go 1.25.0 golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# golang.org/x/time v0.11.0 -## explicit; go 1.23.0 +# golang.org/x/time v0.15.0 +## explicit; go 1.25.0 golang.org/x/time/rate # golang.org/x/tools v0.44.0 ## explicit; go 1.25.0 diff --git a/web/context.go b/web/context.go index c3681be03ac..da9119939fa 100644 --- a/web/context.go +++ b/web/context.go @@ -5,16 +5,16 @@ import ( "crypto" "crypto/ecdsa" "crypto/rsa" - "encoding/json" "fmt" + "log/slog" "net/http" "net/netip" "strings" "time" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/identifier" - blog "github.com/letsencrypt/boulder/log" ) type userAgentContextKey struct{} @@ -37,17 +37,19 @@ func WithUserAgent(ctx context.Context, ua string) context.Context { // single web request. It is generated when a request is received, passed to // the request handler which can populate its fields as appropriate, and then // logged when the request completes. +// +// If new fields are added to this struct, they MUST be added to logEvent below. type RequestEvent struct { // These fields are not rendered in JSON; instead, they are rendered // whitespace-separated ahead of the JSON. This saves bytes in the logs since // we don't have to include field names, quotes, or commas -- all of these // fields are known to not include whitespace. - Method string `json:"-"` - Endpoint string `json:"-"` - Requester int64 `json:"-"` - Code int `json:"-"` - Latency float64 `json:"-"` - RealIP string `json:"-"` + Method string `json:"-"` + Endpoint string `json:"-"` + Requester int64 `json:"-"` + Code int `json:"-"` + Latency time.Duration `json:"-"` + RealIP string `json:"-"` Slug string `json:",omitempty"` InternalErrors []string `json:",omitempty"` @@ -191,27 +193,73 @@ func (th *TopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // to 200 itself when writing to the wire logEvent.Code = http.StatusOK } - logEvent.Latency = time.Since(begin).Seconds() - th.logEvent(logEvent) + logEvent.Latency = time.Since(begin) + th.logEvent(r.Context(), logEvent) }() th.wfe.ServeHTTP(logEvent, rwws, r) } -func (th *TopHandler) logEvent(logEvent *RequestEvent) { +// logEvent converts a RequestEvent into a message and collection of slog.Attr, +// and logs the result. +// +// This function MUST handle any new fields added to RequestEvent. +func (th *TopHandler) logEvent(ctx context.Context, logEvent *RequestEvent) { if logEvent.suppressed { return } - jsonEvent, err := json.Marshal(logEvent) - if err != nil { - th.log.Errf("%s %s %d %d %d %s JSON={\"InternalErrors\": %q}", - logEvent.Method, logEvent.Endpoint, logEvent.Requester, logEvent.Code, - int(logEvent.Latency*1000), logEvent.RealIP, - fmt.Errorf("failed to marshal json log event: %w", err).Error()) - return + + // These first attributes are guaranteed to be set on every RequestEvent. They + // are set during initial construction in TopHandler.ServeHTTP, by the wrapper + // code in wfe.HandleFunc, or by the deferred func also in ServeHTTP. + attrs := []slog.Attr{ + slog.String("method", logEvent.Method), + slog.String("endpoint", logEvent.Endpoint), + slog.String("ip", logEvent.RealIP), + slog.String("ua", logEvent.UserAgent), + slog.String("origin", logEvent.Origin), + slog.Int("code", logEvent.Code), + slog.Duration("latency", logEvent.Latency), + } + + // These attributes are only sometimes set, depending on the handler. + if logEvent.Slug != "" { + attrs = append(attrs, slog.String("slug", logEvent.Slug)) + } + if logEvent.Requester != 0 { + attrs = append(attrs, blog.Acct(logEvent.Requester)) + } + if len(logEvent.Identifiers) != 0 { + attrs = append(attrs, blog.Idents(logEvent.Identifiers...)) + } + if logEvent.ChallengeType != "" { + attrs = append(attrs, slog.String("challengeType", logEvent.ChallengeType)) } - th.log.Infof("%s %s %d %d %d %s JSON=%s", - logEvent.Method, logEvent.Endpoint, logEvent.Requester, logEvent.Code, - int(logEvent.Latency*1000), logEvent.RealIP, jsonEvent) + if logEvent.Created != "" { + attrs = append(attrs, slog.String("created", logEvent.Created)) + } + if logEvent.Status != "" { + attrs = append(attrs, slog.String("status", logEvent.Status)) + } + + // The "Extra" field was a hack to avoid having to create a bunch of new + // fields on the RequestEvent struct. Elevate these to the top-level. + for k, v := range logEvent.Extra { + attrs = append(attrs, slog.Any(k, v)) + } + + // There's a long-standing TODO to treat IgnoredRateLimitError the same as + // other InternalErrors, so just join them together here. + errs := logEvent.InternalErrors + if logEvent.IgnoredRateLimitError != "" { + errs = append(errs, logEvent.IgnoredRateLimitError) + } + if len(errs) != 0 { + attrs = append(attrs, slog.Any("internalErrors", errs)) + } + + // The WFE always logs at info level, even if there was an error processing + // the request, because we have successfully supplied an HTTP response. + th.log.Info(ctx, "", attrs...) } func KeyTypeToString(pub crypto.PublicKey) string { diff --git a/web/context_test.go b/web/context_test.go index e14fe7c4a1f..2c5644e6f5e 100644 --- a/web/context_test.go +++ b/web/context_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/tls" - "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -13,8 +12,8 @@ import ( "testing" "time" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/features" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) @@ -27,23 +26,23 @@ func (m myHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Req } func TestLogCode(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() th := NewTopHandler(mockLog, myHandler{}) req, err := http.NewRequest("GET", "/thisisignored", &bytes.Reader{}) if err != nil { t.Fatal(err) } th.ServeHTTP(httptest.NewRecorder(), req) - expected := `INFO: GET /endpoint 0 201 0 0.0.0.0 JSON={}` + expected := `level=INFO msg="" method=GET endpoint=/endpoint ip=0.0.0.0 ua="" origin="" code=201` if len(mockLog.GetAllMatching(expected)) != 1 { t.Errorf("Expected exactly one log line matching %q. Got \n%s", - expected, strings.Join(mockLog.GetAllMatching(".*"), "\n")) + expected, strings.Join(mockLog.GetAll(), "\n")) } } // TestLogUA tests that user-agents are truncated before logging, and faithfully pass along non-ASCII. func TestLogUA(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() th := NewTopHandler(mockLog, myHandler{}) req, err := http.NewRequest("GET", "/thisisignored", &bytes.Reader{}) if err != nil { @@ -52,26 +51,18 @@ func TestLogUA(t *testing.T) { req.Header.Add("User-Agent", "🪨"+strings.Repeat("a", 200)) th.ServeHTTP(httptest.NewRecorder(), req) - matching := mockLog.GetAllMatching("JSON=") + matching := mockLog.GetAllMatching("endpoint=/endpoint") if len(matching) != 1 { - t.Errorf("Expected exactly one log line. Got: %s", - strings.Join(mockLog.GetAllMatching(".*"), "\n")) + t.Errorf("Expected exactly one log line, got: %s", strings.Join(mockLog.GetAll(), "\n")) } - re := regexp.MustCompile(`JSON=({.*})$`) - m := re.FindStringSubmatch(matching[0]) - if len(m) < 2 { - t.Fatalf("logging user-agent: no regexp match") + re := regexp.MustCompile(`ua=(\S*)`) + got := re.FindStringSubmatch(matching[0]) + if len(got) != 2 { + t.Fatalf("logging user-agent: got %d regexp matches but want 2 (1 overall and 1 capture group)", len(got)) } - var ua struct { - UA string - } - err = json.Unmarshal([]byte(m[1]), &ua) - if err != nil { - t.Fatal(err) - } - expected := "🪨aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." - if ua.UA != expected { - t.Errorf("logging user-agent: got %x, want %x", ua.UA, expected) + want := "🪨aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + if got[1] != want { + t.Errorf("logging user-agent: got %x, want %x", got[1], want) } } @@ -83,22 +74,22 @@ func (ch codeHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http. } func TestStatusCodeLogging(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() th := NewTopHandler(mockLog, codeHandler{}) req, err := http.NewRequest("GET", "/thisisignored", &bytes.Reader{}) if err != nil { t.Fatal(err) } th.ServeHTTP(httptest.NewRecorder(), req) - expected := `INFO: GET /endpoint 0 200 0 0.0.0.0 JSON={}` + expected := `level=INFO .* code=200` if len(mockLog.GetAllMatching(expected)) != 1 { t.Errorf("Expected exactly one log line matching %q. Got \n%s", - expected, strings.Join(mockLog.GetAllMatching(".*"), "\n")) + expected, strings.Join(mockLog.GetAll(), "\n")) } } func TestOrigin(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() th := NewTopHandler(mockLog, myHandler{}) req, err := http.NewRequest("GET", "/thisisignored", &bytes.Reader{}) if err != nil { @@ -106,10 +97,10 @@ func TestOrigin(t *testing.T) { } req.Header.Add("Origin", "https://example.com") th.ServeHTTP(httptest.NewRecorder(), req) - expected := `INFO: GET /endpoint 0 201 0 0.0.0.0 JSON={.*"Origin":"https://example.com"}` + expected := `level=INFO .* origin=https://example.com` if len(mockLog.GetAllMatching(expected)) != 1 { t.Errorf("Expected exactly one log line matching %q. Got \n%s", - expected, strings.Join(mockLog.GetAllMatching(".*"), "\n")) + expected, strings.Join(mockLog.GetAll(), "\n")) } } @@ -122,7 +113,7 @@ func (hhh hostHeaderHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r } func TestHostHeaderRewrite(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() hhh := hostHeaderHandler{f: func(_ *RequestEvent, _ http.ResponseWriter, r *http.Request) { t.Helper() test.AssertEquals(t, r.Host, "localhost") @@ -171,7 +162,7 @@ func (ch cancelHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *htt } func TestPropagateCancel(t *testing.T) { - mockLog := blog.UseMock() + mockLog := blog.NewMock() res := make(chan string) features.Set(features.Config{PropagateCancels: true}) th := NewTopHandler(mockLog, cancelHandler{res}) diff --git a/web/send_error.go b/web/send_error.go index 6d5ec941cc5..5a2394c30d8 100644 --- a/web/send_error.go +++ b/web/send_error.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/probs" ) diff --git a/web/send_error_test.go b/web/send_error_test.go index 84b6eed9630..130b0a7e628 100644 --- a/web/send_error_test.go +++ b/web/send_error_test.go @@ -5,9 +5,9 @@ import ( "net/http/httptest" "testing" + "github.com/letsencrypt/boulder/blog" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" - "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test" ) @@ -36,7 +36,7 @@ func TestSendErrorSubProblemNamespace(t *testing.T) { }), "dfoop", ) - SendError(log.NewMock(), rw, &RequestEvent{}, prob, errors.New("it bad")) + SendError(blog.NewMock(), rw, &RequestEvent{}, prob, errors.New("it bad")) body := rw.Body.String() test.AssertUnmarshaledEquals(t, body, `{ @@ -91,7 +91,7 @@ func TestSendErrorSubProbLogging(t *testing.T) { "dfoop", ) logEvent := RequestEvent{} - SendError(log.NewMock(), rw, &logEvent, prob, errors.New("it bad")) + SendError(blog.NewMock(), rw, &logEvent, prob, errors.New("it bad")) test.AssertEquals(t, logEvent.Error, `400 :: malformed :: dfoop :: bad ["example.com :: malformed :: dfoop :: nop", "what about example.com :: malformed :: dfoop :: nah"]`) } @@ -99,7 +99,7 @@ func TestSendErrorSubProbLogging(t *testing.T) { func TestSendErrorPausedProblemLoggingSuppression(t *testing.T) { rw := httptest.NewRecorder() logEvent := RequestEvent{} - SendError(log.NewMock(), rw, &logEvent, probs.Paused("I better not see any of this"), nil) + SendError(blog.NewMock(), rw, &logEvent, probs.Paused("I better not see any of this"), nil) test.AssertEquals(t, logEvent.Error, "429 :: rateLimited :: account/ident pair is paused") } @@ -107,7 +107,7 @@ func TestSendErrorPausedProblemLoggingSuppression(t *testing.T) { func TestSendErrorDoesNotEscapeHTML(t *testing.T) { rw := httptest.NewRecorder() logEvent := RequestEvent{} - SendError(log.NewMock(), rw, &logEvent, probs.Malformed("nonce less than lowest eligible nonce: 1 < 2"), nil) + SendError(blog.NewMock(), rw, &logEvent, probs.Malformed("nonce less than lowest eligible nonce: 1 < 2"), nil) test.AssertEquals(t, logEvent.Error, "400 :: malformed :: nonce less than lowest eligible nonce: 1 < 2") body := rw.Body.String() diff --git a/web/server.go b/web/server.go index ebcb8d8b5cb..369a573422b 100644 --- a/web/server.go +++ b/web/server.go @@ -2,27 +2,30 @@ package web import ( "bytes" + "context" + "errors" "log" "net/http" "time" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" ) +// errorWriter is an adaptor for blog.Logger to meet the io.Writer interface, +// used by the go stdlib's log.Logger, which in turn is used by http.Server. +// It appears here rather than in //blog/adapters.go because it is used +// locally, not set at a package-global level. type errorWriter struct { blog.Logger } -func (ew errorWriter) Write(p []byte) (n int, err error) { - // log.Logger will append a newline to all messages before calling - // Write. Our log checksum checker doesn't like newlines, because - // syslog will strip them out so the calculated checksums will - // differ. So that we don't hit this corner case for every line - // logged from inside net/http.Server we strip the newline before - // we get to the checksum generator. +func (ew errorWriter) Write(p []byte) (int, error) { + // log.Logger appends a newline to all messages before calling Write. Our + // logging infra will append another. Strip the first one to prevent + // redundancy. p = bytes.TrimRight(p, "\n") - ew.Logger.Errf("net/http.Server: %s", p) - return + ew.Logger.Error(context.Background(), "net/http.Server", errors.New(string(p))) + return len(p), nil } // NewServer returns an http.Server which will listen on the given address, when diff --git a/web/server_test.go b/web/server_test.go index ecdbcc82804..3a32977288a 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -7,7 +7,7 @@ import ( "sync" "testing" - blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/test" ) diff --git a/wfe2/verify.go b/wfe2/verify.go index 6bb528f9837..f6550c443df 100644 --- a/wfe2/verify.go +++ b/wfe2/verify.go @@ -300,9 +300,9 @@ func (wfe *WebFrontEndImpl) validPOSTURL( func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner jose.Header) error { // Verify that the outer JWS has a non-empty URL header. This is strictly // defensive since the expectation is that endpoints using `matchJWSURLs` - // have received at least one of their JWS from an account-authenticated - // verifier, which checks the outer JWS has the expected URL header before - // processing the inner JWS. + // have received at least one of their JWS from calling validPOSTForAccount(), + // which checks the outer JWS has the expected URL header before processing + // the inner JWS. outerURL, ok := outer.ExtraHeaders[jose.HeaderKey("url")].(string) if !ok || len(outerURL) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverOuterJWSNoURL"}).Inc() @@ -489,19 +489,11 @@ func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) // authentication and does not contain an embedded JWK. Callers should have // acquired headers from a bJSONWebSignature. func (wfe *WebFrontEndImpl) lookupJWK( - header jose.Header, ctx context.Context, - request *http.Request, - logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, error) { - return wfe.lookupJWKUsing(header, ctx, request, logEvent, wfe.accountGetter) -} - -func (wfe *WebFrontEndImpl) lookupJWKUsing( header jose.Header, - ctx context.Context, request *http.Request, - logEvent *web.RequestEvent, - accountGetter AccountGetter) (*jose.JSONWebKey, *core.Registration, error) { + accountGetter AccountGetter, + logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, error) { // We expect the request to be using an embedded Key ID auth type and to not // contain the mutually exclusive embedded JWK. if err := wfe.enforceJWSAuthType(header, embeddedKeyID); err != nil { @@ -610,27 +602,13 @@ func (wfe *WebFrontEndImpl) validJWSForKey( // JSONWebSignature, and a pointer to the JWK's associated account. If any of // these conditions are not met or an error occurs only a error is returned. func (wfe *WebFrontEndImpl) validJWSForAccount( - jws *bJSONWebSignature, - request *http.Request, ctx context.Context, - logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { - return wfe.validJWSForAccountUsing(jws, request, ctx, logEvent, wfe.accountGetter) -} - -func (wfe *WebFrontEndImpl) validJWSForAccountUsing( jws *bJSONWebSignature, request *http.Request, - ctx context.Context, - logEvent *web.RequestEvent, - accountGetter AccountGetter) ([]byte, *bJSONWebSignature, *core.Registration, error) { + accountGetter AccountGetter, + logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { // Lookup the account and JWK for the key ID that authenticated the JWS - pubKey, account, err := wfe.lookupJWKUsing( - jws.Signatures[0].Header, - ctx, - request, - logEvent, - accountGetter, - ) + pubKey, account, err := wfe.lookupJWK(ctx, jws.Signatures[0].Header, request, accountGetter, logEvent) if err != nil { return nil, nil, nil, err } @@ -644,65 +622,62 @@ func (wfe *WebFrontEndImpl) validJWSForAccountUsing( return payload, jws, account, nil } -// validPOSTForAccount checks a POST request against the configured account -// getter, which may be the WFE-local account cache. Use -// validPOSTForCurrentAccount before account-authenticated requests cross -// issuance or account mutation boundaries. +// validPOSTForAccount checks that a given POST request has a valid JWS using +// `validJWSForAccount` and the WFE's database/SA connection. If valid, the +// authenticated JWS body and the registration that authenticated the body are +// returned. Otherwise an error is returned. +// +// This function is not ideal for validating POST-as-GET requests; although it +// will work correctly, it will not validate that the request body is empty. +// Those requests should be validated via validPOSTAsGETForAccount. func (wfe *WebFrontEndImpl) validPOSTForAccount( - request *http.Request, ctx context.Context, + request *http.Request, logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { // Parse the JWS from the POST request jws, err := wfe.parseJWSRequest(request) if err != nil { return nil, nil, nil, err } - return wfe.validJWSForAccount(jws, request, ctx, logEvent) -} -// validPOSTForCurrentAccount checks a POST request against current SA account -// state, bypassing the WFE-local account cache. Use it before account-authenticated -// requests cross issuance or account mutation boundaries. -func (wfe *WebFrontEndImpl) validPOSTForCurrentAccount( - request *http.Request, - ctx context.Context, - logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { - return wfe.validPOSTForAccountUsing(request, ctx, logEvent, wfe.sa) + // Use wfe.sa (i.e. the real database) as the AccountGetter for all POST + // requests, as POSTs may modify the database. This prevents a stale account + // cache from letting an old key perform mutating operations, like rotating + // to a new key. + return wfe.validJWSForAccount(ctx, jws, request, wfe.sa, logEvent) } -func (wfe *WebFrontEndImpl) validPOSTForAccountUsing( - request *http.Request, +// validPOSTAsGETForAccount checks that a given POST request is valid using +// `validJWSForAccount` and the wfe's read-through account cache. It +// additionally validates that the JWS request payload is empty. If a non empty +// payload is provided, it returns MalformedError. +// +// This function must only be used to validate POST-as-GET requests, it must +// not be used to validate mutating POST requests. +func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount( ctx context.Context, - logEvent *web.RequestEvent, - accountGetter AccountGetter) ([]byte, *bJSONWebSignature, *core.Registration, error) { + request *http.Request, + logEvent *web.RequestEvent) (*core.Registration, error) { // Parse the JWS from the POST request jws, err := wfe.parseJWSRequest(request) if err != nil { - return nil, nil, nil, err + return nil, err } - return wfe.validJWSForAccountUsing(jws, request, ctx, logEvent, accountGetter) -} -// validPOSTAsGETForAccount checks that a given POST request is valid using -// the configured account getter, which may be the WFE-local account cache. It -// additionally validates that the JWS request payload is empty, indicating -// that it is a POST-as-GET request per ACME draft 15+ section 6.3 "GET and -// POST-as-GET requests". If a non empty payload is provided in the JWS the -// invalidPOSTAsGETErr error is returned. This function is useful only for -// endpoints that do not need to handle both POSTs with a body and POST-as-GET -// requests (e.g. Order, Certificate). -func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount( - request *http.Request, - ctx context.Context, - logEvent *web.RequestEvent) (*core.Registration, error) { - body, _, reg, err := wfe.validPOSTForAccount(request, ctx, logEvent) + // Use the account cache to look up the account. We are willing to accept + // idempotent read-only POST-as-GET requests authenticated by a key in the + // possibly-stale account cache, because such requests are relatively safe + // and relatively high-volume. + body, _, reg, err := wfe.validJWSForAccount(ctx, jws, request, wfe.accountCache, logEvent) if err != nil { return nil, err } + // Verify the POST-as-GET payload is empty if string(body) != "" { return nil, berrors.MalformedError("POST-as-GET requests must have an empty payload") } + // To make log analysis easier we choose to elevate the pseudo ACME HTTP // method "POST-as-GET" to the logEvent's Method, replacing the // http.MethodPost value. @@ -793,9 +768,9 @@ type rolloverOperation struct { // validKeyRollover checks if the innerJWS is a valid key rollover operation // given the outer JWS that carried it. It is assumed that the outerJWS has -// already been validated per the normal ACME process. It is *critical* this is -// the case since `validKeyRollover` does not check the outerJWS signature. This -// function checks that: +// already been validated per the normal ACME process using `validPOSTForAccount`. +// It is *critical* this is the case since `validKeyRollover` does not check the +// outerJWS signature. This function checks that: // 1) the inner JWS is valid and well formed // 2) the inner JWS has the same "url" header as the outer JWS // 3) the inner JWS is self-authenticated with an embedded JWK @@ -844,8 +819,8 @@ func (wfe *WebFrontEndImpl) validKeyRollover( return nil, berrors.MalformedError("Inner JWS does not verify with embedded JWK") } // NOTE(@cpu): we do not stomp the web.RequestEvent's payload here since that is set - // from the outerJWS by the account-authenticated verifier and contains the - // inner JWS and inner payload already. + // from the outerJWS in validPOSTForAccount and contains the inner JWS and inner + // payload already. // Verify that the outer and inner JWS protected URL headers match if err := wfe.matchJWSURLs(outerJWS.Signatures[0].Header, innerJWS.Signatures[0].Header); err != nil { diff --git a/wfe2/verify_test.go b/wfe2/verify_test.go index 5ce13ab045d..a2a81dd9a63 100644 --- a/wfe2/verify_test.go +++ b/wfe2/verify_test.go @@ -1178,7 +1178,7 @@ func TestLookupJWK(t *testing.T) { in := tc.JWS.Signatures[0].Header inputLogEvent := newRequestEvent() - gotJWK, gotAcct, gotErr := wfe.lookupJWK(in, context.Background(), tc.Request, inputLogEvent) + gotJWK, gotAcct, gotErr := wfe.lookupJWK(t.Context(), in, tc.Request, wfe.accountCache, inputLogEvent) if tc.WantErrDetail == "" { if gotErr != nil { t.Fatalf("lookupJWK(%#v) = %#v, want nil", in, gotErr) @@ -1395,7 +1395,7 @@ func TestValidPOSTForAccount(t *testing.T) { wfe.stats.joseErrorCount.Reset() inputLogEvent := newRequestEvent() - gotPayload, gotJWS, gotAcct, gotErr := wfe.validPOSTForAccount(tc.Request, context.Background(), inputLogEvent) + gotPayload, gotJWS, gotAcct, gotErr := wfe.validPOSTForAccount(t.Context(), tc.Request, inputLogEvent) if tc.WantErrDetail == "" { if gotErr != nil { t.Fatalf("validPOSTForAccount(%#v) = %#v, want nil", tc.Request, gotErr) @@ -1478,11 +1478,11 @@ func setupAuthOnlyWFE( inmemNonceService := &inmemnonce.NonceService{Impl: nonceService} freshGetter := &staticRegistrationGetter{registration: freshAccount} wfe := WebFrontEndImpl{ - sa: freshGetter, - rnc: inmemNonceService, - rncKey: rncKey, - accountGetter: rejectingRegistrationGetter{t: t}, - stats: initStats(metrics.NoopRegisterer), + sa: freshGetter, + rnc: inmemNonceService, + rncKey: rncKey, + accountCache: rejectingRegistrationGetter{t: t}, + stats: initStats(metrics.NoopRegisterer), } return wfe, requestSigner{t, inmemNonceService.AsSource()}, freshGetter @@ -1497,7 +1497,7 @@ func TestValidPOSTForCurrentAccountRejectsCachedDeactivatedAccount(t *testing.T) _, _, body := signer.byKeyID(1, nil, "http://localhost/test", "{}") request := makePostRequestWithPath("test", body) - _, _, _, err := wfe.validPOSTForCurrentAccount(request, ctx, newRequestEvent()) + _, _, _, err := wfe.validPOSTForAccount(ctx, request, newRequestEvent()) test.AssertErrorIs(t, err, berrors.Unauthorized) test.AssertContains(t, err.Error(), `Account is not valid, has status "deactivated"`) test.AssertEquals(t, freshGetter.calls, 1) @@ -1512,7 +1512,7 @@ func TestValidPOSTForCurrentAccountRejectsCachedPreRolloverKey(t *testing.T) { _, _, body := signer.byKeyID(1, nil, "http://localhost/test", "{}") request := makePostRequestWithPath("test", body) - _, _, _, err := wfe.validPOSTForCurrentAccount(request, ctx, newRequestEvent()) + _, _, _, err := wfe.validPOSTForAccount(ctx, request, newRequestEvent()) test.AssertErrorIs(t, err, berrors.Malformed) test.AssertContains(t, err.Error(), "JWS verification error") test.AssertEquals(t, freshGetter.calls, 1) @@ -1557,7 +1557,7 @@ func TestValidPOSTAsGETForAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { ev := newRequestEvent() - _, gotErr := wfe.validPOSTAsGETForAccount(tc.Request, context.Background(), ev) + _, gotErr := wfe.validPOSTAsGETForAccount(t.Context(), tc.Request, ev) if tc.WantErrDetail == "" { if gotErr != nil { t.Fatalf("validPOSTAsGETForAccount(%#v) = %#v, want nil", tc.Request, gotErr) @@ -1595,7 +1595,7 @@ func (sa mockSADifferentStoredKey) GetRegistration(_ context.Context, _ *sapb.Re func TestValidPOSTForAccountSwappedKey(t *testing.T) { wfe, _, signer := setupWFE(t) wfe.sa = &mockSADifferentStoredKey{} - wfe.accountGetter = wfe.sa + wfe.accountCache = wfe.sa event := newRequestEvent() payload := `{"resource":"ima-payload"}` @@ -1606,7 +1606,7 @@ func TestValidPOSTForAccountSwappedKey(t *testing.T) { // Ensure that ValidPOSTForAccount produces an error since the // mockSADifferentStoredKey will return a different key than the one we used to // sign the request - _, _, _, err := wfe.validPOSTForAccount(request, ctx, event) + _, _, _, err := wfe.validPOSTForAccount(ctx, request, event) test.AssertError(t, err, "No error returned for request signed by wrong key") test.AssertErrorIs(t, err, berrors.Malformed) test.AssertContains(t, err.Error(), "JWS verification error") diff --git a/wfe2/wfe.go b/wfe2/wfe.go index 049c70e5a92..432fbfef20b 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "errors" "fmt" + "log/slog" "math/big" "math/rand/v2" "net/http" @@ -26,6 +27,7 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" @@ -35,7 +37,6 @@ import ( _ "github.com/letsencrypt/boulder/grpc/noncebalancer" // imported for its init function. "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics/measured_http" "github.com/letsencrypt/boulder/nonce" "github.com/letsencrypt/boulder/policy" @@ -105,11 +106,11 @@ type WebFrontEndImpl struct { rnc nonce.Redeemer // rncKey is the HMAC key used to derive the prefix of nonce backends used // for nonce redemption. - rncKey []byte - accountGetter AccountGetter - log blog.Logger - clk clock.Clock - stats wfe2Stats + rncKey []byte + accountCache AccountGetter + log blog.Logger + clk clock.Clock + stats wfe2Stats // certificateChains maps IssuerNameIDs to slice of []byte containing a leading // newline and one or more PEM encoded certificates separated by a newline, @@ -154,6 +155,10 @@ type WebFrontEndImpl struct { // How many contacts to allow in a single NewAccount request. maxContactsPerReg int + // maxCumulativeIdentifierLength rejects new-order requests if the cumulative length of all identifiers + // is greater than its value. + maxCumulativeIdentifierLength int + // requestTimeout is the per-request overall timeout. requestTimeout time.Duration @@ -179,6 +184,9 @@ type WebFrontEndImpl struct { // given request counts as a renewal or not. blockedOnDemandLabels []string `validate:"omitempty"` + // accountBlocker checks whether accounts are blocked and returns errors if so. + accountBlocker AccountBlocker + // certProfiles is a map of acceptable certificate profile names to // descriptions (perhaps including URLs) of those profiles. NewOrder // Requests with a profile name not present in this map will be rejected. @@ -190,12 +198,18 @@ type accountCachePurger interface { } func (wfe *WebFrontEndImpl) purgeCachedAccount(regID int64) { - cache, ok := wfe.accountGetter.(accountCachePurger) + cache, ok := wfe.accountCache.(accountCachePurger) if ok { cache.purgeRegistration(regID) } } +// AccountBlocker defines an interface that can check whether a given ID is +// blocked, and return an error if so. +type AccountBlocker interface { + CheckAccountID(id int64) error +} + // NewWebFrontEndImpl constructs a web service for Boulder func NewWebFrontEndImpl( stats prometheus.Registerer, @@ -207,13 +221,14 @@ func NewWebFrontEndImpl( requestTimeout time.Duration, staleTimeout time.Duration, maxContactsPerReg int, + maxCumulativeIdentifierLength int, rac rapb.RegistrationAuthorityClient, sac sapb.StorageAuthorityReadOnlyClient, eec emailpb.ExporterClient, gnc nonce.Getter, rnc nonce.Redeemer, rncKey []byte, - accountGetter AccountGetter, + accountCache AccountGetter, limiter *ratelimits.Limiter, txnBuilder *ratelimits.TransactionBuilder, certProfiles map[string]string, @@ -221,6 +236,7 @@ func NewWebFrontEndImpl( unpauseJWTLifetime time.Duration, unpauseURL string, blockedOnDemandLabels []string, + accountBlocker AccountBlocker, caaIdentity string, ) (WebFrontEndImpl, error) { if len(issuerCertificates) == 0 { @@ -250,30 +266,32 @@ func NewWebFrontEndImpl( } wfe := WebFrontEndImpl{ - log: logger, - clk: clk, - keyPolicy: keyPolicy, - certificateChains: certificateChains, - issuerCertificates: issuerCertificates, - stats: initStats(stats), - requestTimeout: requestTimeout, - staleTimeout: staleTimeout, - maxContactsPerReg: maxContactsPerReg, - ra: rac, - sa: sac, - ee: eec, - gnc: gnc, - rnc: rnc, - rncKey: rncKey, - accountGetter: accountGetter, - limiter: limiter, - txnBuilder: txnBuilder, - certProfiles: certProfiles, - unpauseSigner: unpauseSigner, - unpauseJWTLifetime: unpauseJWTLifetime, - unpauseURL: unpauseURL, - blockedOnDemandLabels: blockedLabels, - DirectoryCAAIdentity: normalizedCAAIdentity, + log: logger, + clk: clk, + keyPolicy: keyPolicy, + certificateChains: certificateChains, + issuerCertificates: issuerCertificates, + stats: initStats(stats), + requestTimeout: requestTimeout, + staleTimeout: staleTimeout, + maxContactsPerReg: maxContactsPerReg, + maxCumulativeIdentifierLength: maxCumulativeIdentifierLength, + ra: rac, + sa: sac, + ee: eec, + gnc: gnc, + rnc: rnc, + rncKey: rncKey, + accountCache: accountCache, + limiter: limiter, + txnBuilder: txnBuilder, + certProfiles: certProfiles, + unpauseSigner: unpauseSigner, + unpauseJWTLifetime: unpauseJWTLifetime, + unpauseURL: unpauseURL, + blockedOnDemandLabels: blockedLabels, + accountBlocker: accountBlocker, + DirectoryCAAIdentity: normalizedCAAIdentity, } return wfe, nil @@ -372,7 +390,7 @@ func marshalIndent(v any) ([]byte, error) { return json.MarshalIndent(v, "", " ") } -func (wfe *WebFrontEndImpl) writeJsonResponse(response http.ResponseWriter, logEvent *web.RequestEvent, status int, v any) error { +func (wfe *WebFrontEndImpl) writeJsonResponse(ctx context.Context, response http.ResponseWriter, logEvent *web.RequestEvent, status int, v any) error { jsonReply, err := marshalIndent(v) if err != nil { return err // All callers are responsible for handling this error @@ -384,7 +402,7 @@ func (wfe *WebFrontEndImpl) writeJsonResponse(response http.ResponseWriter, logE if err != nil { // Don't worry about returning this error because the caller will // never handle it. - wfe.log.Warningf("Could not write response: %s", err) + wfe.log.Warn(ctx, "Could not write response", blog.Error(err)) logEvent.AddError("failed to write response: %s", err) } return nil @@ -549,7 +567,7 @@ func (wfe *WebFrontEndImpl) Directory( directoryEndpoints["renewalInfo"] = strings.TrimRight(renewalInfoPath, "/") if request.Method == http.MethodPost { - acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + acct, err := wfe.validPOSTAsGETForAccount(ctx, request, logEvent) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return @@ -609,7 +627,7 @@ func (wfe *WebFrontEndImpl) Nonce( response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPost { - acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + acct, err := wfe.validPOSTAsGETForAccount(ctx, request, logEvent) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return @@ -752,7 +770,7 @@ func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, ip netip. return func() { _, err := wfe.limiter.BatchRefund(ctx, txns) if err != nil { - wfe.log.Warningf("refunding new account limits: %s", err) + wfe.log.Warn(ctx, "refunding new account limits", blog.Error(err)) } }, nil } @@ -806,7 +824,7 @@ func (wfe *WebFrontEndImpl) NewAccount( return } - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, acct) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, acct) if err != nil { // ServerInternal because we just created this account, and it // should be OK. @@ -931,7 +949,7 @@ func (wfe *WebFrontEndImpl) NewAccount( response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } - err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, acct) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusCreated, acct) if err != nil { // ServerInternal because we just created this account, and it // should be OK. @@ -1011,13 +1029,6 @@ func (wfe *WebFrontEndImpl) parseRevocation( return parsedCertificate, reason, nil } -type revocationEvidence struct { - Serial string - Reason revocation.Reason - Requester int64 - Method string -} - // revokeCertBySubscriberKey processes an outer JWS as a revocation request that // is authenticated by a KeyID and the associated account. func (wfe *WebFrontEndImpl) revokeCertBySubscriberKey( @@ -1026,14 +1037,8 @@ func (wfe *WebFrontEndImpl) revokeCertBySubscriberKey( request *http.Request, logEvent *web.RequestEvent) error { // For Key ID revocations we authenticate the outer JWS by using - // `validJWSForAccount` similar to other WFE endpoints - jwsBody, _, acct, err := wfe.validJWSForAccountUsing( - outerJWS, - request, - ctx, - logEvent, - wfe.sa, - ) + // `validJWSForAccount` similar to other WFE endpoints, bypassing the cache. + jwsBody, _, acct, err := wfe.validJWSForAccount(ctx, outerJWS, request, wfe.sa, logEvent) if err != nil { return err } @@ -1043,12 +1048,12 @@ func (wfe *WebFrontEndImpl) revokeCertBySubscriberKey( return err } - wfe.log.AuditInfo("Authenticated revocation", revocationEvidence{ - Serial: core.SerialToString(cert.SerialNumber), - Reason: reason, - Requester: acct.ID, - Method: "applicant", - }) + wfe.log.AuditInfo(ctx, "Authenticated revocation", + blog.Acct(acct.ID), + blog.Serial(core.SerialToString(cert.SerialNumber)), + slog.Int64("reason", int64(reason)), + slog.String("method", "applicant"), + ) // The RA will confirm that the authenticated account either originally // issued the certificate, or has demonstrated control over all identifiers @@ -1096,12 +1101,11 @@ func (wfe *WebFrontEndImpl) revokeCertByCertKey( "JWK embedded in revocation request must be the same public key as the cert to be revoked") } - wfe.log.AuditInfo("Authenticated revocation", revocationEvidence{ - Serial: core.SerialToString(cert.SerialNumber), - Reason: reason, - Requester: 0, - Method: "privkey", - }) + wfe.log.AuditInfo(ctx, "Authenticated revocation", + blog.Serial(core.SerialToString(cert.SerialNumber)), + slog.Int64("reason", int64(reason)), + slog.String("method", "privkey"), + ) // The RA assumes here that the WFE2 has validated the JWS as proving // control of the private key corresponding to this certificate. @@ -1126,7 +1130,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate( // The ACME specification handles the verification of revocation requests // differently from other endpoints. For this reason we do *not* immediately - // authenticate the request against an account like most other endpoints. + // call `wfe.validPOSTForAccount` like all of the other endpoints. // For this endpoint we need to accept a JWS with an embedded JWK, or a JWS // with an embedded key ID, handling each case differently in terms of which // certificates are authorized to be revoked by the requester @@ -1246,7 +1250,7 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay( challenge *core.Challenge, ) { // Update the challenge URL to be relative to the HTTP request Host - challenge.URL = web.RelativeEndpoint(request, challengePath, fmt.Sprintf("%d", authz.RegistrationID), authz.ID, challenge.StringID()) + challenge.URL = web.RelativeEndpoint(request, challengePath, fmt.Sprintf("%d", authz.RegistrationID), fmt.Sprintf("%d", authz.ID), challenge.StringID()) // Internally, we store challenge error problems with just the short form // (e.g. "CAA") of the problem type. But for external display, we need to @@ -1328,7 +1332,7 @@ func (wfe *WebFrontEndImpl) getChallenge( response.Header().Add("Location", challenge.URL) response.Header().Add("Link", link(authzURL, "up")) - err := wfe.writeJsonResponse(response, logEvent, http.StatusOK, challenge) + err := wfe.writeJsonResponse(request.Context(), response, logEvent, http.StatusOK, challenge) if err != nil { // InternalServerError because this is a failure to decode data passed in // by the caller, which got it from the DB. @@ -1344,9 +1348,10 @@ func (wfe *WebFrontEndImpl) postChallenge( authz core.Authorization, challengeIndex int, logEvent *web.RequestEvent) { - body, _, currAcct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + body, _, currAcct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { + // validPOSTForAccount handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -1421,7 +1426,7 @@ func (wfe *WebFrontEndImpl) postChallenge( response.Header().Add("Location", challenge.URL) response.Header().Add("Link", link(authzURL, "up")) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, challenge) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, challenge) if err != nil { // ServerInternal because we made the challenges, they should be OK wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err) @@ -1435,9 +1440,10 @@ func (wfe *WebFrontEndImpl) Account( logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - body, _, currAcct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + body, _, currAcct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { + // validPOSTForAccount handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -1476,7 +1482,7 @@ func (wfe *WebFrontEndImpl) Account( response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, acct) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, acct) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal account"), err) return @@ -1590,7 +1596,7 @@ func (wfe *WebFrontEndImpl) Authorization( // B) a POST-as-GET to query the authorization details if request.Method == "POST" { // Both POST options need to be authenticated by an account - body, _, acct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) @@ -1663,7 +1669,7 @@ func (wfe *WebFrontEndImpl) Authorization( wfe.prepAuthorizationForDisplay(request, &authz) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, authz) if err != nil { // InternalServerError because this is a failure to decode from our DB. wfe.sendError(response, logEvent, probs.ServerInternal("Failed to JSON marshal authz"), err) @@ -1692,7 +1698,7 @@ func (wfe *WebFrontEndImpl) CertificateInfo(ctx context.Context, logEvent *web.R }{ NotAfter: metadata.Expires.AsTime(), } - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, certInfoStruct) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, certInfoStruct) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Error marshalling certInfoStruct"), err) return @@ -1706,7 +1712,7 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque // Any POSTs to the Certificate endpoint should be POST-as-GET requests. There are // no POSTs with a body allowed for this endpoint. if request.Method == "POST" { - acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + acct, err := wfe.validPOSTAsGETForAccount(ctx, request, logEvent) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return @@ -1852,7 +1858,7 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque response.Header().Set("Content-Type", "application/pem-certificate-chain") response.WriteHeader(http.StatusOK) if _, err = response.Write(responsePEM); err != nil { - wfe.log.Warningf("Could not write response: %s", err) + wfe.log.Warn(ctx, "Could not write response", blog.Error(err)) } } @@ -1862,7 +1868,7 @@ func (wfe *WebFrontEndImpl) BuildID(ctx context.Context, logEvent *web.RequestEv response.WriteHeader(http.StatusOK) detailsString := fmt.Sprintf("Boulder=(%s %s)", core.GetBuildID(), core.GetBuildTime()) if _, err := fmt.Fprintln(response, detailsString); err != nil { - wfe.log.Warningf("Could not write response: %s", err) + wfe.log.Warn(ctx, "Could not write response", blog.Error(err)) } } @@ -1882,12 +1888,12 @@ func (wfe *WebFrontEndImpl) Healthz(ctx context.Context, logEvent *web.RequestEv jsonResponse, err := json.Marshal(WfeHealthzResponse{Details: details}) if err != nil { - wfe.log.Warningf("Could not marshal healthz response: %s", err) + wfe.log.Warn(ctx, "Could not marshal healthz response", blog.Error(err)) } - err = wfe.writeJsonResponse(response, logEvent, status, jsonResponse) + err = wfe.writeJsonResponse(ctx, response, logEvent, status, jsonResponse) if err != nil { - wfe.log.Warningf("Could not write response: %s", err) + wfe.log.Warn(ctx, "Could not write response", blog.Error(err)) } } @@ -1960,8 +1966,8 @@ func (wfe *WebFrontEndImpl) KeyRollover( logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - // Validate the outer JWS against current account state before changing keys. - outerBody, outerJWS, acct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + // Validate the outer JWS on the key rollover in standard fashion. + outerBody, outerJWS, acct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) @@ -2062,7 +2068,7 @@ func (wfe *WebFrontEndImpl) KeyRollover( } wfe.purgeCachedAccount(updatedAcct.ID) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, updatedAcct) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, updatedAcct) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal updated account"), err) } @@ -2099,11 +2105,11 @@ func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corep if order.Error != nil { prob, err := bgrpc.PBToProblemDetails(order.Error) if err != nil { - wfe.log.AuditErr("Failed to serialize order problem details", err, map[string]any{ - "requester": order.RegistrationID, - "order": order.Id, - "prob": order.Error.String(), - }) + wfe.log.AuditError(request.Context(), "Failed to serialize order problem details", err, + blog.Acct(order.RegistrationID), + blog.Order(order.Id), + slog.String("prob", order.Error.String()), + ) } respObj.Error = prob respObj.Error.Type = probs.ErrorNS + respObj.Error.Type @@ -2148,7 +2154,7 @@ func (wfe *WebFrontEndImpl) checkNewOrderLimits(ctx context.Context, regId int64 return func() { _, err := wfe.limiter.BatchRefund(ctx, txns) if err != nil { - wfe.log.Warningf("refunding new order limits: %s", err) + wfe.log.Warn(ctx, "refunding new order limits", blog.Error(err)) } }, nil } @@ -2348,9 +2354,10 @@ func (wfe *WebFrontEndImpl) NewOrder( logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - body, _, acct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { + // validPOSTForAccount handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -2383,6 +2390,7 @@ func (wfe *WebFrontEndImpl) NewOrder( } idents := newOrderRequest.Identifiers + var totalIdentifierLen int for _, ident := range idents { if !ident.Type.IsValid() { wfe.sendError(response, logEvent, @@ -2395,7 +2403,15 @@ func (wfe *WebFrontEndImpl) NewOrder( wfe.sendError(response, logEvent, probs.Malformed("NewOrder request included empty identifier"), nil) return } + totalIdentifierLen += len(ident.Value) + if wfe.maxCumulativeIdentifierLength != 0 && totalIdentifierLen > wfe.maxCumulativeIdentifierLength { + wfe.sendError(response, logEvent, + probs.Malformed("Cumulative length of all identifier values was greater than %d bytes", + wfe.maxCumulativeIdentifierLength), nil) + return + } } + idents = identifier.Normalize(idents) logEvent.Identifiers = idents @@ -2405,6 +2421,14 @@ func (wfe *WebFrontEndImpl) NewOrder( return } + if wfe.accountBlocker != nil { + err = wfe.accountBlocker.CheckAccountID(acct.ID) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Account blocked"), err) + return + } + } + if features.Get().CheckIdentifiersPaused { pausedValues, err := wfe.checkIdentifiersPaused(ctx, idents, acct.ID) if err != nil { @@ -2512,7 +2536,7 @@ func (wfe *WebFrontEndImpl) NewOrder( response.Header().Set("Location", orderURL) respObj := wfe.orderToOrderJSON(request, order) - err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, respObj) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusCreated, respObj) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling order"), err) return @@ -2526,7 +2550,7 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE // Any POSTs to the Order endpoint should be POST-as-GET requests. There are // no POSTs with a body allowed for this endpoint. if request.Method == http.MethodPost { - acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + acct, err := wfe.validPOSTAsGETForAccount(ctx, request, logEvent) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return @@ -2590,7 +2614,7 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE fmt.Sprintf("%d", acctID), fmt.Sprintf("%d", order.Id)) response.Header().Set("Location", orderURL) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, respObj) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, respObj) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling order"), err) return @@ -2603,7 +2627,7 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { // Validate the POST body signature and get the authenticated account for this // finalize order request - body, _, acct, err := wfe.validPOSTForCurrentAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(ctx, request, logEvent) addRequesterHeader(response, logEvent.Requester) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) @@ -2725,7 +2749,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req response.Header().Set(headerRetryAfter, strconv.Itoa(orderRetryAfter)) } - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, respObj) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, respObj) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Unable to write finalize order response"), err) return @@ -2797,7 +2821,7 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque } response.Header().Set(headerRetryAfter, jitterRetryHeader(6*time.Hour)) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, renewalInfo) + err = wfe.writeJsonResponse(ctx, response, logEvent, http.StatusOK, renewalInfo) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Error marshalling renewalInfo"), err) return @@ -2805,7 +2829,7 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque } func urlForAuthz(authz core.Authorization, request *http.Request) string { - return web.RelativeEndpoint(request, authzPath, fmt.Sprintf("%d", authz.RegistrationID), authz.ID) + return web.RelativeEndpoint(request, authzPath, fmt.Sprintf("%d", authz.RegistrationID), fmt.Sprintf("%d", authz.ID)) } // jitterRetryHeader will return a string formatted random integer of seconds within a 20% window of the diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index a4e6efdd44d..5c9b16c480a 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -36,6 +36,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/blog" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" @@ -45,7 +46,6 @@ import ( "github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" - blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/must" @@ -429,6 +429,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { 10*time.Second, 10*time.Second, 2, + 1000, &MockRegistrationAuthority{clk: fc}, mockSA, nil, @@ -443,6 +444,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { unpauseLifetime, unpauseURL, []string{"asdf"}, + nil, "letsencrypt.org", ) test.AssertNotError(t, err, "Unable to create WFE") @@ -2367,7 +2369,7 @@ func TestGetCertificate(t *testing.T) { test.Assert(t, bytes.Equal(bodyBytes, tc.ExpectedCert), "Certificates don't match") // Successful requests should be logged as such - reqlogs := mockLog.GetAllMatching(`INFO: [^ ]+ [^ ]+ [^ ]+ 200 .*`) + reqlogs := mockLog.GetAllMatching(`level=INFO .* code=200`) if len(reqlogs) != 1 { t.Errorf("Didn't find info logs with code 200. Instead got:\n%s\n", strings.Join(mockLog.GetAllMatching(`.*`), "\n")) @@ -2378,7 +2380,7 @@ func TestGetCertificate(t *testing.T) { test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody) // Unsuccessful requests should be logged as such - reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`INFO: [^ ]+ [^ ]+ [^ ]+ %d .*`, tc.ExpectedStatus)) + reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`level=INFO .* code=%d`, tc.ExpectedStatus)) if len(reqlogs) != 1 { t.Errorf("Didn't find info logs with code %d. Instead got:\n%s\n", tc.ExpectedStatus, strings.Join(mockLog.GetAllMatching(`.*`), "\n")) @@ -2524,7 +2526,7 @@ func TestGetCertificateNew(t *testing.T) { test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody) // Unsuccessful requests should be logged as such - reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`INFO: [^ ]+ [^ ]+ [^ ]+ %d .*`, tc.ExpectedStatus)) + reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`level=INFO .* code=%d`, tc.ExpectedStatus)) if len(reqlogs) != 1 { t.Errorf("Didn't find info logs with code %d. Instead got:\n%s\n", tc.ExpectedStatus, strings.Join(mockLog.GetAllMatching(`.*`), "\n")) @@ -3373,9 +3375,15 @@ func TestRevokeCertificateByApplicantValid(t *testing.T) { test.AssertEquals(t, responseWriter.Code, 200) test.AssertEquals(t, responseWriter.Body.String(), "") - test.AssertDeepEquals(t, mockLog.GetAllMatching("Authenticated revocation"), []string{ - `INFO: [AUDIT] Authenticated revocation JSON={"Serial":"000000000000000000001d72443db5189821","Reason":0,"Requester":1,"Method":"applicant"}`, - }) + matches := mockLog.GetAllMatching("Authenticated revocation") + test.AssertEquals(t, len(matches), 1) + test.AssertContains(t, matches[0], `level=INFO`) + test.AssertContains(t, matches[0], `[AUDIT]`) + test.AssertContains(t, matches[0], `msg="Authenticated revocation"`) + test.AssertContains(t, matches[0], `serial=000000000000000000001d72443db5189821`) + test.AssertContains(t, matches[0], `reason=0`) + test.AssertContains(t, matches[0], `method=applicant`) + test.AssertContains(t, matches[0], `acct=1`) } // Valid revocation request for existing, non-revoked cert, signed using the @@ -3402,9 +3410,14 @@ func TestRevokeCertificateByKeyValid(t *testing.T) { test.AssertEquals(t, responseWriter.Code, 200) test.AssertEquals(t, responseWriter.Body.String(), "") - test.AssertDeepEquals(t, mockLog.GetAllMatching("Authenticated revocation"), []string{ - `INFO: [AUDIT] Authenticated revocation JSON={"Serial":"000000000000000000001d72443db5189821","Reason":1,"Requester":0,"Method":"privkey"}`, - }) + matches := mockLog.GetAllMatching("Authenticated revocation") + test.AssertEquals(t, len(matches), 1) + test.AssertContains(t, matches[0], `level=INFO`) + test.AssertContains(t, matches[0], `[AUDIT]`) + test.AssertContains(t, matches[0], `msg="Authenticated revocation"`) + test.AssertContains(t, matches[0], `serial=000000000000000000001d72443db5189821`) + test.AssertContains(t, matches[0], `reason=1`) + test.AssertContains(t, matches[0], `method=privkey`) } // Invalid revocation request: although signed with the cert key, the cert @@ -3633,7 +3646,7 @@ func TestPrepAuthzForDisplay(t *testing.T) { wfe, _, _ := setupWFE(t) authz := &core.Authorization{ - ID: "12345", + ID: 12345, Status: core.StatusPending, RegistrationID: 1, Identifier: identifier.NewDNS("example.com"), @@ -3672,7 +3685,7 @@ func TestPrepRevokedAuthzForDisplay(t *testing.T) { wfe, _, _ := setupWFE(t) authz := &core.Authorization{ - ID: "12345", + ID: 12345, Status: core.StatusInvalid, RegistrationID: 1, Identifier: identifier.NewDNS("example.com"), @@ -3698,7 +3711,7 @@ func TestPrepWildcardAuthzForDisplay(t *testing.T) { wfe, _, _ := setupWFE(t) authz := &core.Authorization{ - ID: "12345", + ID: 12345, Status: core.StatusPending, RegistrationID: 1, Identifier: identifier.NewDNS("*.example.com"), @@ -3721,7 +3734,7 @@ func TestPrepAuthzForDisplayShuffle(t *testing.T) { wfe, _, _ := setupWFE(t) authz := &core.Authorization{ - ID: "12345", + ID: 12345, Status: core.StatusPending, RegistrationID: 1, Identifier: identifier.NewDNS("example.com"), @@ -4521,3 +4534,84 @@ func TestLooksLikeRecursiveOnDemandRequest(t *testing.T) { }) } } + +type acctBlock struct{} + +func (ab *acctBlock) CheckAccountID(id int64) error { + return berrors.UnauthorizedError("oh no") +} + +func TestAccountBlocker(t *testing.T) { + t.Parallel() + wfe, _, signer := setupWFE(t) + mux := wfe.Handler(metrics.NoopRegisterer) + + wfe.accountBlocker = new(acctBlock) + + responseWriter := httptest.NewRecorder() + r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, ` + { + "Identifiers": [ + {"type": "dns", "value": "example.com"} + ] + }`) + mux.ServeHTTP(responseWriter, r) + if responseWriter.Code != http.StatusForbidden { + t.Fatalf("newOrder with blocked account: got %d, want %d; %s", responseWriter.Code, http.StatusForbidden, + responseWriter.Body.String()) + } + var errorResp1 map[string]any + err := json.Unmarshal(responseWriter.Body.Bytes(), &errorResp1) + if err != nil { + t.Fatalf("newOrder with blocked account: got error unmarshaling response: %s", err) + } + detail := errorResp1["detail"] + expected := "Account blocked :: oh no" + if detail != expected { + t.Errorf("newOrder with blocked account: got %q, want %q", detail, expected) + } +} + +func TestMaxCumulativeIdentifierLength(t *testing.T) { + t.Parallel() + wfe, _, signer := setupWFE(t) + mux := wfe.Handler(metrics.NoopRegisterer) + + // Test that the newOrder endpoint returns no error if the valid profile is specified. + responseWriter := httptest.NewRecorder() + + order := struct { + Identifiers []identifier.ACMEIdentifier + }{} + + const alphabet = "abcdefghijklmnopqrstuvwxyz" + + for i := 0; i < 25; i++ { + order.Identifiers = append(order.Identifiers, + identifier.NewDNS( + fmt.Sprintf("%d.%s.%s.%s.%s.example.com", i, + alphabet, alphabet, alphabet, alphabet))) + } + + orderBytes, err := json.Marshal(order) + if err != nil { + t.Fatalf("marshaling JSON: %s", err) + } + + r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, string(orderBytes)) + mux.ServeHTTP(responseWriter, r) + if responseWriter.Code != http.StatusBadRequest { + t.Fatalf("newOrder with too long identifiers: got %d, want %d; %s", responseWriter.Code, http.StatusBadRequest, + responseWriter.Body.String()) + } + var errorResp1 map[string]any + err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp1) + if err != nil { + t.Fatalf("newOrder with too long identifiers: got error unmarshaling response: %s", err) + } + detail := errorResp1["detail"] + expected := "Cumulative length of all identifier values was greater than 1000 bytes" + if detail != expected { + t.Errorf("newOrder with too long identifiers: got %q, want %q", detail, expected) + } +}