From 16f46839123098daa5468749f220825637966c00 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Tue, 20 Feb 2024 02:28:19 +0000 Subject: [PATCH] shelly set-user-ca + put-tls-client-cert + put-tls-client-key --- cmd/gen_data.go | 142 ++++++++++++++++++++++++++++ cmd/shelly_put_tls_client_cert.go | 35 +++++++ cmd/shelly_put_tls_client_key.go | 35 +++++++ cmd/shelly_put_user_ca.go | 35 +++++++ cmd/shelly_set_user_ca.go | 152 ------------------------------ 5 files changed, 247 insertions(+), 152 deletions(-) create mode 100644 cmd/gen_data.go create mode 100644 cmd/shelly_put_tls_client_cert.go create mode 100644 cmd/shelly_put_tls_client_key.go create mode 100644 cmd/shelly_put_user_ca.go delete mode 100644 cmd/shelly_set_user_ca.go diff --git a/cmd/gen_data.go b/cmd/gen_data.go new file mode 100644 index 0000000..0855a9b --- /dev/null +++ b/cmd/gen_data.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + + "github.com/jcodybaker/go-shelly" + "github.com/jcodybaker/shellyctl/pkg/discovery" + "github.com/mongoose-os/mos/common/mgrpc/frame" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type reqBuilder func(data *string, append bool) shelly.RPCRequestBody +type runE func(cmd *cobra.Command, args []string) error + +func newDataCommand(reqBuilder reqBuilder, strParam, fileParam, nullParam string) runE { + return func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + method := reqBuilder(nil, false).Method() + ll := log.Ctx(ctx).With().Str("method", method).Logger() + + var b *bytes.Buffer + data := viper.GetString(strParam) + dataFile := viper.GetString(fileParam) + remove := viper.GetBool(nullParam) + if (data != "" && dataFile != "") || (data != "" && remove) || (dataFile != "" && remove) { + return fmt.Errorf("--%s, --%s, and --%s options are mutually exclusive", strParam, fileParam, nullParam) + } + if data == "" && dataFile == "" && !remove { + return fmt.Errorf("exactly one of --%s, --%s, and --%s options are required", strParam, fileParam, nullParam) + } + if data != "" { + b = bytes.NewBufferString(data) + } else if dataFile == "-" { + b = &bytes.Buffer{} + if _, err := io.Copy(b, os.Stdin); err != nil { + ll.Fatal().Err(err).Msg(fmt.Sprintf("reading stdin for --%s", strParam)) + } + } else if dataFile != "" { + b = &bytes.Buffer{} + f, err := os.Open(dataFile) + if err != nil { + ll.Fatal().Err(err).Str(fileParam, dataFile).Msg(fmt.Sprintf("opening --%s", fileParam)) + } + n, err := io.Copy(b, f) + if err != nil { + ll.Fatal().Err(err).Str(fileParam, dataFile).Msg(fmt.Sprintf("reading --%s", fileParam)) + } + ll.Debug().Int64("bytes_read", n).Str(fileParam, dataFile).Msg(fmt.Sprintf("finished reading --%s", fileParam)) + if err := f.Close(); err != nil { + ll.Warn().Err(err).Str(fileParam, dataFile).Msg(fmt.Sprintf("closing --%s", fileParam)) + } + } + + dOpts, err := discoveryOptionsFromFlags(cmd.Flags()) + if err != nil { + ll.Fatal().Err(err).Msg("parsing flags") + } + + discoverer := discovery.NewDiscoverer(dOpts...) + if err := discoveryAddDevices(ctx, discoverer); err != nil { + ll.Fatal().Err(err).Msg("adding devices") + } + + if _, err := discoverer.Search(ctx); err != nil { + return err + } + + for _, d := range discoverer.AllDevices() { + ll := d.Log(ll) + conn, err := d.Open(ctx) + if err != nil { + return err + } + defer func() { + if err := conn.Disconnect(ctx); err != nil { + ll.Warn().Err(err).Msg("disconnecting from device") + } + }() + if remove { + req := reqBuilder(nil, false) + resp := req.NewResponse() + ll.Debug(). + Str("method", req.Method()). + Any("request_body", req). + Msg("sending request to clear data") + _, err = shelly.Do(ctx, conn, d.AuthCallback(ctx), req, resp) + if err != nil { + if viper.GetBool("skip-failed-hosts") { + ll.Err(err).Msg("error executing request; contining because --skip-failed-hosts=true") + continue + } else { + ll.Fatal().Err(err).Msg("error executing request") + } + } + ll.Info().Str("method", req.Method()).Msg("successfully cleared data") + continue + } + s := bufio.NewScanner(b) + var line int + var resp any + var rawResp *frame.Response + for s.Scan() { + line++ + req := reqBuilder(shelly.StrPtr(s.Text()), line > 1) + resp = req.NewResponse() + ll.Debug(). + Str("method", req.Method()). + Int("line", line). + Any("request_body", req). + Msg("sending data") + rawResp, err = shelly.Do(ctx, conn, d.AuthCallback(ctx), req, resp) + if err != nil { + return err + } + ll.Debug(). + Str("method", req.Method()). + Int("line", line). + Any("request_body", req). + Msg("request succeeded") + } + if err := s.Err(); err != nil { + // We're reading memory buffered data here, so this isn't likely an emphermal IO failure + // but rather something like line larger than the buffer size. + ll.Fatal().Err(err).Msg("reading input data") + } + Output( + ctx, + fmt.Sprintf("Response to %s command for %s", method, d.BestName()), + "response", + resp, + rawResp.Response, + ) + } + return nil + } +} diff --git a/cmd/shelly_put_tls_client_cert.go b/cmd/shelly_put_tls_client_cert.go new file mode 100644 index 0000000..28aa262 --- /dev/null +++ b/cmd/shelly_put_tls_client_cert.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/jcodybaker/go-shelly" + "github.com/spf13/cobra" +) + +var ( + shellyPutTLSClientCertCmd = &cobra.Command{ + Use: "put-tls-client-cert", + } +) + +func init() { + // Passing a whole cert as a cmd argument is awkward, but viper supports config files which + // should facilitate multi-line strings in yaml, JSON, etc. + shellyPutTLSClientCertCmd.Flags().String( + "data", "", "PEM encoded certificate data. (one of --data, --data-file, or --remove-cert is required)", + ) + shellyPutTLSClientCertCmd.Flags().String( + "data-file", "", "path to a file containing PEM encoded certificate data.", + ) + shellyPutTLSClientCertCmd.Flags().Bool( + "remove-cert", false, "remove an existing certificate from the device", + ) + shellyComponent.Parent.AddCommand(shellyPutTLSClientCertCmd) + discoveryFlags(shellyPutTLSClientCertCmd.Flags(), discoveryFlagsOptions{interactive: true}) + shellyPutTLSClientCertCmd.RunE = newDataCommand( + func(data *string, append bool) shelly.RPCRequestBody { + return &shelly.ShellyPutTLSClientCertRequest{ + Data: data, + Append: append, + } + }, "data", "data-file", "remove-cert") +} diff --git a/cmd/shelly_put_tls_client_key.go b/cmd/shelly_put_tls_client_key.go new file mode 100644 index 0000000..acd6de8 --- /dev/null +++ b/cmd/shelly_put_tls_client_key.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/jcodybaker/go-shelly" + "github.com/spf13/cobra" +) + +var ( + shellyPutTLSClientKeyCmd = &cobra.Command{ + Use: "put-tls-client-key", + } +) + +func init() { + // Passing a whole key as a cmd argument is awkward, but viper supports config files which + // should facilitate multi-line strings in yaml, JSON, etc. + shellyPutTLSClientKeyCmd.Flags().String( + "data", "", "PEM encoded key data. (one of --data, --data-file, or --remove-key is required)", + ) + shellyPutTLSClientKeyCmd.Flags().String( + "data-file", "", "path to a file containing PEM encoded key data.", + ) + shellyPutTLSClientKeyCmd.Flags().Bool( + "remove-key", false, "remove an existing key from the device", + ) + shellyComponent.Parent.AddCommand(shellyPutTLSClientKeyCmd) + discoveryFlags(shellyPutTLSClientKeyCmd.Flags(), discoveryFlagsOptions{interactive: true}) + shellyPutTLSClientKeyCmd.RunE = newDataCommand( + func(data *string, append bool) shelly.RPCRequestBody { + return &shelly.ShellyPutTLSClientKeyRequest{ + Data: data, + Append: append, + } + }, "data", "data-file", "remove-key") +} diff --git a/cmd/shelly_put_user_ca.go b/cmd/shelly_put_user_ca.go new file mode 100644 index 0000000..42b76e6 --- /dev/null +++ b/cmd/shelly_put_user_ca.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/jcodybaker/go-shelly" + "github.com/spf13/cobra" +) + +var ( + shellyPutUserCACmd = &cobra.Command{ + Use: "put-user-ca", + } +) + +func init() { + // Passing a whole cert as a cmd argument is awkward, but viper supports config files which + // should facilitate multi-line strings in yaml, JSON, etc. + shellyPutUserCACmd.Flags().String( + "data", "", "PEM encoded certificate authority data. (one of --data, --data-file, or --remove-ca is required)", + ) + shellyPutUserCACmd.Flags().String( + "data-file", "", "path to a file containing PEM encoded certificate authority data.", + ) + shellyPutUserCACmd.Flags().Bool( + "remove-ca", false, "remove an existing CA certificate from the device", + ) + shellyComponent.Parent.AddCommand(shellyPutUserCACmd) + discoveryFlags(shellyPutUserCACmd.Flags(), discoveryFlagsOptions{interactive: true}) + shellyPutUserCACmd.RunE = newDataCommand( + func(data *string, append bool) shelly.RPCRequestBody { + return &shelly.ShellyPutUserCARequest{ + Data: data, + Append: append, + } + }, "data", "data-file", "remove-ca") +} diff --git a/cmd/shelly_set_user_ca.go b/cmd/shelly_set_user_ca.go deleted file mode 100644 index 2fcd7ff..0000000 --- a/cmd/shelly_set_user_ca.go +++ /dev/null @@ -1,152 +0,0 @@ -package cmd - -import ( - "bufio" - "bytes" - "errors" - "io" - "os" - - "github.com/jcodybaker/go-shelly" - "github.com/jcodybaker/shellyctl/pkg/discovery" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - shellyPutUserCACmd = &cobra.Command{ - Use: "put-user-ca", - RunE: shellyPutUserCACmdRunE, - } -) - -func init() { - // Passing a whole cert as a cmd argument is awkward, but viper supports config files which - // should facilitate multi-line strings in yaml, JSON, etc. - shellyPutUserCACmd.Flags().String( - "ca-data", "", "PEM encoded certificate authority data. (either --ca-data OR --ca-data-file is required)", - ) - shellyPutUserCACmd.Flags().String( - "ca-data-file", "", "path to a file containing PEM encoded certificate authority data.", - ) - shellyPutUserCACmd.Flags().Bool( - "remove-ca", false, "remove an existing CA certificate from the device", - ) - shellyComponent.Parent.AddCommand(shellyPutUserCACmd) - discoveryFlags(shellyPutUserCACmd.Flags(), discoveryFlagsOptions{interactive: true}) -} - -func shellyPutUserCACmdRunE(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - ll := log.Ctx(ctx).With().Str("request", (&shelly.ShellyPutUserCARequest{}).Method()).Logger() - - var b *bytes.Buffer - data := viper.GetString("ca-data") - dataFile := viper.GetString("ca-data-file") - remove := viper.GetBool("remove-ca") - if (data != "" && dataFile != "") || (data != "" && remove) || (dataFile != "" && remove) { - return errors.New("--ca-data, --ca-data-file, and --remove-ca options are mutually exclusive") - } - if data == "" && dataFile == "" && !remove { - return errors.New("exactly one of `--ca-data`, `--ca-data-file`, or `--remove-ca` options are required") - } - if data != "" { - b = bytes.NewBufferString(data) - } else if dataFile == "-" { - b = &bytes.Buffer{} - if _, err := io.Copy(b, os.Stdin); err != nil { - ll.Fatal().Err(err).Msg("reading stdin for --ca-data-file") - } - } else if dataFile != "" { - b = &bytes.Buffer{} - f, err := os.Open(dataFile) - if err != nil { - ll.Fatal().Err(err).Str("ca-data-file", dataFile).Msg("reading --ca-data-file") - } - n, err := io.Copy(b, f) - if err != nil { - ll.Fatal().Err(err).Str("ca-data-file", dataFile).Msg("reading --ca-data-file") - } - ll.Debug().Int64("bytes_read", n).Str("ca-data-file", dataFile).Msg("finished reading --ca-data-file") - if err := f.Close(); err != nil { - ll.Warn().Err(err).Str("ca-data-file", dataFile).Msg("closing --ca-data-file") - } - } - - dOpts, err := discoveryOptionsFromFlags(cmd.Flags()) - if err != nil { - ll.Fatal().Err(err).Msg("parsing flags") - } - - discoverer := discovery.NewDiscoverer(dOpts...) - if err := discoveryAddDevices(ctx, discoverer); err != nil { - ll.Fatal().Err(err).Msg("adding devices") - } - - if _, err := discoverer.Search(ctx); err != nil { - return err - } - - for _, d := range discoverer.AllDevices() { - ll := d.Log(ll) - conn, err := d.Open(ctx) - if err != nil { - return err - } - defer func() { - if err := conn.Disconnect(ctx); err != nil { - ll.Warn().Err(err).Msg("disconnecting from device") - } - }() - if remove { - req := &shelly.ShellyPutUserCARequest{} - ll.Debug(). - Str("method", req.Method()). - Any("request_body", req). - Msg("sending request to clear data") - _, _, err := req.Do(ctx, conn, d.AuthCallback(ctx)) - if err != nil { - if viper.GetBool("skip-failed-hosts") { - ll.Err(err).Msg("error executing request; contining because --skip-failed-hosts=true") - continue - } else { - ll.Fatal().Err(err).Msg("error executing request") - } - } - ll.Info().Str("method", req.Method()).Msg("successfully cleared data") - continue - } - s := bufio.NewScanner(b) - req := &shelly.ShellyPutUserCARequest{} - var line int - for s.Scan() { - line++ - req.Data = shelly.StrPtr(s.Text()) - req.Append = line > 1 - ll.Debug(). - Str("method", req.Method()). - Int("line", line). - Any("request_body", req). - Msg("sending data") - if _, _, err := req.Do(ctx, conn, d.AuthCallback(ctx)); err != nil { - return err - } - ll.Debug(). - Str("method", req.Method()). - Int("line", line). - Any("request_body", req). - Msg("request succeeded") - } - if err := s.Err(); err != nil { - // We're reading memory buffered data here, so this isn't likely an emphermal IO failure - // but rather something like line larger than the buffer size. - ll.Fatal().Err(err).Str("method", req.Method()).Msg("reading input data") - } - ll.Info(). - Str("method", req.Method()). - Int("lines", line). - Msg("upload succeeded") - } - return nil -}