feat: formatted JSON and certificates, errors in the status pane, no scope access list #19
No files matched your search
@@ -313,7 +313,8 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
case ValueShown:
|
case ValueShown:
|
||||||
// Where the dots were, continuing under itself; the keys below.
|
// Where the dots were, continuing under itself; the keys below.
|
||||||
first := true
|
first := true
|
||||||
for _, line := range strings.Split(d.value.Reveal(), "\n") {
|
lines, note := valueView(d.value.Reveal())
|
||||||
|
for _, line := range lines {
|
||||||
for _, w := range strings.Split(ansi.Hardwrap(line, max(1, inner-11), true), "\n") {
|
for _, w := range strings.Split(ansi.Hardwrap(line, max(1, inner-11), true), "\n") {
|
||||||
if first {
|
if first {
|
||||||
out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+st.Value.Render(w))
|
out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+st.Value.Render(w))
|
||||||
@@ -324,6 +325,12 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var more []string
|
var more []string
|
||||||
|
if note != "" && d.CopyKey != "" {
|
||||||
|
note += ": " + d.CopyKey + " copies it as stored"
|
||||||
|
}
|
||||||
|
if note != "" {
|
||||||
|
more = append(more, note)
|
||||||
|
}
|
||||||
if d.hidesIn != "" {
|
if d.hidesIn != "" {
|
||||||
more = append(more, "hides in "+d.hidesIn)
|
more = append(more, "hides in "+d.hidesIn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package panes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A revealed value is shown as it reads best: JSON indented, and PEM
|
||||||
|
// certificates as what they say (subject, issuer, dates, names) rather
|
||||||
|
// than base64, with any other PEM block, a private key say, named but not
|
||||||
|
// shown. Anything else is shown as it is. y copies it as stored.
|
||||||
|
|
||||||
|
// valueView is how to show v, and what's been done to it ("" when it's
|
||||||
|
// shown as it is).
|
||||||
|
func valueView(v string) (lines []string, note string) {
|
||||||
|
if lines, ok := pemView(v); ok {
|
||||||
|
return lines, "certificate"
|
||||||
|
}
|
||||||
|
if t := strings.TrimSpace(v); (strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[")) && json.Valid([]byte(t)) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if json.Indent(&b, []byte(t), "", " ") == nil {
|
||||||
|
return strings.Split(b.String(), "\n"), "JSON, formatted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Split(v, "\n"), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// pemView describes the PEM blocks in v, if it's PEM with a certificate.
|
||||||
|
func pemView(v string) ([]string, bool) {
|
||||||
|
rest := []byte(strings.TrimSpace(v))
|
||||||
|
var out []string
|
||||||
|
certs := 0
|
||||||
|
for len(rest) > 0 {
|
||||||
|
block, after := pem.Decode(rest)
|
||||||
|
if block == nil {
|
||||||
|
return nil, false // text that isn't PEM: show it all
|
||||||
|
}
|
||||||
|
rest = bytes.TrimSpace(after)
|
||||||
|
if len(out) > 0 {
|
||||||
|
out = append(out, "")
|
||||||
|
}
|
||||||
|
if block.Type != "CERTIFICATE" {
|
||||||
|
out = append(out, block.Type+" (not shown)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
certs++
|
||||||
|
out = append(out, certLines(cert)...)
|
||||||
|
}
|
||||||
|
return out, certs > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func certLines(c *x509.Certificate) []string {
|
||||||
|
day := func(t time.Time) string { return t.UTC().Format("2006-01-02") }
|
||||||
|
out := []string{
|
||||||
|
"subject " + c.Subject.String(),
|
||||||
|
"issuer " + c.Issuer.String(),
|
||||||
|
"valid " + day(c.NotBefore) + " to " + day(c.NotAfter),
|
||||||
|
}
|
||||||
|
names := append([]string{}, c.DNSNames...)
|
||||||
|
for _, ip := range c.IPAddresses {
|
||||||
|
names = append(names, ip.String())
|
||||||
|
}
|
||||||
|
names = append(names, c.EmailAddresses...)
|
||||||
|
if len(names) > 0 {
|
||||||
|
out = append(out, "names "+strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
sum := sha1.Sum(c.Raw) // what Azure and Windows call the thumbprint
|
||||||
|
return append(out, "SHA-1 "+fmt.Sprintf("%X", sum[:]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package panes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValueViewJSON(t *testing.T) {
|
||||||
|
lines, note := valueView(`{"user":"app","port":5432,"tags":["a"]}`)
|
||||||
|
want := []string{"{", ` "user": "app",`, ` "port": 5432,`, ` "tags": [`, ` "a"`, " ]", "}"}
|
||||||
|
if !slices.Equal(lines, want) || note != "JSON, formatted" {
|
||||||
|
t.Errorf("lines %q, note %q", lines, note)
|
||||||
|
}
|
||||||
|
for _, v := range []string{"plain value", "{not json", "42", "line one\nline two"} {
|
||||||
|
if lines, note := valueView(v); note != "" || strings.Join(lines, "\n") != v {
|
||||||
|
t.Errorf("%q shown as %q (%q)", v, lines, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValueViewCertificate(t *testing.T) {
|
||||||
|
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "app.example.com"},
|
||||||
|
DNSNames: []string{"app.example.com", "www.app.example.com"},
|
||||||
|
NotBefore: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
keyDER, _ := x509.MarshalECPrivateKey(key)
|
||||||
|
bundle := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) +
|
||||||
|
string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))
|
||||||
|
|
||||||
|
lines, note := valueView(bundle)
|
||||||
|
text := strings.Join(lines, "\n")
|
||||||
|
for _, want := range []string{"subject CN=app.example.com", "valid 2026-01-01 to 2027-01-01",
|
||||||
|
"names app.example.com, www.app.example.com", "SHA-1 ", "EC PRIVATE KEY (not shown)"} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Errorf("missing %q:\n%s", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if note != "certificate" || strings.Contains(text, "BEGIN") {
|
||||||
|
t.Errorf("note %q, or PEM shown:\n%s", note, text)
|
||||||
|
}
|
||||||
|
// A private key alone isn't a certificate: shown as it is.
|
||||||
|
if _, note := valueView(string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))); note != "" {
|
||||||
|
t.Errorf("a key alone: note %q", note)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user