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 }