diff --git a/services/httpd/handler.go b/services/httpd/handler.go index 587c39dd9f3..53551754218 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -44,6 +44,8 @@ import ( "go.uber.org/zap" ) +var ErrDiagnosticsValueMissing = errors.New("expected diagnostic value missing") + const ( // DefaultChunkSize specifies the maximum number of points that will // be read before sending results back to the engine. @@ -2249,6 +2251,40 @@ func (h *Handler) serveExpvar(w http.ResponseWriter, r *http.Request) { first = false fmt.Fprintf(w, "\"cmdline\": %s", val) } + + // We're going to print some kind of crypto data, we just + // need to find the proper source for it. + { + var jv map[string]interface{} + val := diags["crypto"] + if val != nil { + jv, err = parseCryptoDiagnostics(val) + if err != nil { + if errors.Is(err, ErrDiagnosticsValueMissing) { + // log missing values, but don't error out + h.Logger.Warn(err.Error()) + } else { + h.httpError(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + jv = ossCryptoDiagnostics() + } + + data, err := json.Marshal(jv) + if err != nil { + h.httpError(w, err.Error(), http.StatusInternalServerError) + return + } + + if !first { + fmt.Fprintln(w, ",") + } + first = false + fmt.Fprintf(w, "\"crypto\": %s", data) + } + if val := expvar.Get("memstats"); val != nil { if !first { fmt.Fprintln(w, ",") @@ -2433,6 +2469,55 @@ func parseBuildInfo(d *diagnostics.Diagnostics) (map[string]interface{}, error) return m, nil } +// ossCryptoDiagnostics creates a default crypto diagnostics map that +// can be marshaled into JSON for /debug/vars. +func ossCryptoDiagnostics() map[string]interface{} { + return map[string]interface{}{ + "ensureFIPS": false, + "FIPS": false, + "implementation": "Go", + "passwordHash": "bcrypt", + } +} + +// parseCryptoDiagnostics converts the crypto diagnostics into an appropriate +// format for marshaling to JSON in the /debug/vars format. +func parseCryptoDiagnostics(d *diagnostics.Diagnostics) (map[string]interface{}, error) { + // We use ossCryptoDiagnostics as a template for columns we need to pull from d. + // If the column is missing from d, we will nil out the value in m to avoid lying + // about a value to the user and making troubleshooting harder. + m := ossCryptoDiagnostics() + var missing []string + + for key := range m { + // Find the associated column. + ci := -1 + for i, col := range d.Columns { + if col == key { + ci = i + break + } + } + + // Don't error out if we can't find the column or cell for a given key, just nil + // out the value in m. There could still be other useful information we gather. + // Column not found or data cell not found + if ci == -1 || len(d.Rows) < 1 || len(d.Rows[0]) <= ci { + m[key] = nil + missing = append(missing, key) + continue + } + + m[key] = d.Rows[0][ci] + } + + if len(missing) > 0 { + // If you're getting this error, you probably need to update enterprise. + return m, fmt.Errorf("parseCryptoDiagnostics: missing %s: %w", strings.Join(missing, ","), ErrDiagnosticsValueMissing) + } + return m, nil +} + // httpError writes an error to the client in a standard format. func (h *Handler) httpError(w http.ResponseWriter, errmsg string, code int) { if code == http.StatusUnauthorized { diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index 60f1c40970f..31475f0ccbb 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -2616,6 +2616,12 @@ func TestHandlerDebugVars(t *testing.T) { return res } + newDiagFn := func(d map[string]*diagnostics.Diagnostics) func() (map[string]*diagnostics.Diagnostics, error) { + return func() (map[string]*diagnostics.Diagnostics, error) { + return d, nil + } + } + var Ignored = []string{"memstats", "cmdline"} read := func(t *testing.T, b *bytes.Buffer, del ...string) map[string]interface{} { t.Helper() @@ -2652,17 +2658,19 @@ func TestHandlerDebugVars(t *testing.T) { stat("shard", tags("path", "/mnt/foo", "id", "111"), nil), ) } + h.Monitor.DiagnosticsFn = newDiagFn(map[string]*diagnostics.Diagnostics{}) req := MustNewRequest("GET", "/debug/vars", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) got := keys(read(t, w.Body, Ignored...)) - exp := []string{"database:foo", "hh:/mnt/foo/bar", "httpd:https:127.0.0.1:8088", "other", "shard:/mnt/foo:111"} + exp := []string{"crypto", "database:foo", "hh:/mnt/foo/bar", "httpd:https:127.0.0.1:8088", "other", "shard:/mnt/foo:111"} if !cmp.Equal(got, exp) { t.Errorf("unexpected keys; -got/+exp\n%s", cmp.Diff(got, exp)) } }) t.Run("generates numbered keys for collisions", func(t *testing.T) { + // This also implicitly tests the case where no `crypto` diagnostics are not set by application. h := NewHandler(false) h.Monitor.StatisticsFn = func(_ map[string]string) ([]*monitor.Statistic, error) { return stats( @@ -2677,6 +2685,12 @@ func TestHandlerDebugVars(t *testing.T) { h.ServeHTTP(w, req) got := read(t, w.Body, Ignored...) exp := map[string]interface{}{ + "crypto": map[string]interface{}{ + "FIPS": false, + "ensureFIPS": false, + "passwordHash": "bcrypt", + "implementation": "Go", + }, "hh_processor": map[string]interface{}{ "name": "hh_processor", "tags": map[string]interface{}{"db": "foo", "shardID": "10"}, @@ -2703,6 +2717,35 @@ func TestHandlerDebugVars(t *testing.T) { } }) }) + + t.Run("checks crypto diagnostic handling", func(t *testing.T) { + h := NewHandler(false) + // intentionally leave out "ensureFIPS" to test that code path + h.Monitor.DiagnosticsFn = newDiagFn( + map[string]*diagnostics.Diagnostics{ + "crypto": diagnostics.RowFromMap(map[string]interface{}{ + "FIPS": true, + "passwordHash": "pbkdf2-sha256", + "implementation": "BoringCrypto", + }), + }) + req := MustNewRequest("GET", "/debug/vars", nil) + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + got := read(t, w.Body, Ignored...) + exp := map[string]interface{}{ + "crypto": map[string]interface{}{ + "FIPS": true, + "ensureFIPS": nil, + "passwordHash": "pbkdf2-sha256", + "implementation": "BoringCrypto", + }, + } + if !cmp.Equal(got, exp) { + t.Errorf("unexpected keys; -got/+exp\n%s", cmp.Diff(got, exp)) + } + }) + } // NewHandler represents a test wrapper for httpd.Handler.