package main import ( "bytes" "fmt" "io" "mime" "net/http" "os" "path" "strings" "github.com/Luzifer/ots/pkg/client" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) type ( authRoundTripper struct { http.RoundTripper headers http.Header user, pass string } ) var createCmd = &cobra.Command{ Use: "create [-f file]... [--instance url] [--secret-from file]", Short: "Create a new encrypted secret in the given OTS instance", Long: "", Example: `echo "I'm a very secret secret" | ots-cli create`, Args: cobra.NoArgs, RunE: createRunE, } func init() { createCmd.Flags().Duration("expire", 0, "When to expire the secret (0 to use server-default)") createCmd.Flags().StringSliceP("header", "H", nil, "Headers to include in the request (i.e. 'Authorization: Token ...')") createCmd.Flags().String("instance", "https://ots.fyi/", "Instance to create the secret with") createCmd.Flags().StringSliceP("file", "f", nil, "File(s) to attach to the secret") createCmd.Flags().Bool("no-text", false, "Disable secret read (create a secret with only files)") createCmd.Flags().String("secret-from", "-", `File to read the secret content from ("-" for STDIN)`) createCmd.Flags().StringP("user", "u", "", "Username / Password for basic auth, specified as 'user:pass'") rootCmd.AddCommand(createCmd) } func createRunE(cmd *cobra.Command, _ []string) (err error) { cmd.SilenceUsage = true var secret client.Secret if client.HTTPClient, err = constructHTTPClient(cmd); err != nil { return fmt.Errorf("constructing authorized HTTP client: %w", err) } // Read the secret content logrus.Info("reading secret content...") if secret.Secret, err = getSecretContent(cmd); err != nil { return fmt.Errorf("getting secret content: %w", err) } // Attach any file given files, err := cmd.Flags().GetStringSlice("file") if err != nil { return fmt.Errorf("getting file flag: %w", err) } for _, f := range files { logrus.WithField("file", f).Info("attaching file...") content, err := os.ReadFile(f) //#nosec:G304 // Opening user specified file is intended if err != nil { return fmt.Errorf("reading attachment %q: %w", f, err) } secret.Attachments = append(secret.Attachments, client.SecretAttachment{ Name: path.Base(f), Type: mime.TypeByExtension(path.Ext(f)), Content: content, }) } if secret.Secret == "" && secret.Attachments == nil { return fmt.Errorf("secret has no content and no attachments") } // Get flags for creation logrus.Info("creating the secret...") instanceURL, err := cmd.Flags().GetString("instance") if err != nil { return fmt.Errorf("getting instance flag: %w", err) } expire, err := cmd.Flags().GetDuration("expire") if err != nil { return fmt.Errorf("getting expire flag: %w", err) } // Execute sanity checks if err = client.SanityCheck(instanceURL, secret); err != nil { return fmt.Errorf("sanity checking secret: %w", err) } // Create the secret secretURL, expiresAt, err := client.Create(instanceURL, secret, expire) if err != nil { return fmt.Errorf("creating secret: %w", err) } // Tell them where to find the secret if expiresAt.IsZero() { logrus.Info("secret created, see URL below") } else { logrus.WithField("expires-at", expiresAt).Info("secret created, see URL below") } fmt.Println(secretURL) //nolint:forbidigo // Output intended for STDOUT return nil } func constructHTTPClient(cmd *cobra.Command) (*http.Client, error) { basic, _ := cmd.Flags().GetString("user") headers, _ := cmd.Flags().GetStringSlice("header") if basic == "" && headers == nil { // No authorization needed return http.DefaultClient, nil } t := authRoundTripper{RoundTripper: http.DefaultTransport, headers: http.Header{}} // Set basic auth if available user, pass, ok := strings.Cut(basic, ":") if ok { t.user = user t.pass = pass } // Parse and set headers if available for _, hdr := range headers { key, value, ok := strings.Cut(hdr, ":") if !ok { logrus.WithField("header", hdr).Warn("invalid header format, skipping") continue } t.headers.Add(key, strings.TrimSpace(value)) } return &http.Client{Transport: t}, nil } func getSecretContent(cmd *cobra.Command) (string, error) { secretSourceName, err := cmd.Flags().GetString("secret-from") if err != nil { return "", fmt.Errorf("getting secret-from flag: %w", err) } noSecret, err := cmd.Flags().GetBool("no-text") if err != nil { return "", fmt.Errorf("getting no-text flag: %w", err) } var secretSource io.Reader switch { case noSecret: secretSource = bytes.NewReader(nil) case secretSourceName == "-": secretSource = os.Stdin default: f, err := os.Open(secretSourceName) //#nosec:G304 // Opening user specified file is intended if err != nil { return "", fmt.Errorf("opening secret-from file: %w", err) } defer f.Close() //nolint:errcheck // The file will be force-closed by program exit secretSource = f } secretContent, err := io.ReadAll(secretSource) if err != nil { return "", fmt.Errorf("reading secret content: %w", err) } return strings.TrimSpace(string(secretContent)), nil } func (a authRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { if a.user != "" { r.SetBasicAuth(a.user, a.pass) } for key, values := range a.headers { if r.Header == nil { r.Header = http.Header{} } for _, value := range values { r.Header.Add(key, value) } } resp, err := a.RoundTripper.RoundTrip(r) if err != nil { return nil, fmt.Errorf("executing round-trip: %w", err) } return resp, nil }