mirror of
https://github.com/hanzo-ds/go.git
synced 2026-10-30 19:45:01 +00:00
The server reads X-Datastore-User/-Key/-SSL-Certificate-Auth verbatim and defines no alias for the upstream vendor spelling, so the TLS branch of applyOptionsToRequest could not authenticate at all: the server ignored the headers, fell back to the default user and answered 401. Only the TLS branch was affected. The plaintext branch sets req.URL.User, which net/http sends as HTTP Basic, and Basic authenticates fine — which is why every current consumer works and this stayed latent. A DSN with scheme https or secure=true routes into the broken branch. The three names are now declared once in headers.go, and covered by tests that exercise the branch over a real TLS round trip. Also reports ClientName as datastore-go; it is a self-reported label that lands in system.query_log, and nothing filters on its value.
123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package datastore
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// The Datastore HTTP interface reads X-Datastore-* verbatim and defines no
|
|
// alias for the upstream vendor spelling, so a client that sends anything else
|
|
// cannot authenticate. This branch is reachable only when opt.TLS != nil — the
|
|
// plaintext branch authenticates with HTTP Basic via req.URL.User instead and
|
|
// therefore cannot cover it.
|
|
func TestApplyOptionsToRequestTLSAuthHeaders(t *testing.T) {
|
|
newReq := func(t *testing.T) *http.Request {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodPost, "https://datastore.invalid/", nil)
|
|
if err != nil {
|
|
t.Fatalf("new request: %v", err)
|
|
}
|
|
return req
|
|
}
|
|
|
|
t.Run("username and password", func(t *testing.T) {
|
|
req := newReq(t)
|
|
opt := &Options{TLS: &tls.Config{}}
|
|
opt.Auth.Username = "reader"
|
|
opt.Auth.Password = "s3cret"
|
|
if err := applyOptionsToRequest(context.Background(), req, opt); err != nil {
|
|
t.Fatalf("applyOptionsToRequest: %v", err)
|
|
}
|
|
|
|
if got := req.Header.Get("X-Datastore-User"); got != "reader" {
|
|
t.Errorf("X-Datastore-User = %q, want %q", got, "reader")
|
|
}
|
|
if got := req.Header.Get("X-Datastore-Key"); got != "s3cret" {
|
|
t.Errorf("X-Datastore-Key = %q, want %q", got, "s3cret")
|
|
}
|
|
if got := req.Header.Get("X-Datastore-SSL-Certificate-Auth"); got != "off" {
|
|
t.Errorf("X-Datastore-SSL-Certificate-Auth = %q, want %q", got, "off")
|
|
}
|
|
})
|
|
|
|
t.Run("username only selects certificate auth", func(t *testing.T) {
|
|
req := newReq(t)
|
|
opt := &Options{TLS: &tls.Config{}}
|
|
opt.Auth.Username = "reader"
|
|
if err := applyOptionsToRequest(context.Background(), req, opt); err != nil {
|
|
t.Fatalf("applyOptionsToRequest: %v", err)
|
|
}
|
|
if got := req.Header.Get("X-Datastore-SSL-Certificate-Auth"); got != "on" {
|
|
t.Errorf("X-Datastore-SSL-Certificate-Auth = %q, want %q", got, "on")
|
|
}
|
|
})
|
|
|
|
// A regression guard: no request may carry the upstream vendor spelling,
|
|
// which the server answers with 401.
|
|
t.Run("no vendor branded headers", func(t *testing.T) {
|
|
req := newReq(t)
|
|
opt := &Options{TLS: &tls.Config{}}
|
|
opt.Auth.Username = "reader"
|
|
opt.Auth.Password = "s3cret"
|
|
if err := applyOptionsToRequest(context.Background(), req, opt); err != nil {
|
|
t.Fatalf("applyOptionsToRequest: %v", err)
|
|
}
|
|
for name := range req.Header {
|
|
if http.CanonicalHeaderKey(name) == http.CanonicalHeaderKey("X-ClickHouse-User") ||
|
|
http.CanonicalHeaderKey(name) == http.CanonicalHeaderKey("X-ClickHouse-Key") ||
|
|
http.CanonicalHeaderKey(name) == http.CanonicalHeaderKey("X-ClickHouse-SSL-Certificate-Auth") {
|
|
t.Errorf("request carries un-renamed header %q", name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// End to end over real TLS: the headers must survive an actual https round trip
|
|
// with the driver's own transport, not just a hand built http.Request.
|
|
func TestHTTPConnectSendsDatastoreAuthHeadersOverTLS(t *testing.T) {
|
|
got := make(chan http.Header, 1)
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case got <- r.Header.Clone():
|
|
default:
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL, nil)
|
|
if err != nil {
|
|
t.Fatalf("new request: %v", err)
|
|
}
|
|
opt := &Options{TLS: &tls.Config{InsecureSkipVerify: true}}
|
|
opt.Auth.Username = "reader"
|
|
opt.Auth.Password = "s3cret"
|
|
if err := applyOptionsToRequest(context.Background(), req, opt); err != nil {
|
|
t.Fatalf("applyOptionsToRequest: %v", err)
|
|
}
|
|
|
|
client := srv.Client()
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("do: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
h := <-got
|
|
if h.Get("X-Datastore-User") != "reader" || h.Get("X-Datastore-Key") != "s3cret" {
|
|
t.Fatalf("server saw %v, want X-Datastore-User/Key", h)
|
|
}
|
|
if h.Get("X-ClickHouse-User") != "" || h.Get("X-ClickHouse-Key") != "" {
|
|
t.Fatalf("server saw un-renamed vendor headers: %v", h)
|
|
}
|
|
}
|
|
|
|
func TestClientNameIsDatastore(t *testing.T) {
|
|
if ClientName != "datastore-go" {
|
|
t.Errorf("ClientName = %q, want %q", ClientName, "datastore-go")
|
|
}
|
|
}
|