diff --git a/cmd/hetty/cert.go b/cmd/hetty/cert.go new file mode 100644 index 0000000..feebae5 --- /dev/null +++ b/cmd/hetty/cert.go @@ -0,0 +1,211 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/mitchellh/go-homedir" + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/smallstep/truststore" +) + +var certUsage = ` +Usage: + hetty cert [flags] + +Certificate management tools. + +Options: + --help, -h Output this usage text. + +Subcommands: + - install Installs a certificate to the system trust store, and + (optionally) to the Firefox and Java trust stores. + - uninstall Uninstalls a certificate from the system trust store, and + (optionally) from the Firefox and Java trust stores. + +Run ` + "`hetty cert --help`" + ` for subcommand specific usage instructions. + +Visit https://hetty.xyz to learn more about Hetty. +` + +var certInstallUsage = ` +Usage: + hetty cert install [flags] + +Installs a certificate to the system trust store, and (optionally) to the Firefox +and Java trust stores. + +Options: + --cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem") + --firefox Install certificate to Firefox trust store. (Default: false) + --java Install certificate to Java trust store. (Default: false) + --skip-system Skip installing certificate to system trust store (Default: false) + --help, -h Output this usage text. + +Visit https://hetty.xyz to learn more about Hetty. +` + +var certUninstallUsage = ` +Usage: + hetty cert uninstall [flags] + +Uninstalls a certificate from the system trust store, and (optionally) from the Firefox +and Java trust stores. + +Options: + --cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem") + --firefox Uninstall certificate from Firefox trust store. (Default: false) + --java Uninstall certificate from Java trust store. (Default: false) + --skip-system Skip uninstalling certificate from system trust store (Default: false) + --help, -h Output this usage text. + +Visit https://hetty.xyz to learn more about Hetty. +` + +type CertInstallCommand struct { + config *Config + cert string + firefox bool + java bool + skipSystem bool +} + +type CertUninstallCommand struct { + config *Config + cert string + firefox bool + java bool + skipSystem bool +} + +func NewCertCommand(rootConfig *Config) *ffcli.Command { + return &ffcli.Command{ + Name: "cert", + Subcommands: []*ffcli.Command{ + NewCertInstallCommand(rootConfig), + NewCertUninstallCommand(rootConfig), + }, + Exec: func(context.Context, []string) error { + return flag.ErrHelp + }, + UsageFunc: func(*ffcli.Command) string { + return certUsage + }, + } +} + +func NewCertInstallCommand(rootConfig *Config) *ffcli.Command { + cmd := CertInstallCommand{ + config: rootConfig, + } + fs := flag.NewFlagSet("hetty cert install", flag.ExitOnError) + + fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.") + fs.BoolVar(&cmd.firefox, "firefox", false, "Install certificate to Firefox trust store. (Default: false)") + fs.BoolVar(&cmd.java, "java", false, "Install certificate to Java trust store. (Default: false)") + fs.BoolVar(&cmd.skipSystem, "skip-system", false, "Skip installing certificate to system trust store (Default: false)") + + cmd.config.RegisterFlags(fs) + + return &ffcli.Command{ + Name: "install", + FlagSet: fs, + Exec: cmd.Exec, + UsageFunc: func(*ffcli.Command) string { + return certInstallUsage + }, + } +} + +func (cmd *CertInstallCommand) Exec(_ context.Context, _ []string) error { + caCertFile, err := homedir.Expand(cmd.cert) + if err != nil { + return fmt.Errorf("failed to parse certificate filepath: %w", err) + } + + opts := []truststore.Option{} + + if cmd.skipSystem { + opts = append(opts, truststore.WithNoSystem()) + } + + if cmd.firefox { + opts = append(opts, truststore.WithFirefox()) + } + + if cmd.java { + opts = append(opts, truststore.WithJava()) + } + + if !cmd.skipSystem { + cmd.config.logger.Info( + "To install the certificate in the system trust store, you might be prompted for your password.") + } + + if err := truststore.InstallFile(caCertFile, opts...); err != nil { + return fmt.Errorf("failed to install certificate: %w", err) + } + + cmd.config.logger.Info("Finished installing certificate.") + + return nil +} + +func NewCertUninstallCommand(rootConfig *Config) *ffcli.Command { + cmd := CertUninstallCommand{ + config: rootConfig, + } + fs := flag.NewFlagSet("hetty cert uninstall", flag.ExitOnError) + + fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.") + fs.BoolVar(&cmd.firefox, "firefox", false, "Uninstall certificate from Firefox trust store. (Default: false)") + fs.BoolVar(&cmd.java, "java", false, "Uninstall certificate from Java trust store. (Default: false)") + fs.BoolVar(&cmd.skipSystem, "skip-system", false, "Skip uninstalling certificate from system trust store (Default: false)") + + cmd.config.RegisterFlags(fs) + + return &ffcli.Command{ + Name: "uninstall", + FlagSet: fs, + Exec: cmd.Exec, + UsageFunc: func(*ffcli.Command) string { + return certUninstallUsage + }, + } +} + +func (cmd *CertUninstallCommand) Exec(_ context.Context, _ []string) error { + caCertFile, err := homedir.Expand(cmd.cert) + if err != nil { + return fmt.Errorf("failed to parse certificate filepath: %w", err) + } + + opts := []truststore.Option{} + + if cmd.skipSystem { + opts = append(opts, truststore.WithNoSystem()) + } + + if cmd.firefox { + opts = append(opts, truststore.WithFirefox()) + } + + if cmd.java { + opts = append(opts, truststore.WithJava()) + } + + if !cmd.skipSystem { + cmd.config.logger.Info( + "To uninstall the certificate from the system trust store, you might be prompted for your password.") + } + + if err := truststore.UninstallFile(caCertFile, opts...); err != nil { + return fmt.Errorf("failed to uninstall certificate: %w", err) + } + + cmd.config.logger.Info("Finished uninstalling certificate.") + + return nil +} diff --git a/cmd/hetty/hetty.go b/cmd/hetty/hetty.go index 0bed2a3..dd0e367 100644 --- a/cmd/hetty/hetty.go +++ b/cmd/hetty/hetty.go @@ -43,7 +43,9 @@ var adminContent embed.FS var hettyUsage = ` Usage: - hetty [flags] + hetty [flags] [subcommand] [flags] + +Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface. Options: --cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem") @@ -56,6 +58,11 @@ Options: --version, -v Output version. --help, -h Output this usage text. +Subcommands: + - cert Certificate management + +Run ` + "`hetty --help`" + ` for subcommand specific usage instructions. + Visit https://hetty.xyz to learn more about Hetty. ` @@ -90,10 +97,12 @@ func NewHettyCommand() (*ffcli.Command, *Config) { cmd.config.RegisterFlags(fs) return &ffcli.Command{ - Name: "hetty", - ShortUsage: "hetty [-v] [subcommand] [flags] [...]", - FlagSet: fs, - Exec: cmd.Exec, + Name: "hetty", + FlagSet: fs, + Subcommands: []*ffcli.Command{ + NewCertCommand(cmd.config), + }, + Exec: cmd.Exec, UsageFunc: func(*ffcli.Command) string { return hettyUsage }, diff --git a/cmd/hetty/main.go b/cmd/hetty/main.go index 107ecca..c37cde2 100644 --- a/cmd/hetty/main.go +++ b/cmd/hetty/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "errors" + "flag" llog "log" "os" @@ -25,7 +27,8 @@ func main() { cfg.logger = logger - if err := hettyCmd.Run(context.Background()); err != nil { - logger.Fatal("Unexpected error running command.", zap.Error(err)) + err = hettyCmd.Run(context.Background()) + if err != nil && !errors.Is(err, flag.ErrHelp) { + logger.Fatal("Command failed.", zap.Error(err)) } } diff --git a/go.mod b/go.mod index 3b79a2d..b2b6795 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/oklog/ulid v1.3.1 github.com/peterbourgon/ff/v3 v3.1.2 + github.com/smallstep/truststore v0.11.0 github.com/vektah/gqlparser/v2 v2.2.0 go.uber.org/zap v1.21.0 ) @@ -54,4 +55,5 @@ require ( golang.org/x/tools v0.1.5 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect + howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect ) diff --git a/go.sum b/go.sum index e33c924..4c90842 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,7 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -133,6 +134,8 @@ github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk= +github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -250,6 +253,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= @@ -258,5 +262,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=