github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/cmd/tools.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"crypto/rsa"
     7  	"crypto/sha1"
     8  	"crypto/x509"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"net/url"
    14  	"os"
    15  	"os/exec"
    16  	"runtime"
    17  
    18  	"github.com/cozy/cozy-stack/client/request"
    19  	"github.com/cozy/cozy-stack/cmd/browser"
    20  	build "github.com/cozy/cozy-stack/pkg/config"
    21  	"github.com/spf13/cobra"
    22  )
    23  
    24  var toolsCmdGroup = &cobra.Command{
    25  	Use:   "tools <command>",
    26  	Short: "Regroup some tools for debugging and tests",
    27  	RunE: func(cmd *cobra.Command, args []string) error {
    28  		return cmd.Usage()
    29  	},
    30  }
    31  
    32  var heapCmd = &cobra.Command{
    33  	Use:   "heap",
    34  	Short: "Dump a sampling of memory allocations of live objects",
    35  	Long: `
    36  This command can be used for memory profiling. It dumps a sampling of memory
    37  allocations of live objects on stdout.
    38  
    39  See https://go.dev/doc/diagnostics#profiling.
    40  `,
    41  	Example: "$ cozy-stack tools heap > heap.pprof && go tool pprof heap.pprof",
    42  	RunE: func(cmd *cobra.Command, args []string) error {
    43  		ac := newAdminClient()
    44  		heap, err := ac.ProfileHeap()
    45  		if err != nil {
    46  			return err
    47  		}
    48  		_, err = io.Copy(os.Stdout, heap)
    49  		if errc := heap.Close(); errc != nil && err == nil {
    50  			err = errc
    51  		}
    52  		return err
    53  	},
    54  }
    55  
    56  var unxorDocumentID = &cobra.Command{
    57  	Use:   "unxor-document-id <domain> <sharing_id> <document_id>",
    58  	Short: "transform the id of a shared document",
    59  	Long: `
    60  This command can be used when you have the identifier of a shared document on a
    61  recipient instance, and you want the identifier of the same document on the
    62  owner's instance.
    63  `,
    64  	Example: `$ cozy-stack tools unxor-document-id bob.localhost:8080 7f47c470c7b1013a8a8818c04daba326 8cced87acb34b151cc8d7e864e0690ed`,
    65  	RunE: func(cmd *cobra.Command, args []string) error {
    66  		if len(args) != 3 {
    67  			return cmd.Usage()
    68  		}
    69  		ac := newAdminClient()
    70  		path := fmt.Sprintf("/instances/%s/sharings/%s/unxor/%s", args[0], args[1], args[2])
    71  		res, err := ac.Req(&request.Options{
    72  			Method: "GET",
    73  			Path:   path,
    74  		})
    75  		if err != nil {
    76  			return err
    77  		}
    78  		defer res.Body.Close()
    79  
    80  		var data map[string]interface{}
    81  		if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
    82  			return err
    83  		}
    84  		fmt.Fprintf(os.Stdout, "ID: %q\n", data["id"])
    85  		return nil
    86  	},
    87  }
    88  
    89  var encryptRSACmd = &cobra.Command{
    90  	Use:   "encrypt-with-rsa <key> <payload",
    91  	Short: "encrypt a payload in RSA",
    92  	Long: `
    93  This command is used by system tests to encrypt bitwarden organization keys. It
    94  takes the public or private key of the user and the payload (= the organization
    95  key) as inputs (both encoded in base64), and print on stdout the encrypted data
    96  (encoded as base64 too).
    97  `,
    98  	RunE: func(cmd *cobra.Command, args []string) error {
    99  		if len(args) != 2 {
   100  			return cmd.Usage()
   101  		}
   102  		encryptKey, err := base64.StdEncoding.DecodeString(args[0])
   103  		if err != nil {
   104  			return err
   105  		}
   106  		payload, err := base64.StdEncoding.DecodeString(args[1])
   107  		if err != nil {
   108  			return err
   109  		}
   110  		pub, err := getEncryptKey(encryptKey)
   111  		if err != nil {
   112  			return err
   113  		}
   114  		hash := sha1.New()
   115  		rng := rand.Reader
   116  		encrypted, err := rsa.EncryptOAEP(hash, rng, pub, payload, nil)
   117  		if err != nil {
   118  			return err
   119  		}
   120  		fmt.Fprintf(os.Stdout, "4.%s", base64.StdEncoding.EncodeToString(encrypted))
   121  		return nil
   122  	},
   123  }
   124  
   125  func getEncryptKey(key []byte) (*rsa.PublicKey, error) {
   126  	pubKey, err := x509.ParsePKIXPublicKey(key)
   127  	if err == nil {
   128  		return pubKey.(*rsa.PublicKey), nil
   129  	}
   130  	privateKey, err := x509.ParsePKCS8PrivateKey(key)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	return &privateKey.(*rsa.PrivateKey).PublicKey, nil
   135  }
   136  
   137  const bugHeader = `Please answer these questions before submitting your issue. Thanks!
   138  
   139  
   140  #### What did you do?
   141  
   142  If possible, provide a recipe for reproducing the error.
   143  
   144  
   145  #### What did you expect to see?
   146  
   147  
   148  #### What did you see instead?
   149  
   150  
   151  `
   152  
   153  type body struct {
   154  	buf bytes.Buffer
   155  	err error
   156  }
   157  
   158  func (b *body) Append(format string, args ...interface{}) {
   159  	if b.err != nil {
   160  		return
   161  	}
   162  	_, b.err = fmt.Fprintf(&b.buf, format+"\n", args...)
   163  }
   164  
   165  func (b *body) String() string {
   166  	return b.buf.String()
   167  }
   168  
   169  // bugCmd represents the `cozy-stack bug` command, inspired from go bug.
   170  // Cf https://tip.golang.org/src/cmd/go/internal/bug/bug.go
   171  var bugCmd = &cobra.Command{
   172  	Use:   "bug",
   173  	Short: "start a bug report",
   174  	Long: `
   175  Bug opens the default browser and starts a new bug report.
   176  The report includes useful system information.
   177  	`,
   178  	RunE: func(cmd *cobra.Command, args []string) error {
   179  		var b body
   180  		b.Append("%s", bugHeader)
   181  		b.Append("#### System details\n")
   182  		b.Append("```")
   183  		b.Append("cozy-stack %s", build.Version)
   184  		b.Append("build in mode %s - %s\n", build.BuildMode, build.BuildTime)
   185  		b.Append("go version %s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
   186  		printOSDetails(&b.buf)
   187  		b.Append("```")
   188  		if b.err != nil {
   189  			return b.err
   190  		}
   191  		param := url.QueryEscape(b.String())
   192  		url := "https://github.com/cozy/cozy-stack/issues/new?body=" + param
   193  		if !browser.Open(url) {
   194  			fmt.Print("Please file a new issue at https://github.com/cozy/cozy-stack/issues/new using this template:\n\n")
   195  			fmt.Print(b.String())
   196  		}
   197  		return nil
   198  	},
   199  }
   200  
   201  func init() {
   202  	toolsCmdGroup.AddCommand(heapCmd)
   203  	toolsCmdGroup.AddCommand(unxorDocumentID)
   204  	toolsCmdGroup.AddCommand(encryptRSACmd)
   205  	toolsCmdGroup.AddCommand(bugCmd)
   206  	RootCmd.AddCommand(toolsCmdGroup)
   207  }
   208  
   209  func printOSDetails(w io.Writer) {
   210  	switch runtime.GOOS {
   211  	case "darwin":
   212  		printCmdOut(w, "uname -v: ", "uname", "-v")
   213  		printCmdOut(w, "", "sw_vers")
   214  
   215  	case "linux":
   216  		printCmdOut(w, "uname -sr: ", "uname", "-sr")
   217  		printCmdOut(w, "", "lsb_release", "-a")
   218  
   219  	case "openbsd", "netbsd", "freebsd", "dragonfly":
   220  		printCmdOut(w, "uname -v: ", "uname", "-v")
   221  	}
   222  }
   223  
   224  // printCmdOut prints the output of running the given command.
   225  // It ignores failures; 'go bug' is best effort.
   226  func printCmdOut(w io.Writer, prefix, path string, args ...string) {
   227  	cmd := exec.Command(path, args...)
   228  	out, err := cmd.Output()
   229  	if err != nil {
   230  		return
   231  	}
   232  	fmt.Fprintf(w, "%s%s\n", prefix, bytes.TrimSpace(out))
   233  }