Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rqlite for openSUSE:Factory checked in at 2025-03-17 22:17:28 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rqlite (Old) and /work/SRC/openSUSE:Factory/.rqlite.new.19136 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rqlite" Mon Mar 17 22:17:28 2025 rev:11 rq:1253514 version:8.36.14 Changes: -------- --- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes 2025-03-13 15:07:42.974022297 +0100 +++ /work/SRC/openSUSE:Factory/.rqlite.new.19136/rqlite.changes 2025-03-17 22:21:24.324033725 +0100 @@ -1,0 +2,7 @@ +Sun Mar 16 13:46:03 UTC 2025 - Andreas Stieger <andreas.stie...@gmx.de> + +- Update to version 8.36.14: + * Support SQL-format loading via Follower + * Minor refactor of HTTP credential handling + +------------------------------------------------------------------- Old: ---- rqlite-8.36.13.tar.xz New: ---- rqlite-8.36.14.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rqlite.spec ++++++ --- /var/tmp/diff_new_pack.1cCsku/_old 2025-03-17 22:21:24.860056115 +0100 +++ /var/tmp/diff_new_pack.1cCsku/_new 2025-03-17 22:21:24.864056283 +0100 @@ -17,7 +17,7 @@ Name: rqlite -Version: 8.36.13 +Version: 8.36.14 Release: 0 Summary: Distributed relational database built on SQLite License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.1cCsku/_old 2025-03-17 22:21:24.900057786 +0100 +++ /var/tmp/diff_new_pack.1cCsku/_new 2025-03-17 22:21:24.904057953 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rqlite/rqlite.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v8.36.13</param> + <param name="revision">v8.36.14</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.1cCsku/_old 2025-03-17 22:21:24.924058789 +0100 +++ /var/tmp/diff_new_pack.1cCsku/_new 2025-03-17 22:21:24.928058955 +0100 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rqlite/rqlite.git</param> - <param name="changesrevision">15827cba077aeef4815f6a7ee04b8427e16c85f1</param> + <param name="changesrevision">000abd80ae237adc7e538e2e800d80a099861f7e</param> </service> </servicedata> (No newline at EOF) ++++++ rqlite-8.36.13.tar.xz -> rqlite-8.36.14.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/CHANGELOG.md new/rqlite-8.36.14/CHANGELOG.md --- old/rqlite-8.36.13/CHANGELOG.md 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/CHANGELOG.md 2025-03-15 16:18:40.000000000 +0100 @@ -1,3 +1,9 @@ +## v8.36.14 (March 15th 2025) +### Implementation changes and bug fixes +- [PR #2056](https://github.com/rqlite/rqlite/pull/2056): Minor refactoring of HTTP credentials handling. +- [PR #2054](https://github.com/rqlite/rqlite/pull/2054): Bump golang.org/x/net from 0.35.0 to 0.36.0. +- [PR #2055](https://github.com/rqlite/rqlite/pull/2055): Support SQL-format loading via Follower. Fixes issue [#2053](https://github.com/rqlite/rqlite/issues/2053). + ## v8.36.13 (March 11th 2025) ### Implementation changes and bug fixes - [PR #2051](https://github.com/rqlite/rqlite/pull/2051): System-level test of SQL format backups. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/cluster/client.go new/rqlite-8.36.14/cluster/client.go --- old/rqlite-8.36.13/cluster/client.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/cluster/client.go 2025-03-15 16:18:40.000000000 +0100 @@ -169,7 +169,7 @@ return a.CommitIndex, nil } -// Execute performs an Execute on a remote node. If username is an empty string +// Execute performs an Execute on a remote node. If creds is nil, then // no credential information will be included in the Execute request to the // remote node. func (c *Client) Execute(er *command.ExecuteRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration, retries int) ([]*command.ExecuteQueryResponse, error) { @@ -198,7 +198,9 @@ return a.Response, nil } -// Query performs a Query on a remote node. +// Query performs a Query on a remote node. If creds is nil, then +// no credential information will be included in the Query request to the +// remote node. func (c *Client) Query(qr *command.QueryRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration) ([]*command.QueryRows, error) { command := &proto.Command{ Type: proto.Command_COMMAND_TYPE_QUERY, @@ -225,7 +227,9 @@ return a.Rows, nil } -// Request performs an ExecuteQuery on a remote node. +// Request performs an ExecuteQuery on a remote node. If creds is nil, then +// no credential information will be included in the ExecuteQuery request to the +// remote node. func (c *Client) Request(r *command.ExecuteQueryRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration, retries int) ([]*command.ExecuteQueryResponse, error) { command := &proto.Command{ Type: proto.Command_COMMAND_TYPE_REQUEST, @@ -252,7 +256,9 @@ return a.Response, nil } -// Backup retrieves a backup from a remote node and writes to the io.Writer +// Backup retrieves a backup from a remote node and writes to the io.Writer. +// If creds is nil, then no credential information will be included in the +// Backup request to the remote node. func (c *Client) Backup(br *command.BackupRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration, w io.Writer) error { conn, err := c.dial(nodeAddr) if err != nil { @@ -305,7 +311,8 @@ return err } -// Load loads a SQLite file into the database. +// Load loads a SQLite file into the database. If creds is nil, then no +// credential information will be included in the Load request to the remote node. func (c *Client) Load(lr *command.LoadRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration, retries int) error { command := &proto.Command{ Type: proto.Command_COMMAND_TYPE_LOAD, @@ -332,7 +339,9 @@ return nil } -// RemoveNode removes a node from the cluster +// RemoveNode removes a node from the cluster. If creds is nil, then no +// credential information will be included in the RemoveNode request to the +// remote node. func (c *Client) RemoveNode(rn *command.RemoveNodeRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration) error { conn, err := c.dial(nodeAddr) if err != nil { @@ -372,6 +381,8 @@ } // Notify notifies a remote node that this node is ready to bootstrap. +// If creds is nil, then no credential information will be included in +// // the Notify request to the remote node. func (c *Client) Notify(nr *command.NotifyRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration) error { conn, err := c.dial(nodeAddr) if err != nil { @@ -411,6 +422,8 @@ } // Join joins this node to a cluster at the remote address nodeAddr. +// If creds is nil, then no credential information will be included in +// the Join request to the remote node. func (c *Client) Join(jr *command.JoinRequest, nodeAddr string, creds *proto.Credentials, timeout time.Duration) error { for { conn, err := c.dial(nodeAddr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/cluster/proto/message.pb_test.go new/rqlite-8.36.14/cluster/proto/message.pb_test.go --- old/rqlite-8.36.13/cluster/proto/message.pb_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/rqlite-8.36.14/cluster/proto/message.pb_test.go 2025-03-15 16:18:40.000000000 +0100 @@ -0,0 +1,17 @@ +package proto + +import ( + "testing" +) + +// Test_Credentials tests the Credentials methods on a nil Credentials, +// ensuring it doesn't panic and instead returns empty strings. +func Test_NilCredentials(t *testing.T) { + var creds *Credentials + if creds.GetUsername() != "" { + t.Fatalf("expected empty username") + } + if creds.GetPassword() != "" { + t.Fatalf("expected empty password") + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/cluster/service.go new/rqlite-8.36.14/cluster/service.go --- old/rqlite-8.36.13/cluster/service.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/cluster/service.go 2025-03-15 16:18:40.000000000 +0100 @@ -250,14 +250,7 @@ if s.credentialStore == nil { return true } - - username := "" - password := "" - if c.Credentials != nil { - username = c.Credentials.GetUsername() - password = c.Credentials.GetPassword() - } - return s.credentialStore.AA(username, password, perm) + return s.credentialStore.AA(c.Credentials.GetUsername(), c.Credentials.GetPassword(), perm) } func (s *Service) checkCommandPermAll(c *proto.Command, perms ...string) bool { @@ -265,14 +258,8 @@ return true } - username := "" - password := "" - if c.Credentials != nil { - username = c.Credentials.GetUsername() - password = c.Credentials.GetPassword() - } for _, perm := range perms { - if !s.credentialStore.AA(username, password, perm) { + if !s.credentialStore.AA(c.Credentials.GetUsername(), c.Credentials.GetPassword(), perm) { return false } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/go.mod new/rqlite-8.36.14/go.mod --- old/rqlite-8.36.13/go.mod 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/go.mod 2025-03-15 16:18:40.000000000 +0100 @@ -20,7 +20,7 @@ github.com/rqlite/rqlite-disco-clients v0.0.0-20250205044118-8ada2b350099 github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196 go.etcd.io/bbolt v1.4.0 - golang.org/x/net v0.35.0 + golang.org/x/net v0.36.0 google.golang.org/protobuf v1.36.5 ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/go.sum new/rqlite-8.36.14/go.sum --- old/rqlite-8.36.13/go.sum 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/go.sum 2025-03-15 16:18:40.000000000 +0100 @@ -309,8 +309,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/http/service.go new/rqlite-8.36.14/http/service.go --- old/rqlite-8.36.13/http/service.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/http/service.go 2025-03-15 16:18:40.000000000 +0100 @@ -595,13 +595,8 @@ return } - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } - w.Header().Add(ServedByHTTPHeader, addr) - removeErr := s.cluster.RemoveNode(rn, addr, makeCredentials(username, password), qp.Timeout(defaultTimeout)) + removeErr := s.cluster.RemoveNode(rn, addr, makeCredentials(r), qp.Timeout(defaultTimeout)) if removeErr != nil { if removeErr.Error() == "unauthorized" { http.Error(w, "remote remove node not authorized", http.StatusUnauthorized) @@ -657,13 +652,8 @@ return } - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } - w.Header().Add(ServedByHTTPHeader, addr) - backupErr := s.cluster.Backup(br, addr, makeCredentials(username, password), qp.Timeout(defaultTimeout), w) + backupErr := s.cluster.Backup(br, addr, makeCredentials(r), qp.Timeout(defaultTimeout), w) if backupErr != nil { if backupErr.Error() == "unauthorized" { http.Error(w, "remote backup not authorized", http.StatusUnauthorized) @@ -697,6 +687,27 @@ return } + // Determine some perhaps-needed details. + ldrAddr, err := s.store.LeaderAddr() + if err != nil { + http.Error(w, fmt.Sprintf("leader address: %s", err.Error()), + http.StatusInternalServerError) + return + } + if ldrAddr == "" { + stats.Add(numLeaderNotFound, 1) + http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) + return + } + + handleRemoteErr := func(err error) { + if err.Error() == "unauthorized" { + http.Error(w, "remote load not authorized", http.StatusUnauthorized) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + resp := NewResponse() b, err := io.ReadAll(r.Body) if err != nil { @@ -720,32 +731,10 @@ return } - addr, err := s.store.LeaderAddr() - if err != nil { - http.Error(w, fmt.Sprintf("leader address: %s", err.Error()), - http.StatusInternalServerError) - return - } - if addr == "" { - stats.Add(numLeaderNotFound, 1) - http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) - return - } - - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } - - w.Header().Add(ServedByHTTPHeader, addr) - loadErr := s.cluster.Load(lr, addr, makeCredentials(username, password), - qp.Timeout(defaultTimeout), qp.Retries(0)) + w.Header().Add(ServedByHTTPHeader, ldrAddr) + loadErr := s.cluster.Load(lr, ldrAddr, makeCredentials(r), qp.Timeout(defaultTimeout), qp.Retries(0)) if loadErr != nil { - if loadErr.Error() == "unauthorized" { - http.Error(w, "remote load not authorized", http.StatusUnauthorized) - } else { - http.Error(w, loadErr.Error(), http.StatusInternalServerError) - } + handleRemoteErr(loadErr) return } stats.Add(numRemoteLoads, 1) @@ -753,7 +742,7 @@ // forwarding was put in place. } } else { - // No JSON structure expected for this API. + // No JSON structure expected for this API, just a bunch of SQL statements. queries := []string{string(b)} er := executeRequestFromStrings(queries, qp.Timings(), false) @@ -763,9 +752,24 @@ if s.DoRedirect(w, r, qp) { return } + + w.Header().Add(ServedByHTTPHeader, ldrAddr) + var exErr error + response, exErr = s.cluster.Execute(er, ldrAddr, makeCredentials(r), + qp.Timeout(defaultTimeout), qp.Retries(0)) + if exErr != nil { + handleRemoteErr(exErr) + return + } + resp.Results.ExecuteQueryResponse = response + stats.Add(numRemoteLoads, 1) + } else { + // Local execute failed for some reason other than not + // being the leader. Nothing we can do here. + resp.Error = err.Error() } - resp.Error = err.Error() } else { + // Successful local execute. resp.Results.ExecuteQueryResponse = response } resp.end = time.Now() @@ -1206,13 +1210,8 @@ return } - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } - w.Header().Add(ServedByHTTPHeader, addr) - results, resultsErr = s.cluster.Execute(er, addr, makeCredentials(username, password), + results, resultsErr = s.cluster.Execute(er, addr, makeCredentials(r), qp.Timeout(defaultTimeout), qp.Retries(0)) if resultsErr != nil { stats.Add(numRemoteExecutionsFailed, 1) @@ -1316,13 +1315,9 @@ http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) return } - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } w.Header().Add(ServedByHTTPHeader, addr) - results, resultsErr = s.cluster.Query(qr, addr, makeCredentials(username, password), qp.Timeout(defaultTimeout)) + results, resultsErr = s.cluster.Query(qr, addr, makeCredentials(r), qp.Timeout(defaultTimeout)) if resultsErr != nil { stats.Add(numRemoteQueriesFailed, 1) if resultsErr.Error() == "unauthorized" { @@ -1403,13 +1398,9 @@ http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable) return } - username, password, ok := r.BasicAuth() - if !ok { - username = "" - } w.Header().Add(ServedByHTTPHeader, addr) - results, resultsErr = s.cluster.Request(eqr, addr, makeCredentials(username, password), + results, resultsErr = s.cluster.Request(eqr, addr, makeCredentials(r), qp.Timeout(defaultTimeout), qp.Retries(0)) if resultsErr != nil { stats.Add(numRemoteRequestsFailed, 1) @@ -1793,7 +1784,11 @@ } } -func makeCredentials(username, password string) *clstrPB.Credentials { +func makeCredentials(r *http.Request) *clstrPB.Credentials { + username, password, ok := r.BasicAuth() + if !ok { + return nil + } return &clstrPB.Credentials{ Username: username, Password: password, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/http/service_test.go new/rqlite-8.36.14/http/service_test.go --- old/rqlite-8.36.13/http/service_test.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/http/service_test.go 2025-03-15 16:18:40.000000000 +0100 @@ -709,7 +709,9 @@ } func Test_LoadOK(t *testing.T) { - m := &MockStore{} + m := &MockStore{ + leaderAddr: "foo:1234", + } c := &mockClusterService{} s := New("127.0.0.1:0", m, c, nil) if err := s.Start(); err != nil { @@ -725,7 +727,7 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - t.Fatalf("failed to get expected StatusOK for load, got %d", resp.StatusCode) + t.Fatalf("failed to get expected StatusOK for load, got %d, %s", resp.StatusCode, mustReadBody(t, resp)) } if exp, got := `{"results":[]}`, mustReadBody(t, resp); exp != got { t.Fatalf("incorrect response body, exp: %s, got %s", exp, got) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/system_test/cluster_test.go new/rqlite-8.36.14/system_test/cluster_test.go --- old/rqlite-8.36.13/system_test/cluster_test.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/system_test/cluster_test.go 2025-03-15 16:18:40.000000000 +0100 @@ -5,6 +5,7 @@ "fmt" "net" "os" + "path/filepath" "sync" "testing" "time" @@ -1456,6 +1457,39 @@ } } +func Test_MultiNodeCluster_FollowerLoad_SQL(t *testing.T) { + node1 := mustNewLeaderNode("leader1") + defer node1.Deprovision() + + node2 := mustNewNode("node2", false) + defer node2.Deprovision() + if err := node2.Join(node1); err != nil { + t.Fatalf("node failed to join leader: %s", err.Error()) + } + _, err := node2.WaitForLeader() + if err != nil { + t.Fatalf("failed waiting for leader: %s", err.Error()) + } + + // Get a follower, make sure Load works via it. + c := Cluster{node1, node2} + followers, err := c.Followers() + if err != nil { + t.Fatalf("failed to get followers: %s", err.Error()) + } + if _, err := followers[0].Load(filepath.Join("testdata", "auto-restore.sql")); err != nil { + t.Fatalf("failed to load via follower: %s", err.Error()) + } + + r, err := node1.QueryStrongConsistency("SELECT * FROM foo WHERE id=2") + if err != nil { + t.Fatalf("failed to execute query: %s", err.Error()) + } + if r != `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[2,"fiona"]]}]}` { + t.Fatalf("test received wrong result got %s", r) + } +} + // Test_MultiNodeClusterWithNonVoter tests formation of a 4-node cluster, one of which is // a non-voter. This test also checks that if the Leader changes the non-voter is still in // the cluster and gets updates from the new leader. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/system_test/helpers.go new/rqlite-8.36.14/system_test/helpers.go --- old/rqlite-8.36.13/system_test/helpers.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/system_test/helpers.go 2025-03-15 16:18:40.000000000 +0100 @@ -267,7 +267,7 @@ return err } -// Load loads a SQLite database file into the node. +// Boot boots a node using a SQLite database file. func (n *Node) Boot(filename string) (string, error) { return n.postFile("/boot", filename) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/system_test/single_node_test.go new/rqlite-8.36.14/system_test/single_node_test.go --- old/rqlite-8.36.13/system_test/single_node_test.go 2025-03-12 04:47:36.000000000 +0100 +++ new/rqlite-8.36.14/system_test/single_node_test.go 2025-03-15 16:18:40.000000000 +0100 @@ -1823,3 +1823,21 @@ t.Fatalf("expected error loading data") } } + +func Test_SingleNodeLoad_OK(t *testing.T) { + node := mustNewLeaderNode("leader1") + defer node.Deprovision() + + _, err := node.Load(filepath.Join("testdata", "auto-restore.sql")) + if err != nil { + t.Fatalf("failed to load data: %s", err.Error()) + } + + r, err := node.Query("SELECT * FROM foo WHERE id=2") + if err != nil { + t.Fatalf("failed to execute query: %s", err.Error()) + } + if r != `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[2,"fiona"]]}]}` { + t.Fatalf("test received wrong result got %s", r) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-8.36.13/system_test/testdata/auto-restore.sql new/rqlite-8.36.14/system_test/testdata/auto-restore.sql --- old/rqlite-8.36.13/system_test/testdata/auto-restore.sql 1970-01-01 01:00:00.000000000 +0100 +++ new/rqlite-8.36.14/system_test/testdata/auto-restore.sql 2025-03-15 16:18:40.000000000 +0100 @@ -0,0 +1,7 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE foo (id integer not null primary key, name text); +INSERT INTO foo VALUES(1,'fiona'); +INSERT INTO foo VALUES(2,'fiona'); +INSERT INTO foo VALUES(3,'fiona'); +COMMIT; ++++++ vendor.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/golang.org/x/net/http2/server.go new/vendor/golang.org/x/net/http2/server.go --- old/vendor/golang.org/x/net/http2/server.go 2025-03-12 21:36:00.000000000 +0100 +++ new/vendor/golang.org/x/net/http2/server.go 2025-03-16 14:43:35.000000000 +0100 @@ -2233,25 +2233,25 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*responseWriter, *http.Request, error) { sc.serveG.check() - rp := requestParam{ - method: f.PseudoValue("method"), - scheme: f.PseudoValue("scheme"), - authority: f.PseudoValue("authority"), - path: f.PseudoValue("path"), - protocol: f.PseudoValue("protocol"), + rp := httpcommon.ServerRequestParam{ + Method: f.PseudoValue("method"), + Scheme: f.PseudoValue("scheme"), + Authority: f.PseudoValue("authority"), + Path: f.PseudoValue("path"), + Protocol: f.PseudoValue("protocol"), } // extended connect is disabled, so we should not see :protocol - if disableExtendedConnectProtocol && rp.protocol != "" { + if disableExtendedConnectProtocol && rp.Protocol != "" { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } - isConnect := rp.method == "CONNECT" + isConnect := rp.Method == "CONNECT" if isConnect { - if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") { + if rp.Protocol == "" && (rp.Path != "" || rp.Scheme != "" || rp.Authority == "") { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } - } else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") { + } else if rp.Method == "" || rp.Path == "" || (rp.Scheme != "https" && rp.Scheme != "http") { // See 8.1.2.6 Malformed Requests and Responses: // // Malformed requests or responses that are detected @@ -2265,15 +2265,16 @@ return nil, nil, sc.countError("bad_path_method", streamError(f.StreamID, ErrCodeProtocol)) } - rp.header = make(http.Header) + header := make(http.Header) + rp.Header = header for _, hf := range f.RegularFields() { - rp.header.Add(sc.canonicalHeader(hf.Name), hf.Value) + header.Add(sc.canonicalHeader(hf.Name), hf.Value) } - if rp.authority == "" { - rp.authority = rp.header.Get("Host") + if rp.Authority == "" { + rp.Authority = header.Get("Host") } - if rp.protocol != "" { - rp.header.Set(":protocol", rp.protocol) + if rp.Protocol != "" { + header.Set(":protocol", rp.Protocol) } rw, req, err := sc.newWriterAndRequestNoBody(st, rp) @@ -2282,7 +2283,7 @@ } bodyOpen := !f.StreamEnded() if bodyOpen { - if vv, ok := rp.header["Content-Length"]; ok { + if vv, ok := rp.Header["Content-Length"]; ok { if cl, err := strconv.ParseUint(vv[0], 10, 63); err == nil { req.ContentLength = int64(cl) } else { @@ -2298,84 +2299,38 @@ return rw, req, nil } -type requestParam struct { - method string - scheme, authority, path string - protocol string - header http.Header -} - -func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*responseWriter, *http.Request, error) { +func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.ServerRequestParam) (*responseWriter, *http.Request, error) { sc.serveG.check() var tlsState *tls.ConnectionState // nil if not scheme https - if rp.scheme == "https" { + if rp.Scheme == "https" { tlsState = sc.tlsState } - needsContinue := httpguts.HeaderValuesContainsToken(rp.header["Expect"], "100-continue") - if needsContinue { - rp.header.Del("Expect") - } - // Merge Cookie headers into one "; "-delimited value. - if cookies := rp.header["Cookie"]; len(cookies) > 1 { - rp.header.Set("Cookie", strings.Join(cookies, "; ")) - } - - // Setup Trailers - var trailer http.Header - for _, v := range rp.header["Trailer"] { - for _, key := range strings.Split(v, ",") { - key = http.CanonicalHeaderKey(textproto.TrimString(key)) - switch key { - case "Transfer-Encoding", "Trailer", "Content-Length": - // Bogus. (copy of http1 rules) - // Ignore. - default: - if trailer == nil { - trailer = make(http.Header) - } - trailer[key] = nil - } - } - } - delete(rp.header, "Trailer") - - var url_ *url.URL - var requestURI string - if rp.method == "CONNECT" && rp.protocol == "" { - url_ = &url.URL{Host: rp.authority} - requestURI = rp.authority // mimic HTTP/1 server behavior - } else { - var err error - url_, err = url.ParseRequestURI(rp.path) - if err != nil { - return nil, nil, sc.countError("bad_path", streamError(st.id, ErrCodeProtocol)) - } - requestURI = rp.path + res := httpcommon.NewServerRequest(rp) + if res.InvalidReason != "" { + return nil, nil, sc.countError(res.InvalidReason, streamError(st.id, ErrCodeProtocol)) } body := &requestBody{ conn: sc, stream: st, - needsContinue: needsContinue, + needsContinue: res.NeedsContinue, } - req := &http.Request{ - Method: rp.method, - URL: url_, + req := (&http.Request{ + Method: rp.Method, + URL: res.URL, RemoteAddr: sc.remoteAddrStr, - Header: rp.header, - RequestURI: requestURI, + Header: rp.Header, + RequestURI: res.RequestURI, Proto: "HTTP/2.0", ProtoMajor: 2, ProtoMinor: 0, TLS: tlsState, - Host: rp.authority, + Host: rp.Authority, Body: body, - Trailer: trailer, - } - req = req.WithContext(st.ctx) - + Trailer: res.Trailer, + }).WithContext(st.ctx) rw := sc.newResponseWriter(st, req) return rw, req, nil } @@ -3270,12 +3225,12 @@ // we start in "half closed (remote)" for simplicity. // See further comments at the definition of stateHalfClosedRemote. promised := sc.newStream(promisedID, msg.parent.id, stateHalfClosedRemote) - rw, req, err := sc.newWriterAndRequestNoBody(promised, requestParam{ - method: msg.method, - scheme: msg.url.Scheme, - authority: msg.url.Host, - path: msg.url.RequestURI(), - header: cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE + rw, req, err := sc.newWriterAndRequestNoBody(promised, httpcommon.ServerRequestParam{ + Method: msg.method, + Scheme: msg.url.Scheme, + Authority: msg.url.Host, + Path: msg.url.RequestURI(), + Header: cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE }) if err != nil { // Should not happen, since we've already validated msg.url. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/golang.org/x/net/http2/transport.go new/vendor/golang.org/x/net/http2/transport.go --- old/vendor/golang.org/x/net/http2/transport.go 2025-03-12 21:36:00.000000000 +0100 +++ new/vendor/golang.org/x/net/http2/transport.go 2025-03-16 14:43:35.000000000 +0100 @@ -1286,6 +1286,19 @@ return 0 } +// actualContentLength returns a sanitized version of +// req.ContentLength, where 0 actually means zero (not unknown) and -1 +// means unknown. +func actualContentLength(req *http.Request) int64 { + if req.Body == nil || req.Body == http.NoBody { + return 0 + } + if req.ContentLength != 0 { + return req.ContentLength + } + return -1 +} + func (cc *ClientConn) decrStreamReservations() { cc.mu.Lock() defer cc.mu.Unlock() @@ -1310,7 +1323,7 @@ reqCancel: req.Cancel, isHead: req.Method == "HEAD", reqBody: req.Body, - reqBodyContentLength: httpcommon.ActualContentLength(req), + reqBodyContentLength: actualContentLength(req), trace: httptrace.ContextClientTrace(ctx), peerClosed: make(chan struct{}), abort: make(chan struct{}), @@ -1318,7 +1331,7 @@ donec: make(chan struct{}), } - cs.requestedGzip = httpcommon.IsRequestGzip(req, cc.t.disableCompression()) + cs.requestedGzip = httpcommon.IsRequestGzip(req.Method, req.Header, cc.t.disableCompression()) go cs.doRequest(req, streamf) @@ -1349,7 +1362,7 @@ } res.Request = req res.TLS = cc.tlsState - if res.Body == noBody && httpcommon.ActualContentLength(req) == 0 { + if res.Body == noBody && actualContentLength(req) == 0 { // If there isn't a request or response body still being // written, then wait for the stream to be closed before // RoundTrip returns. @@ -1596,12 +1609,7 @@ // sent by writeRequestBody below, along with any Trailers, // again in form HEADERS{1}, CONTINUATION{0,}) cc.hbuf.Reset() - res, err := httpcommon.EncodeHeaders(httpcommon.EncodeHeadersParam{ - Request: req, - AddGzipHeader: cs.requestedGzip, - PeerMaxHeaderListSize: cc.peerMaxHeaderListSize, - DefaultUserAgent: defaultUserAgent, - }, func(name, value string) { + res, err := encodeRequestHeaders(req, cs.requestedGzip, cc.peerMaxHeaderListSize, func(name, value string) { cc.writeHeader(name, value) }) if err != nil { @@ -1617,6 +1625,22 @@ return err } +func encodeRequestHeaders(req *http.Request, addGzipHeader bool, peerMaxHeaderListSize uint64, headerf func(name, value string)) (httpcommon.EncodeHeadersResult, error) { + return httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{ + Request: httpcommon.Request{ + Header: req.Header, + Trailer: req.Trailer, + URL: req.URL, + Host: req.Host, + Method: req.Method, + ActualContentLength: actualContentLength(req), + }, + AddGzipHeader: addGzipHeader, + PeerMaxHeaderListSize: peerMaxHeaderListSize, + DefaultUserAgent: defaultUserAgent, + }, headerf) +} + // cleanupWriteRequest performs post-request tasks. // // If err (the result of writeRequest) is non-nil and the stream is not closed, @@ -2186,6 +2210,13 @@ } cc.cond.Broadcast() cc.mu.Unlock() + + if !cc.seenSettings { + // If we have a pending request that wants extended CONNECT, + // let it continue and fail with the connection error. + cc.extendedConnectAllowed = true + close(cc.seenSettingsChan) + } } // countReadFrameError calls Transport.CountError with a string @@ -2278,9 +2309,6 @@ if VerboseLogs { cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err) } - if !cc.seenSettings { - close(cc.seenSettingsChan) - } return err } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/golang.org/x/net/internal/httpcommon/headermap.go new/vendor/golang.org/x/net/internal/httpcommon/headermap.go --- old/vendor/golang.org/x/net/internal/httpcommon/headermap.go 2025-03-12 21:36:00.000000000 +0100 +++ new/vendor/golang.org/x/net/internal/httpcommon/headermap.go 2025-03-16 14:43:35.000000000 +0100 @@ -5,7 +5,7 @@ package httpcommon import ( - "net/http" + "net/textproto" "sync" ) @@ -82,7 +82,7 @@ commonLowerHeader = make(map[string]string, len(common)) commonCanonHeader = make(map[string]string, len(common)) for _, v := range common { - chk := http.CanonicalHeaderKey(v) + chk := textproto.CanonicalMIMEHeaderKey(v) commonLowerHeader[chk] = v commonCanonHeader[v] = chk } @@ -104,7 +104,7 @@ if s, ok := commonCanonHeader[v]; ok { return s } - return http.CanonicalHeaderKey(v) + return textproto.CanonicalMIMEHeaderKey(v) } // CachedCanonicalHeader returns the canonical form of a well-known header name. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/golang.org/x/net/internal/httpcommon/request.go new/vendor/golang.org/x/net/internal/httpcommon/request.go --- old/vendor/golang.org/x/net/internal/httpcommon/request.go 2025-03-12 21:36:00.000000000 +0100 +++ new/vendor/golang.org/x/net/internal/httpcommon/request.go 2025-03-16 14:43:35.000000000 +0100 @@ -5,10 +5,12 @@ package httpcommon import ( + "context" "errors" "fmt" - "net/http" "net/http/httptrace" + "net/textproto" + "net/url" "sort" "strconv" "strings" @@ -21,9 +23,21 @@ ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit") ) +// Request is a subset of http.Request. +// It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http +// without creating a dependency cycle. +type Request struct { + URL *url.URL + Method string + Host string + Header map[string][]string + Trailer map[string][]string + ActualContentLength int64 // 0 means 0, -1 means unknown +} + // EncodeHeadersParam is parameters to EncodeHeaders. type EncodeHeadersParam struct { - Request *http.Request + Request Request // AddGzipHeader indicates that an "accept-encoding: gzip" header should be // added to the request. @@ -47,11 +61,11 @@ // It validates a request and calls headerf with each pseudo-header and header // for the request. // The headerf function is called with the validated, canonicalized header name. -func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) { +func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) { req := param.Request // Check for invalid connection-level headers. - if err := checkConnHeaders(req); err != nil { + if err := checkConnHeaders(req.Header); err != nil { return res, err } @@ -73,7 +87,10 @@ // isNormalConnect is true if this is a non-extended CONNECT request. isNormalConnect := false - protocol := req.Header.Get(":protocol") + var protocol string + if vv := req.Header[":protocol"]; len(vv) > 0 { + protocol = vv[0] + } if req.Method == "CONNECT" && protocol == "" { isNormalConnect = true } else if protocol != "" && req.Method != "CONNECT" { @@ -107,9 +124,7 @@ return res, fmt.Errorf("invalid HTTP trailer %s", err) } - contentLength := ActualContentLength(req) - - trailers, err := commaSeparatedTrailers(req) + trailers, err := commaSeparatedTrailers(req.Trailer) if err != nil { return res, err } @@ -123,7 +138,7 @@ f(":authority", host) m := req.Method if m == "" { - m = http.MethodGet + m = "GET" } f(":method", m) if !isNormalConnect { @@ -198,8 +213,8 @@ f(k, v) } } - if shouldSendReqContentLength(req.Method, contentLength) { - f("content-length", strconv.FormatInt(contentLength, 10)) + if shouldSendReqContentLength(req.Method, req.ActualContentLength) { + f("content-length", strconv.FormatInt(req.ActualContentLength, 10)) } if param.AddGzipHeader { f("accept-encoding", "gzip") @@ -225,7 +240,7 @@ } } - trace := httptrace.ContextClientTrace(req.Context()) + trace := httptrace.ContextClientTrace(ctx) // Header list size is ok. Write the headers. enumerateHeaders(func(name, value string) { @@ -243,19 +258,19 @@ } }) - res.HasBody = contentLength != 0 + res.HasBody = req.ActualContentLength != 0 res.HasTrailers = trailers != "" return res, nil } // IsRequestGzip reports whether we should add an Accept-Encoding: gzip header // for a request. -func IsRequestGzip(req *http.Request, disableCompression bool) bool { +func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool { // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? if !disableCompression && - req.Header.Get("Accept-Encoding") == "" && - req.Header.Get("Range") == "" && - req.Method != "HEAD" { + len(header["Accept-Encoding"]) == 0 && + len(header["Range"]) == 0 && + method != "HEAD" { // Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: https://zlib.net/zlib_faq.html#faq39 @@ -280,22 +295,22 @@ // // Certain headers are special-cased as okay but not transmitted later. // For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding. -func checkConnHeaders(req *http.Request) error { - if v := req.Header.Get("Upgrade"); v != "" { - return fmt.Errorf("invalid Upgrade request header: %q", req.Header["Upgrade"]) +func checkConnHeaders(h map[string][]string) error { + if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") { + return fmt.Errorf("invalid Upgrade request header: %q", vv) } - if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") { + if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") { return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv) } - if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) { + if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) { return fmt.Errorf("invalid Connection request header: %q", vv) } return nil } -func commaSeparatedTrailers(req *http.Request) (string, error) { - keys := make([]string, 0, len(req.Trailer)) - for k := range req.Trailer { +func commaSeparatedTrailers(trailer map[string][]string) (string, error) { + keys := make([]string, 0, len(trailer)) + for k := range trailer { k = CanonicalHeader(k) switch k { case "Transfer-Encoding", "Trailer", "Content-Length": @@ -310,19 +325,6 @@ return "", nil } -// ActualContentLength returns a sanitized version of -// req.ContentLength, where 0 actually means zero (not unknown) and -1 -// means unknown. -func ActualContentLength(req *http.Request) int64 { - if req.Body == nil || req.Body == http.NoBody { - return 0 - } - if req.ContentLength != 0 { - return req.ContentLength - } - return -1 -} - // validPseudoPath reports whether v is a valid :path pseudo-header // value. It must be either: // @@ -340,7 +342,7 @@ return (len(v) > 0 && v[0] == '/') || v == "*" } -func validateHeaders(hdrs http.Header) string { +func validateHeaders(hdrs map[string][]string) string { for k, vv := range hdrs { if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" { return fmt.Sprintf("name %q", k) @@ -377,3 +379,89 @@ return false } } + +// ServerRequestParam is parameters to NewServerRequest. +type ServerRequestParam struct { + Method string + Scheme, Authority, Path string + Protocol string + Header map[string][]string +} + +// ServerRequestResult is the result of NewServerRequest. +type ServerRequestResult struct { + // Various http.Request fields. + URL *url.URL + RequestURI string + Trailer map[string][]string + + NeedsContinue bool // client provided an "Expect: 100-continue" header + + // If the request should be rejected, this is a short string suitable for passing + // to the http2 package's CountError function. + // It might be a bit odd to return errors this way rather than returing an error, + // but this ensures we don't forget to include a CountError reason. + InvalidReason string +} + +func NewServerRequest(rp ServerRequestParam) ServerRequestResult { + needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue") + if needsContinue { + delete(rp.Header, "Expect") + } + // Merge Cookie headers into one "; "-delimited value. + if cookies := rp.Header["Cookie"]; len(cookies) > 1 { + rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")} + } + + // Setup Trailers + var trailer map[string][]string + for _, v := range rp.Header["Trailer"] { + for _, key := range strings.Split(v, ",") { + key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key)) + switch key { + case "Transfer-Encoding", "Trailer", "Content-Length": + // Bogus. (copy of http1 rules) + // Ignore. + default: + if trailer == nil { + trailer = make(map[string][]string) + } + trailer[key] = nil + } + } + } + delete(rp.Header, "Trailer") + + // "':authority' MUST NOT include the deprecated userinfo subcomponent + // for "http" or "https" schemed URIs." + // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8 + if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") { + return ServerRequestResult{ + InvalidReason: "userinfo_in_authority", + } + } + + var url_ *url.URL + var requestURI string + if rp.Method == "CONNECT" && rp.Protocol == "" { + url_ = &url.URL{Host: rp.Authority} + requestURI = rp.Authority // mimic HTTP/1 server behavior + } else { + var err error + url_, err = url.ParseRequestURI(rp.Path) + if err != nil { + return ServerRequestResult{ + InvalidReason: "bad_path", + } + } + requestURI = rp.Path + } + + return ServerRequestResult{ + URL: url_, + NeedsContinue: needsContinue, + RequestURI: requestURI, + Trailer: trailer, + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2025-03-12 21:36:01.000000000 +0100 +++ new/vendor/modules.txt 2025-03-16 14:43:39.000000000 +0100 @@ -279,8 +279,8 @@ # golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 ## explicit; go 1.23.0 golang.org/x/exp/slices -# golang.org/x/net v0.35.0 -## explicit; go 1.18 +# golang.org/x/net v0.36.0 +## explicit; go 1.23.0 golang.org/x/net/http/httpguts golang.org/x/net/http2 golang.org/x/net/http2/hpack