From c11d743fcc37d2fb95e69e55d30cb835492e7577 Mon Sep 17 00:00:00 2001 From: geemus Date: Thu, 4 Apr 2024 16:30:06 -0500 Subject: [PATCH] v0.0.20: initial windows support --- .github/workflows/release-notify.yml | 13 + .github/workflows/release.yml | 16 +- api/apitest/apitest.go | 10 +- api/apitest/apitest_unix.go | 26 ++ api/apitest/apitest_windows.go | 30 ++ auth/client.go | 2 +- auth/whoami_test.go | 5 + cert/provision.go | 18 +- go.mod | 24 +- go.sum | 48 ++-- lcl/audit_test.go | 5 + lcl/clean_test.go | 40 +++ lcl/config_test.go | 179 ++++++++++++ lcl/lcl_test.go | 70 +++-- lcl/mkcert.go | 11 +- lcl/mkcert_test.go | 46 +++ lcl/models/config.go | 4 +- lcl/setup.go | 10 +- lcl/setup_test.go | 137 +++++++++ lcl/testdata/TestAudit/basics.golden | 16 +- lcl/testdata/TestClean/basics.golden | 17 ++ lcl/testdata/TestLclConfig/basics.golden | 338 +++++++++++++++++++++++ trust/audit_test.go | 5 + trust/testdata/TestTrust/basics.golden | 22 +- trust/trust_test.go | 46 +-- truststore/audit.go | 13 + truststore/nss.go | 74 ++--- truststore/platform_windows.go | 89 +++++- ui/driver.go | 47 +++- ui/uitest/uitest.go | 2 +- version/command_test.go | 10 + 31 files changed, 1156 insertions(+), 217 deletions(-) create mode 100644 .github/workflows/release-notify.yml create mode 100644 api/apitest/apitest_unix.go create mode 100644 api/apitest/apitest_windows.go create mode 100644 lcl/clean_test.go create mode 100644 lcl/config_test.go create mode 100644 lcl/mkcert_test.go create mode 100644 lcl/setup_test.go create mode 100644 lcl/testdata/TestClean/basics.golden create mode 100644 lcl/testdata/TestLclConfig/basics.golden diff --git a/.github/workflows/release-notify.yml b/.github/workflows/release-notify.yml new file mode 100644 index 0000000..cc748dd --- /dev/null +++ b/.github/workflows/release-notify.yml @@ -0,0 +1,13 @@ +name: Release Notify + +on: + release: + types: [published] + +jobs: + discord: + runs-on: ubuntu-latest + steps: + - uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0510400..0338f00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,29 +5,27 @@ concurrency: release on: push: branches: - - main + - main jobs: - goreleaser: + release: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - - - name: Set up Go + - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: go.mod - - - name: Run GoReleaser + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: - distribution: goreleaser + distribution: goreleaser-pro version: latest args: release --clean workdir: cmd/anchor env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/api/apitest/apitest.go b/api/apitest/apitest.go index c117337..9e42f7e 100644 --- a/api/apitest/apitest.go +++ b/api/apitest/apitest.go @@ -3,7 +3,6 @@ package apitest import ( "bytes" "context" - "errors" "flag" "path/filepath" "strings" @@ -14,7 +13,6 @@ import ( "net/http" "os" "os/exec" - "syscall" "time" "github.com/creack/pty" @@ -206,10 +204,10 @@ func (s *Server) startMock(ctx context.Context) (string, func() error, error) { func (s *Server) startCmd(ctx context.Context, args []string) (func() error, error) { cmd := exec.CommandContext(ctx, args[0], args[1:]...) cmd.Dir = s.RootDir - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + setpgid(cmd) cmd.Cancel = func() error { - err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - if err != nil && !errors.Is(err, syscall.ESRCH) { + err := terminateProcess(cmd) + if err != nil { return err } return nil @@ -278,7 +276,7 @@ func (s *Server) waitTCP(addr string) error { if conn, err := net.Dial("tcp4", addr); err == nil { conn.Close() return nil - } else if !errors.Is(err, syscall.ECONNREFUSED) { + } else if !isConnRefused(err) { return err } diff --git a/api/apitest/apitest_unix.go b/api/apitest/apitest_unix.go new file mode 100644 index 0000000..6050cb2 --- /dev/null +++ b/api/apitest/apitest_unix.go @@ -0,0 +1,26 @@ +//go:build !windows + +package apitest + +import ( + "errors" + "os/exec" + "syscall" +) + +func isConnRefused(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) +} + +func setpgid(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + // cmd.SysProcAttr = &syscall.SysProcAttr{} +} + +func terminateProcess(cmd *exec.Cmd) error { + err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + if err != nil && !errors.Is(err, syscall.ESRCH) { + return err + } + return nil +} diff --git a/api/apitest/apitest_windows.go b/api/apitest/apitest_windows.go new file mode 100644 index 0000000..3f5bcb3 --- /dev/null +++ b/api/apitest/apitest_windows.go @@ -0,0 +1,30 @@ +//go:build windows + +package apitest + +import ( + "errors" + "os" + "os/exec" + "syscall" + + "golang.org/x/sys/windows" +) + +// based on: https://github.com/go-cmd/cmd/blob/500562c204744af1802ae24316a7e0bf88dcc545/cmd_windows.go + +func isConnRefused(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, windows.WSAECONNREFUSED) +} + +func setpgid(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{} +} + +func terminateProcess(cmd *exec.Cmd) error { + p, err := os.FindProcess(cmd.Process.Pid) + if err != nil { + return err + } + return p.Kill() +} diff --git a/auth/client.go b/auth/client.go index 8c93edf..3ce1619 100644 --- a/auth/client.go +++ b/auth/client.go @@ -34,7 +34,7 @@ func (c Client) Perform(ctx context.Context, drv *ui.Driver) (*api.Session, erro drv.Send(models.ClientProbed(true)) if newClientErr == nil { - _, userInfoErr := c.Anc.UserInfo(ctx) + _, userInfoErr = c.Anc.UserInfo(ctx) if userInfoErr != nil && !errors.Is(userInfoErr, api.ErrSignedOut) { return nil, userInfoErr } diff --git a/auth/whoami_test.go b/auth/whoami_test.go index 0a06d93..603f135 100644 --- a/auth/whoami_test.go +++ b/auth/whoami_test.go @@ -2,6 +2,7 @@ package auth import ( "context" + "runtime" "testing" "github.com/anchordotdev/cli" @@ -10,6 +11,10 @@ import ( ) func TestWhoAmI(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no pty support on windows") + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/cert/provision.go b/cert/provision.go index 7ee6b8a..645e7a6 100644 --- a/cert/provision.go +++ b/cert/provision.go @@ -45,8 +45,10 @@ func (p *Provision) RunTUI(ctx context.Context, drv *ui.Driver, domains ...strin Bytes: cert.Certificate[0], } - if err := os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0644); err != nil { - return err + if !p.Config.Trust.MockMode { + if err := os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0644); err != nil { + return err + } } var chainData []byte @@ -59,8 +61,10 @@ func (p *Provision) RunTUI(ctx context.Context, drv *ui.Driver, domains ...strin chainData = append(chainData, pem.EncodeToMemory(chainBlock)...) } - if err := os.WriteFile(chainFile, chainData, 0644); err != nil { - return err + if !p.Config.Trust.MockMode { + if err := os.WriteFile(chainFile, chainData, 0644); err != nil { + return err + } } keyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey) @@ -74,8 +78,10 @@ func (p *Provision) RunTUI(ctx context.Context, drv *ui.Driver, domains ...strin Bytes: keyDER, } - if err := os.WriteFile(keyFile, pem.EncodeToMemory(keyBlock), 0644); err != nil { - return err + if !p.Config.Trust.MockMode { + if err := os.WriteFile(keyFile, pem.EncodeToMemory(keyBlock), 0644); err != nil { + return err + } } drv.Send(models.ProvisionedFiles{certFile, chainFile, keyFile}) diff --git a/go.mod b/go.mod index c5995c3..253cabf 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 - github.com/charmbracelet/lipgloss v0.9.1 + github.com/charmbracelet/lipgloss v0.10.0 github.com/charmbracelet/x/exp/teatest v0.0.0-20240222131549-03ee51df8bea github.com/cli/browser v1.3.0 github.com/creack/pty v1.1.21 @@ -22,10 +22,11 @@ require ( github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/zalando/go-keyring v0.2.3 - golang.org/x/crypto v0.20.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d + github.com/zalando/go-keyring v0.2.4 + golang.org/x/crypto v0.21.0 + golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 golang.org/x/sync v0.6.0 + golang.org/x/sys v0.18.0 howett.net/plist v1.0.1 ) @@ -43,10 +44,10 @@ require ( github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.5.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -60,16 +61,15 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.4.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.16.0 // indirect + golang.org/x/tools v0.19.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index f5912b2..af99d5d 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/ github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 h1:kyT+aGp1z5jwlus3OY0cP6FuT05jYeeExx/4TYxnyrs= github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/teatest v0.0.0-20240222131549-03ee51df8bea h1:rMsCa4AcGApEidjhRpitA2HZds22ZSnAuVjx8SVF3yA= @@ -56,8 +56,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -66,8 +66,8 @@ github.com/google/go-github/v54 v54.0.0 h1:OZdXwow4EAD5jEo5qg+dGFH2DpkyZvVsAehjv github.com/google/go-github/v54 v54.0.0/go.mod h1:Sw1LXWHhXRZtzJ9LI5fyJg9wbQzYvFhW8W5P2yaAQ7s= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -124,8 +124,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= -github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -151,14 +151,14 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= +github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -167,10 +167,10 @@ golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= @@ -183,12 +183,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -198,8 +198,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= diff --git a/lcl/audit_test.go b/lcl/audit_test.go index e117c48..2475162 100644 --- a/lcl/audit_test.go +++ b/lcl/audit_test.go @@ -2,6 +2,7 @@ package lcl import ( "context" + "runtime" "testing" "github.com/anchordotdev/cli" @@ -9,6 +10,10 @@ import ( ) func TestAudit(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no pty support on windows") + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/lcl/clean_test.go b/lcl/clean_test.go new file mode 100644 index 0000000..1f7e11b --- /dev/null +++ b/lcl/clean_test.go @@ -0,0 +1,40 @@ +package lcl + +import ( + "context" + "testing" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/ui/uitest" +) + +func TestClean(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := new(cli.Config) + cfg.API.URL = srv.URL + cfg.Trust.MockMode = true + cfg.Trust.NoSudo = true + cfg.Trust.Stores = []string{"mock"} + + var err error + if cfg.API.Token, err = srv.GeneratePAT("anky@anchor.dev"); err != nil { + t.Fatal(err) + } + + t.Run("basics", func(t *testing.T) { + if srv.IsProxy() { + t.Skip("lcl clean unsupported in proxy mode") + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + cmd := LclClean{ + Config: cfg, + } + + uitest.TestTUIOutput(ctx, t, cmd.UI()) + }) +} diff --git a/lcl/config_test.go b/lcl/config_test.go new file mode 100644 index 0000000..df7426d --- /dev/null +++ b/lcl/config_test.go @@ -0,0 +1,179 @@ +package lcl + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/truststore" + "github.com/anchordotdev/cli/ui/uitest" +) + +func TestLclConfig(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + diagPort := "4433" + + diagAddr := "0.0.0.0:" + diagPort + httpURL := "http://hello-world.lcl.host:" + diagPort + httpsURL := "https://hello-world.lcl.host:" + diagPort + + cfg := new(cli.Config) + cfg.API.URL = srv.URL + cfg.AnchorURL = "http://anchor.lcl.host:" + srv.RailsPort + cfg.Lcl.DiagnosticAddr = diagAddr + cfg.Lcl.Service = "hi-ankydotdev" + cfg.Lcl.Subdomain = "hi-ankydotdev" + cfg.Trust.MockMode = true + cfg.Trust.NoSudo = true + cfg.Trust.Stores = []string{"mock"} + + var err error + if cfg.API.Token, err = srv.GeneratePAT("lcl_config@anchor.dev"); err != nil { + t.Fatal(err) + } + + t.Run("basics", func(t *testing.T) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + drv, tm := uitest.TestTUI(ctx, t) + + cmd := LclConfig{ + Config: cfg, + } + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + + tm.Quit() + }() + + // wait for prompt + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + return bytes.Contains(bts, []byte("? What lcl.host domain would you like to use for diagnostics?")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Type("hello-world") + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + if !srv.IsProxy() { + t.Skip("diagnostic unsupported in mock mode") + } + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := "Entered hello-world.lcl.host domain for lcl.host diagnostic certificate." + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := fmt.Sprintf("! Press Enter to open %s in your browser.", httpURL) + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + if _, err := http.Get(httpURL); err != nil { + t.Fatal(err) + } + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := "! Press Enter to install 2 missing certificates. (requires sudo)" + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := fmt.Sprintf("! Press Enter to open %s in your browser.", httpsURL) + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + pool := x509.NewCertPool() + for _, ca := range truststore.MockCAs { + pool.AddCert(ca.Certificate) + } + + httpsClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + }, + }, + } + + if _, err := httpsClient.Get(httpsURL); err != nil { + t.Fatal(err) + } + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + teatest.RequireEqualOutput(t, drv.FinalOut()) + }) +} diff --git a/lcl/lcl_test.go b/lcl/lcl_test.go index 9db1e3c..2ceedf9 100644 --- a/lcl/lcl_test.go +++ b/lcl/lcl_test.go @@ -66,7 +66,9 @@ func TestLcl(t *testing.T) { cfg.Trust.NoSudo = true cfg.Trust.Stores = []string{"mock"} - if cfg.API.Token, err = srv.GeneratePAT("anky@anchor.dev"); err != nil { + setupGuideURL := cfg.AnchorURL + "lcl/services/test-app/guide" + + if cfg.API.Token, err = srv.GeneratePAT("lcl@anchor.dev"); err != nil { t.Fatal(err) } @@ -90,13 +92,14 @@ func TestLcl(t *testing.T) { // wait for prompt teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } - return bytes.Contains(bts, []byte("? What lcl.host domain would you like to use for diagnostics?")) + expect := "? What lcl.host domain would you like to use for diagnostics?" + return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3), @@ -112,14 +115,13 @@ func TestLcl(t *testing.T) { } teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } expect := "Entered hello-world.lcl.host domain for lcl.host diagnostic certificate." - return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), @@ -127,7 +129,7 @@ func TestLcl(t *testing.T) { ) teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) @@ -149,7 +151,7 @@ func TestLcl(t *testing.T) { } teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) @@ -167,7 +169,7 @@ func TestLcl(t *testing.T) { }) teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) @@ -202,67 +204,74 @@ func TestLcl(t *testing.T) { } teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } - expect := "- Scanned current directory." + expect := "? What application server type?" return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3), ) - t.Skip("pending ability to stub out the detection.Perform results") + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } - expect := "What is your application server type?" - + expect := "? What is the application name?" return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), - teatest.WithDuration(time.Second*3), + teatest.WithDuration(time.Second*5), ) + tm.Type("test-app") tm.Send(tea.KeyMsg{ Type: tea.KeyEnter, }) teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } - expect := "? What is your application name?" + expect := "? What lcl.host domain would you like to use for local application development?" return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), - teatest.WithDuration(time.Second*5), + teatest.WithDuration(time.Second*3), ) - tm.Type("test-app") + if !srv.IsProxy() { + t.Skip("provisioning unsupported in mock mode") + } + tm.Send(tea.KeyMsg{ Type: tea.KeyEnter, }) + t.Skip("Pending workaround for consistent setup guide port value") + teatest.WaitFor( - t, tm.Output(), + t, drv.Out, func(bts []byte) bool { if len(errc) > 0 { t.Fatal(<-errc) } - expect := "? What lcl.host subdomain would you like to use?" + expect := fmt.Sprintf("! Press Enter to open %s.", setupGuideURL) return bytes.Contains(bts, []byte(expect)) }, teatest.WithCheckInterval(time.Millisecond*100), @@ -273,22 +282,7 @@ func TestLcl(t *testing.T) { Type: tea.KeyEnter, }) - teatest.WaitFor( - t, tm.Output(), - func(bts []byte) bool { - if len(errc) > 0 { - if err != nil { - t.Fatal(<-errc) - } - } - - expect := "- Created test-app resources for lcl.host diagnostic server on Anchor.dev." - return bytes.Contains(bts, []byte(expect)) - }, - teatest.WithCheckInterval(time.Millisecond*1000), - teatest.WithDuration(time.Second*30), - ) - - // TODO: add tests once thing settle down + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + teatest.RequireEqualOutput(t, drv.FinalOut()) }) } diff --git a/lcl/mkcert.go b/lcl/mkcert.go index a6f4d7e..ebda5c3 100644 --- a/lcl/mkcert.go +++ b/lcl/mkcert.go @@ -18,9 +18,14 @@ type MkCert struct { Config *cli.Config anc *api.Session - domains []string - eab *api.Eab - chainSlug, orgSlug, realmSlug, serviceSlug, subCaSubjectUID string + domains []string + eab *api.Eab + + chainSlug string + orgSlug string + realmSlug string + serviceSlug string + subCaSubjectUID string } func (c MkCert) UI() cli.UI { diff --git a/lcl/mkcert_test.go b/lcl/mkcert_test.go new file mode 100644 index 0000000..6a558d5 --- /dev/null +++ b/lcl/mkcert_test.go @@ -0,0 +1,46 @@ +package lcl + +import ( + "context" + "testing" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/ui/uitest" +) + +func TestLclMkcert(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := new(cli.Config) + cfg.API.URL = srv.URL + cfg.AnchorURL = "http://anchor.lcl.host:" + srv.RailsPort + cfg.Lcl.Service = "hi-lcl-mkcert" + cfg.Trust.MockMode = true + cfg.Trust.NoSudo = true + cfg.Trust.Stores = []string{"mock"} + + var err error + if cfg.API.Token, err = srv.GeneratePAT("lcl_mkcert@anchor.dev"); err != nil { + t.Fatal(err) + } + + t.Run("basics", func(t *testing.T) { + t.Skip("pending better support for building needed models before running") + + if !srv.IsProxy() { + t.Skip("mkcert unsupported in proxy mode") + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + cmd := MkCert{ + Config: cfg, + domains: []string{"hi-lcl-mkcert.lcl.host", "hi-lcl-mkcert.localhost"}, + subCaSubjectUID: "ABCD:EF12:23456", + } + + uitest.TestTUIOutput(ctx, t, cmd.UI()) + }) +} diff --git a/lcl/models/config.go b/lcl/models/config.go index ee0b0ba..01f437d 100644 --- a/lcl/models/config.go +++ b/lcl/models/config.go @@ -41,8 +41,8 @@ func (m LclConfigHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } func (m LclConfigHint) View() string { var b strings.Builder fmt.Fprintln(&b, ui.StepHint("Before issuing HTTPS certificates for your local applications, we need to")) - fmt.Fprintln(&b, ui.StepHint("configure your browsers and OS to trust your personal certificates. ")) - fmt.Fprintln(&b, ui.StepHint("")) + fmt.Fprintln(&b, ui.StepHint("configure your browsers and OS to trust your personal certificates.")) + fmt.Fprintln(&b, ui.Whisper(" |")) // whisper instead of stephint to avoid whitespace errors from git + golden fmt.Fprintln(&b, ui.StepHint("We'll start a local diagnostic web server to guide you through the process.")) return b.String() } diff --git a/lcl/setup.go b/lcl/setup.go index 910eb1e..422dfc2 100644 --- a/lcl/setup.go +++ b/lcl/setup.go @@ -48,8 +48,8 @@ func (c Setup) run(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, new(models.SetupHeader)) - drv.Activate(ctx, new(models.SetupHint)) + drv.Activate(ctx, &models.SetupHeader{}) + drv.Activate(ctx, &models.SetupHint{}) err = c.perform(ctx, drv) if err != nil { @@ -194,8 +194,10 @@ func (c Setup) perform(ctx context.Context, drv *ui.Driver) error { return ctx.Err() } - if err := browser.OpenURL(setupGuideURL); err != nil { - return err + if !c.Config.Trust.MockMode { + if err := browser.OpenURL(setupGuideURL); err != nil { + return err + } } return nil diff --git a/lcl/setup_test.go b/lcl/setup_test.go new file mode 100644 index 0000000..f2f01d3 --- /dev/null +++ b/lcl/setup_test.go @@ -0,0 +1,137 @@ +package lcl + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/ui/uitest" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" +) + +func TestSetup(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := new(cli.Config) + cfg.API.URL = srv.URL + cfg.AnchorURL = "http://anchor.lcl.host:" + srv.RailsPort + "/" + cfg.Lcl.Service = "hi-ankydotdev" + cfg.Lcl.Subdomain = "hi-ankydotdev" + cfg.Trust.MockMode = true + cfg.Trust.NoSudo = true + cfg.Trust.Stores = []string{"mock"} + + setupGuideURL := cfg.AnchorURL + "lcl_setup/services/test-app/guide" + + var err error + if cfg.API.Token, err = srv.GeneratePAT("lcl_setup@anchor.dev"); err != nil { + t.Fatal(err) + } + + t.Run("basics", func(t *testing.T) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + drv, tm := uitest.TestTUI(ctx, t) + + cmd := Setup{ + Config: cfg, + } + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + + tm.Quit() + }() + + // wait for prompt + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := "? What application server type?" + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := "? What is the application name?" + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Type("test-app") + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := "? What lcl.host domain would you like to use for local application development?" + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + if !srv.IsProxy() { + t.Skip("provisioning unsupported in mock mode") + } + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + t.Skip("Pending workaround for consistent setup guide port value") + + teatest.WaitFor( + t, drv.Out, + func(bts []byte) bool { + if len(errc) > 0 { + t.Fatal(<-errc) + } + + expect := fmt.Sprintf("! Press Enter to open %s.", setupGuideURL) + return bytes.Contains(bts, []byte(expect)) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + teatest.RequireEqualOutput(t, drv.FinalOut()) + }) +} diff --git a/lcl/testdata/TestAudit/basics.golden b/lcl/testdata/TestAudit/basics.golden index ea4428a..b8af6f0 100644 --- a/lcl/testdata/TestAudit/basics.golden +++ b/lcl/testdata/TestAudit/basics.golden @@ -1,27 +1,27 @@ +─── Client ───────────────────────────────────────────────────────────────────── * Checking authentication: probing credentials locally…* -──────────────────────────────────────────────────────────────────────────────── +─── Client ───────────────────────────────────────────────────────────────────── * Checking authentication: testing credentials remotely…* -──────────────────────────────────────────────────────────────────────────────── +─── AuditHeader ──────────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` -──────────────────────────────────────────────────────────────────────────────── +─── AuditHint ────────────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` | We'll begin by checking your system to determine what you need for your setup. -──────────────────────────────────────────────────────────────────────────────── +─── AuditResources ───────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` | We'll begin by checking your system to determine what you need for your setup. * Checking resources on Anchor.dev…* -──────────────────────────────────────────────────────────────────────────────── +─── AuditResources ───────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` | We'll begin by checking your system to determine what you need for your setup. - Checked resources on Anchor.dev: need to provision resources. -──────────────────────────────────────────────────────────────────────────────── +─── AuditTrust ───────────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` | We'll begin by checking your system to determine what you need for your setup. - Checked resources on Anchor.dev: need to provision resources. * Scanning local and expected CA certificates…* -──────────────────────────────────────────────────────────────────────────────── +─── AuditTrust ───────────────────────────────────────────────────────────────── # Audit lcl.host HTTPS Local Development Environment `anchor lcl audit` | We'll begin by checking your system to determine what you need for your setup. - Checked resources on Anchor.dev: need to provision resources. - Scanned local and expected CA certificates: need to install 2 missing certificates. -──────────────────────────────────────────────────────────────────────────────── diff --git a/lcl/testdata/TestClean/basics.golden b/lcl/testdata/TestClean/basics.golden new file mode 100644 index 0000000..b0ba151 --- /dev/null +++ b/lcl/testdata/TestClean/basics.golden @@ -0,0 +1,17 @@ +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: probing credentials locally…* +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: testing credentials remotely…* +─── LclCleanHeader ───────────────────────────────────────────────────────────── +# Clean lcl.host CA Certificates from Local Trust Store(s) `anchor trust clean` +─── LclCleanHint ─────────────────────────────────────────────────────────────── +# Clean lcl.host CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing lcl.host CA certificates from the mock store(s). +─── TrustCleanAudit ──────────────────────────────────────────────────────────── +# Clean lcl.host CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing lcl.host CA certificates from the mock store(s). + * Auditing local CA certificates…* +─── TrustCleanAudit ──────────────────────────────────────────────────────────── +# Clean lcl.host CA Certificates from Local Trust Store(s) `anchor trust clean` +| Removing lcl.host CA certificates from the mock store(s). + - Audited local CA certificates: need to remove 0 certificates. diff --git a/lcl/testdata/TestLclConfig/basics.golden b/lcl/testdata/TestLclConfig/basics.golden new file mode 100644 index 0000000..cb3eac4 --- /dev/null +++ b/lcl/testdata/TestLclConfig/basics.golden @@ -0,0 +1,338 @@ +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: probing credentials locally…* +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: testing credentials remotely…* +─── LclConfigHeader ──────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` +─── LclConfigHint ────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hi-lcl_config.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? h.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? he.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hel.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hell.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-w.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-wo.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-wor.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-worl.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + ? What lcl.host domain would you like to use for diagnostics? + | We will ignore any characters that are not valid in a domain. + ? hello-world.lcl.host +─── DomainInput ──────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. +─── DomainResolver ───────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + * Resolving hello-world.lcl.host domain…* +─── DomainResolver ───────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! +─── ProvisionService ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Creating hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev… * +─── ProvisionService ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. +─── LclConfig ────────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + ! Press Enter to open http://hello-world.lcl.host:4433 in your browser. +─── LclConfig ────────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo +─── TrustPreflight ───────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + * Comparing local and expected CA certificates…* +─── TrustPreflight ───────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + ! Press Enter to install 2 missing certificates. (requires sudo) +─── TrustPreflight ───────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. +─── TrustUpdateStore ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + * Updating Mock: installing lcl_config/localhost - AnchorCA ECDSA. +─── TrustUpdateStore ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + - Updated Mock: installed lcl_config/localhost - AnchorCA [ECDSA] +─── TrustUpdateStore ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + - Updated Mock: installed lcl_config/localhost - AnchorCA [ECDSA] + * Updating Mock: installing lcl_config/localhost - AnchorCA RSA. +─── TrustUpdateStore ─────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + - Updated Mock: installed lcl_config/localhost - AnchorCA [ECDSA, RSA] +─── LclConfig ────────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + - Updated Mock: installed lcl_config/localhost - AnchorCA [ECDSA, RSA] + | Before we move on, let's test your system configuration by trying out HTTPS. + ! Press Enter to open https://hello-world.lcl.host:4433 in your browser. +─── LclConfig ────────────────────────────────────────────────────────────────── +# Configure System for lcl.host HTTPS Local Development `anchor lcl config` + | Before issuing HTTPS certificates for your local applications, we need to + | configure your browsers and OS to trust your personal certificates. + | + | We'll start a local diagnostic web server to guide you through the process. + - Entered hello-world.lcl.host domain for lcl.host diagnostic certificate. + - Resolved hello-world.lcl.host domain: success! + | Now we'll provision your application's resources on Anchor.dev and the HTTPS + | certificates for your development environment. + - Created hello-world [hello-world.lcl.host, hello-world.localhost] diagnostic resources on Anchor.dev. + - Great, http://hello-world.lcl.host:4433 works as expected (without HTTPS). + | Next, we'll add your personal CA certificates to your system's trust stores. + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + - Compared local and expected CA certificates: need to install 2 missing certificates. + - Updated Mock: installed lcl_config/localhost - AnchorCA [ECDSA, RSA] + | Before we move on, let's test your system configuration by trying out HTTPS. + - Success! https://hello-world.lcl.host:4433 works as expected (encrypted with HTTPS). diff --git a/trust/audit_test.go b/trust/audit_test.go index a5f8ca7..c4bbeab 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "regexp" + "runtime" "testing" "github.com/anchordotdev/cli" @@ -17,6 +18,10 @@ import ( ) func TestAudit(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no pty support on windows") + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/trust/testdata/TestTrust/basics.golden b/trust/testdata/TestTrust/basics.golden index be21cbb..11fef20 100644 --- a/trust/testdata/TestTrust/basics.golden +++ b/trust/testdata/TestTrust/basics.golden @@ -1,22 +1,22 @@ +─── TrustPreflight ───────────────────────────────────────────────────────────── * Comparing local and expected CA certificates…* -──────────────────────────────────────────────────────────────────────────────── +─── TrustPreflight ───────────────────────────────────────────────────────────── - Compared local and expected CA certificates: need to install 2 missing certificates. ! Installing 2 missing certificates. (requires sudo) -──────────────────────────────────────────────────────────────────────────────── +─── TrustUpdateStore ─────────────────────────────────────────────────────────── - Compared local and expected CA certificates: need to install 2 missing certificates. ! Installing 2 missing certificates. (requires sudo) - * Updating Mock: installing oas-examples - AnchorCA ECDSA. -──────────────────────────────────────────────────────────────────────────────── + * Updating Mock: installing ankydotdev/localhost - AnchorCA ECDSA. +─── TrustUpdateStore ─────────────────────────────────────────────────────────── - Compared local and expected CA certificates: need to install 2 missing certificates. ! Installing 2 missing certificates. (requires sudo) - - Updated Mock: installed oas-examples - AnchorCA [ECDSA] -──────────────────────────────────────────────────────────────────────────────── + - Updated Mock: installed ankydotdev/localhost - AnchorCA [ECDSA] +─── TrustUpdateStore ─────────────────────────────────────────────────────────── - Compared local and expected CA certificates: need to install 2 missing certificates. ! Installing 2 missing certificates. (requires sudo) - - Updated Mock: installed oas-examples - AnchorCA [ECDSA] - * Updating Mock: installing oas-examples - AnchorCA RSA. -──────────────────────────────────────────────────────────────────────────────── + - Updated Mock: installed ankydotdev/localhost - AnchorCA [ECDSA] + * Updating Mock: installing ankydotdev/localhost - AnchorCA RSA. +─── TrustUpdateStore ─────────────────────────────────────────────────────────── - Compared local and expected CA certificates: need to install 2 missing certificates. ! Installing 2 missing certificates. (requires sudo) - - Updated Mock: installed oas-examples - AnchorCA [ECDSA, RSA] -──────────────────────────────────────────────────────────────────────────────── + - Updated Mock: installed ankydotdev/localhost - AnchorCA [ECDSA, RSA] diff --git a/trust/trust_test.go b/trust/trust_test.go index 4713223..02ee1ec 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -1,14 +1,10 @@ package trust import ( - "bytes" "context" "flag" "os" "testing" - "time" - - "github.com/charmbracelet/x/exp/teatest" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api/apitest" @@ -48,7 +44,7 @@ func TestTrust(t *testing.T) { t.Fatal(err) } - t.Run("iterative basics", func(t *testing.T) { + t.Run("basics", func(t *testing.T) { if !srv.IsProxy() { t.Skip("trust unsupported in mock mode") } @@ -56,46 +52,6 @@ func TestTrust(t *testing.T) { ctx, cancel := context.WithCancel(ctx) defer cancel() - drv, tm := uitest.TestTUI(ctx, t) - - cmd := Command{ - Config: cfg, - } - - if err := cmd.UI().RunTUI(ctx, drv); err != nil { - t.Fatal(err) - } - defer tm.Quit() - - teatest.WaitFor( - t, tm.Output(), - func(bts []byte) bool { - return bytes.Contains(bts, []byte("* Comparing local and expected CA certificates…*")) - }, - teatest.WithCheckInterval(time.Millisecond*100), - teatest.WithDuration(time.Second*3), - ) - - teatest.WaitFor( - t, tm.Output(), - func(bts []byte) bool { - return bytes.Contains(bts, []byte("- Compared local and expected CA certificates: need to install 2 missing certificates.")) && - bytes.Contains(bts, []byte("! Installing 2 missing certificates. (requires sudo)")) && - bytes.Contains(bts, []byte("- Updated Mock: installed ankydotdev/localhost - AnchorCA [RSA, ECDSA]")) - }, - teatest.WithCheckInterval(time.Millisecond*100), - teatest.WithDuration(time.Second*3), - ) - }) - - t.Run("basics", func(t *testing.T) { - if srv.IsProxy() { - t.Skip("trust unsupported in proxy mode") - } - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - cmd := Command{ Config: cfg, } diff --git a/truststore/audit.go b/truststore/audit.go index 3c8806d..35b4bfe 100644 --- a/truststore/audit.go +++ b/truststore/audit.go @@ -1,6 +1,7 @@ package truststore import ( + "cmp" "slices" "time" ) @@ -128,6 +129,18 @@ func (a *Audit) Perform() (*AuditInfo, error) { info.Valid = append(info.Valid, ca) } } + + // sort everything for more consistent output + for _, slice := range [][]*CA{info.Valid, info.Missing, info.Rotate, info.Expired, info.PreValid, info.Extra} { + slices.SortFunc(slice, func(x, y *CA) int { + if n := cmp.Compare(x.Subject.CommonName, y.Subject.CommonName); n != 0 { + return n + } + // If names are equal, order by PublicKeyAlgorithm + return cmp.Compare(x.PublicKeyAlgorithm.String(), y.PublicKeyAlgorithm.String()) + }) + } + return info, nil } diff --git a/truststore/nss.go b/truststore/nss.go index df1589f..e8c5aea 100644 --- a/truststore/nss.go +++ b/truststore/nss.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/pem" "errors" + "fmt" "io/fs" "os" "os/exec" @@ -144,13 +145,9 @@ func (s *NSS) CheckCA(ca *CA) (installed bool, err error) { count, err := s.forEachNSSProfile(func(profile string) error { out, err := s.SysFS.Exec(s.SysFS.Command(s.certutilPath, "-V", "-d", profile, "-u", "L", "-n", ca.UniqueName)) + err = s.handleCertUtilResult(profile, out, err) if err != nil { - return NSSError{ - Err: errors.New(string(out)), - - CertutilInstallHelp: s.certutilInstallHelp, - NSSBrowsers: nssBrowsers, - } + return err } return nil }) @@ -214,8 +211,10 @@ func (s *NSS) InstallCA(ca *CA) (installed bool, err error) { "-i", ca.FilePath, } - if out, err := s.execCertutil(s.certutilPath, args...); err != nil { - return fatalCmdErr(err, "certutil -A -d "+profile, out) + out, err := s.execCertutil(s.certutilPath, args...) + err = s.handleCertUtilResult(profile, out, err) + if err != nil { + return err } return nil }) @@ -255,19 +254,9 @@ func (s *NSS) ListCAs() ([]*CA, error) { var cas []*CA _, err := s.forEachNSSProfile(func(profile string) error { out, err := s.SysFS.Exec(s.SysFS.Command(s.certutilPath, "-L", "-d", profile)) + err = s.handleCertUtilResult(profile, out, err) if err != nil { - message := string(out) - - if bytes.Contains(out, []byte(certUtilBadDatabaseOutput)) { - message = "certutil bad database `" + profile + "`" - } - - return NSSError{ - Err: errors.New(message), - - CertutilInstallHelp: s.certutilInstallHelp, - NSSBrowsers: nssBrowsers, - } + return err } lines := strings.Split(strings.TrimSpace(string(out)), "\n") @@ -283,7 +272,7 @@ func (s *NSS) ListCAs() ([]*CA, error) { padLen := strings.Index(lines[0], "Trust Attributes") if padLen <= 0 { return NSSError{ - Err: errors.New("unexpected certutil output format"), + Err: errors.New("certutil unexpected output format"), CertutilInstallHelp: s.certutilInstallHelp, NSSBrowsers: nssBrowsers, @@ -298,7 +287,7 @@ func (s *NSS) ListCAs() ([]*CA, error) { for _, line := range lines[3:] { if len(line) < padLen { return NSSError{ - Err: errors.New("unexpected certutil line format"), + Err: errors.New("certutil unexpected line format"), CertutilInstallHelp: s.certutilInstallHelp, NSSBrowsers: nssBrowsers, @@ -310,13 +299,9 @@ func (s *NSS) ListCAs() ([]*CA, error) { for _, nick := range nicks { out, err := s.SysFS.Exec(s.SysFS.Command(s.certutilPath, "-L", "-d", profile, "-n", nick, "-a")) + err = s.handleCertUtilResult(profile, out, err) if err != nil { - return NSSError{ - Err: errors.New(string(out)), - - CertutilInstallHelp: s.certutilInstallHelp, - NSSBrowsers: nssBrowsers, - } + return err } for p, buf := pem.Decode(out); p != nil; p, buf = pem.Decode(buf) { @@ -400,8 +385,10 @@ func (s *NSS) UninstallCA(ca *CA) (bool, error) { "-n", nickName, } - if out, err := s.execCertutil(s.certutilPath, args...); err != nil { - return fatalCmdErr(err, "certutil -D -d "+profile, out) + out, err := s.execCertutil(s.certutilPath, args...) + err = s.handleCertUtilResult(profile, out, err) + if err != nil { + return err } return nil }) @@ -433,15 +420,38 @@ func (s *NSS) forEachNSSProfile(f func(profile string) error) (found int, err er } if pathExists(s.DataFS, filepath.Join(profile, "cert9.db")) { if err := f("sql:" + profile); err != nil { - return 0, err + return 0, NSSError{ + Err: errors.New("`sql:" + profile + "` " + err.Error()), + + CertutilInstallHelp: s.certutilInstallHelp, + NSSBrowsers: nssBrowsers, + } } found++ } else if pathExists(s.DataFS, filepath.Join(profile, "cert8.db")) { if err := f("dbm:" + profile); err != nil { - return 0, err + return 0, NSSError{ + Err: errors.New("`dbm:" + profile + "` " + err.Error()), + + CertutilInstallHelp: s.certutilInstallHelp, + NSSBrowsers: nssBrowsers, + } } found++ } } return } + +func (s *NSS) handleCertUtilResult(profile string, out []byte, err error) error { + if err != nil { + return NSSError{ + Err: fmt.Errorf("`%s` %q", profile, out), + + CertutilInstallHelp: s.certutilInstallHelp, + NSSBrowsers: nssBrowsers, + } + } + + return nil +} diff --git a/truststore/platform_windows.go b/truststore/platform_windows.go index acc5d77..1093ba8 100644 --- a/truststore/platform_windows.go +++ b/truststore/platform_windows.go @@ -3,7 +3,6 @@ package truststore import ( "crypto/x509" "encoding/pem" - "errors" "fmt" "io/fs" "io/ioutil" @@ -45,17 +44,33 @@ type Platform struct { func (s *Platform) Description() string { return "System (Windows)" } func (s *Platform) check() (bool, error) { + store, err := openWindowsRootStore() + if err != nil { + return false, nil + } + store.close() + return true, nil } func (s *Platform) checkCA(ca *CA) (bool, error) { - return false, nil + store, err := openWindowsRootStore() + if err != nil { + return false, fatalErr(err, "open root store") + } + defer store.close() + + cert, err := store.findCertWithSerial(ca.Certificate.SerialNumber) + if err != nil { + return false, fatalErr(err, "find ca") + } + return (cert != nil), nil } func (s *Platform) installCA(ca *CA) (bool, error) { // Load cert cert, err := ioutil.ReadFile(ca.FilePath) - if err == nil { + if err != nil { return false, fatalErr(err, "failed to read root certificate") } // Decode PEM @@ -78,9 +93,27 @@ func (s *Platform) installCA(ca *CA) (bool, error) { } func (s *Platform) listCAs() ([]*CA, error) { - // https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certenumcertificatesinstore + store, err := openWindowsRootStore() + if err != nil { + return nil, fatalErr(err, "open root store") + } + defer store.close() + + certs, err := store.listCerts() + if err != nil { + return nil, fatalErr(err, "list cas") + } + + var cas []*CA + for _, cert := range certs { + ca := &CA{ + Certificate: cert, + UniqueName: cert.SerialNumber.Text(16), + } - return nil, fatalErr(errors.New("unsupported"), "enumerate certs") + cas = append(cas, ca) + } + return cas, nil } func (s *Platform) uninstallCA(ca *CA) (bool, error) { @@ -171,3 +204,49 @@ func (w windowsRootStore) deleteCertsWithSerial(serial *big.Int) (bool, error) { } return deletedAny, nil } + +func (w windowsRootStore) findCertWithSerial(serial *big.Int) (*x509.Certificate, error) { + var cert *syscall.CertContext + for { + // Next enum + certPtr, _, err := procCertEnumCertificatesInStore.Call(uintptr(w), uintptr(unsafe.Pointer(cert))) + if cert = (*syscall.CertContext)(unsafe.Pointer(certPtr)); cert == nil { + if errno, ok := err.(syscall.Errno); ok && errno == 0x80092004 { + break + } + return nil, fmt.Errorf("failed enumerating certs: %v", err) + } + + // Parse cert + certBytes := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:cert.Length] + parsedCert, err := x509.ParseCertificate(certBytes) + if err == nil && parsedCert.SerialNumber != nil && parsedCert.SerialNumber.Cmp(serial) == 0 { + return parsedCert, nil + } + } + return nil, nil +} + +func (w windowsRootStore) listCerts() ([]*x509.Certificate, error) { + var certs []*x509.Certificate + + var cert *syscall.CertContext + for { + // Next enum + certPtr, _, err := procCertEnumCertificatesInStore.Call(uintptr(w), uintptr(unsafe.Pointer(cert))) + if cert = (*syscall.CertContext)(unsafe.Pointer(certPtr)); cert == nil { + if errno, ok := err.(syscall.Errno); ok && errno == 0x80092004 { + break + } + return nil, fmt.Errorf("failed enumerating certs: %v", err) + } + + // Parse cert + certBytes := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:cert.Length] + if parsedCert, err := x509.ParseCertificate(certBytes); err == nil { + // ignore individual cert parsing errors + certs = append(certs, parsedCert) + } + } + return certs, nil +} diff --git a/ui/driver.go b/ui/driver.go index ff60b83..6b7a968 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "reflect" + "strings" "sync" + "unicode/utf8" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/termenv" @@ -26,8 +28,9 @@ type Driver struct { models []tea.Model active tea.Model - Out *bytes.Buffer - mutex sync.RWMutex + finalOut bytes.Buffer + Out io.Reader + out io.ReadWriter test bool lastView string } @@ -38,12 +41,12 @@ func NewDriverTest(ctx context.Context) *Driver { opts := []tea.ProgramOption{ tea.WithInputTTY(), tea.WithContext(ctx), - tea.WithoutCatchPanics(), // TODO: remove tea.WithoutRenderer(), } drv.Program = tea.NewProgram(drv, opts...) - drv.Out = new(bytes.Buffer) + drv.out = &safeReadWriter{rw: new(bytes.Buffer)} + drv.Out = io.TeeReader(drv.out, &drv.finalOut) drv.test = true return drv @@ -55,7 +58,6 @@ func NewDriverTUI(ctx context.Context) (*Driver, Program) { opts := []tea.ProgramOption{ tea.WithInputTTY(), tea.WithContext(ctx), - tea.WithoutCatchPanics(), // TODO: remove } drv.Program = tea.NewProgram(drv, opts...) @@ -88,6 +90,11 @@ func (d *Driver) Activate(ctx context.Context, model tea.Model) { } } +func (d *Driver) FinalOut() []byte { + io.ReadAll(d.Out) + return d.finalOut.Bytes() +} + type stopMsg struct{} func (d *Driver) Stop() { d.Send(stopMsg{}) } @@ -153,11 +160,10 @@ func (d *Driver) View() string { out += mdl.View() } if d.test && out != "" && out != d.lastView { - d.mutex.Lock() - defer d.mutex.Unlock() - - fmt.Fprint(d.Out, out) - fmt.Fprintln(d.Out, "────────────────────────────────────────────────────────────────────────────────") + separator := "─── " + reflect.TypeOf(d.active).Elem().Name() + " " + separator = separator + strings.Repeat("─", 80-utf8.RuneCountInString(separator)) + fmt.Fprintln(d.out, separator) + fmt.Fprint(d.out, out) d.lastView = out } return out @@ -177,3 +183,24 @@ func isExit(cmd tea.Cmd) bool { } func Exit() tea.Msg { return io.EOF } + +// safeReadWriter implements io.ReadWriter, but locks reads and writes. +type safeReadWriter struct { + sync.RWMutex + + rw io.ReadWriter +} + +// Read implements io.ReadWriter. +func (s *safeReadWriter) Read(p []byte) (n int, err error) { + s.RLock() + defer s.RUnlock() + return s.rw.Read(p) //nolint: wrapcheck +} + +// Write implements io.ReadWriter. +func (s *safeReadWriter) Write(p []byte) (int, error) { + s.Lock() + defer s.Unlock() + return s.rw.Write(p) //nolint: wrapcheck +} diff --git a/ui/uitest/uitest.go b/ui/uitest/uitest.go index 5d1c194..3fce888 100644 --- a/ui/uitest/uitest.go +++ b/ui/uitest/uitest.go @@ -27,7 +27,7 @@ func init() { } func TestTUI(ctx context.Context, t *testing.T) (*ui.Driver, *teatest.TestModel) { - drv := new(ui.Driver) + drv := ui.NewDriverTest(ctx) tm := teatest.NewTestModel(t, drv, teatest.WithInitialTermSize(128, 64)) drv.Program = program{tm} diff --git a/version/command_test.go b/version/command_test.go index 3ed46c9..badcfc2 100644 --- a/version/command_test.go +++ b/version/command_test.go @@ -2,12 +2,22 @@ package version import ( "context" + "flag" + "runtime" "testing" "github.com/anchordotdev/cli/api/apitest" ) +var ( + _ = flag.Bool("update", false, "ignored") +) + func TestCommand(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no pty support on windows") + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel()