Skip to content

Commit

Permalink
feat: include crypto diagnostics in /debug/vars output (#23948)
Browse files Browse the repository at this point in the history
* feat: include `crypto` diagnostics in `/debug/vars` output

Pulls `crypto` diagnostics and includes them in `/debug/vars` output.
If no `crypto` diagnostics are available, then OSS crypto information will
be shown instead.

closes: #23947
  • Loading branch information
gwossum authored Dec 6, 2022
1 parent e484c4d commit ac35088
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 1 deletion.
85 changes: 85 additions & 0 deletions services/httpd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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, ",")
Expand Down Expand Up @@ -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 {
Expand Down
45 changes: 44 additions & 1 deletion services/httpd/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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"},
Expand All @@ -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.
Expand Down

0 comments on commit ac35088

Please sign in to comment.