package cli import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "os" "path" "path/filepath" "runtime" "strings" "time" "github.com/schollz/cli/v2" "github.com/schollz/croc/v9/src/comm" "github.com/schollz/croc/v9/src/croc" "github.com/schollz/croc/v9/src/models" "github.com/schollz/croc/v9/src/tcp" "github.com/schollz/croc/v9/src/utils" log "github.com/schollz/logger" "github.com/schollz/pake/v3" ) // Version specifies the version var Version string // Run will run the command line program func Run() (err error) { // use all of the processors runtime.GOMAXPROCS(runtime.NumCPU()) app := cli.NewApp() app.Name = "croc" if Version == "" { Version = "v8.6.12-c373b38" } app.Version = Version app.Compiled = time.Now() app.Usage = "easily and securely transfer stuff from one computer to another" app.UsageText = `Send a file: croc send file.txt Send a file with a custom code: croc send --code secret-code file.txt Receive a file using code: croc secret-code` app.Commands = []*cli.Command{ { Name: "send", Usage: "send a file (see options with croc send -h)", Description: "send a file over the relay", ArgsUsage: "[filename]", Flags: []cli.Flag{ &cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay"}, &cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"}, &cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"}, &cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"}, &cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the local relay (optional)"}, }, HelpName: "croc send", Action: func(c *cli.Context) error { return send(c) }, }, { Name: "relay", Usage: "start your own relay (optional)", Description: "start relay", HelpName: "croc relay", Action: func(c *cli.Context) error { return relay(c) }, Flags: []cli.Flag{ &cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"}, }, }, } app.Flags = []cli.Flag{ &cli.BoolFlag{Name: "remember", Usage: "save these settings to reuse next time"}, &cli.BoolFlag{Name: "debug", Usage: "toggle debug mode"}, &cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"}, &cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"}, &cli.BoolFlag{Name: "no-compress", Usage: "disable compression"}, &cli.BoolFlag{Name: "ask", Usage: "make sure sender and recipient are prompted"}, &cli.BoolFlag{Name: "local", Usage: "force to use only local connections"}, &cli.BoolFlag{Name: "ignore-stdin", Usage: "ignore piped stdin"}, &cli.BoolFlag{Name: "overwrite", Usage: "do not prompt to overwrite"}, &cli.StringFlag{Name: "curve", Value: "siec", Usage: "choose an encryption curve (" + strings.Join(pake.AvailableCurves(), ", ") + ")"}, &cli.StringFlag{Name: "ip", Value: "", Usage: "set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009"}, &cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}}, &cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}}, &cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"}, &cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}}, &cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}}, } app.EnableBashCompletion = true app.HideHelp = false app.HideVersion = false app.Action = func(c *cli.Context) error { allStringsAreFiles := func(strs []string) bool { for _, str := range strs { if !utils.Exists(str) { return false } } return true } // if trying to send but forgot send, let the user know if c.Args().Present() && allStringsAreFiles(c.Args().Slice()) { fnames := []string{} for _, fpath := range c.Args().Slice() { _, basename := filepath.Split(fpath) fnames = append(fnames, "'"+basename+"'") } yn := utils.GetInput(fmt.Sprintf("Did you mean to send %s? (y/n) ", strings.Join(fnames, ", "))) if strings.ToLower(yn) == "y" { return send(c) } } return receive(c) } return app.Run(os.Args) } func getConfigDir() (homedir string, err error) { homedir, err = os.UserHomeDir() if err != nil { log.Error(err) return } if envHomedir, isSet := os.LookupEnv("CROC_CONFIG_DIR"); isSet { homedir = envHomedir } else if xdgConfigHome, isSet := os.LookupEnv("XDG_CONFIG_HOME"); isSet { homedir = path.Join(xdgConfigHome, "croc") } else { homedir = path.Join(homedir, ".config", "croc") } if _, err = os.Stat(homedir); os.IsNotExist(err) { log.Debugf("creating home directory %s", homedir) err = os.MkdirAll(homedir, 0700) } return } func setDebugLevel(c *cli.Context) { if c.Bool("debug") { log.SetLevel("debug") log.Debug("debug mode on") } else { log.SetLevel("info") } } func getConfigFile() string { configFile, err := getConfigDir() if err != nil { log.Error(err) return "" } return path.Join(configFile, "send.json") } func determinePass(c *cli.Context) (pass string) { pass = c.String("pass") b, err := ioutil.ReadFile(pass) if err == nil { pass = strings.TrimSpace(string(b)) } return } func send(c *cli.Context) (err error) { setDebugLevel(c) comm.Socks5Proxy = c.String("socks5") crocOptions := croc.Options{ SharedSecret: c.String("code"), IsSender: true, Debug: c.Bool("debug"), NoPrompt: c.Bool("yes"), RelayAddress: c.String("relay"), RelayAddress6: c.String("relay6"), Stdout: c.Bool("stdout"), DisableLocal: c.Bool("no-local"), OnlyLocal: c.Bool("local"), IgnoreStdin: c.Bool("ignore-stdin"), RelayPorts: strings.Split(c.String("ports"), ","), Ask: c.Bool("ask"), NoMultiplexing: c.Bool("no-multi"), RelayPassword: determinePass(c), SendingText: c.String("text") != "", NoCompress: c.Bool("no-compress"), Overwrite: c.Bool("overwrite"), Curve: c.String("curve"), } if crocOptions.RelayAddress != models.DEFAULT_RELAY { crocOptions.RelayAddress6 = "" } else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 { crocOptions.RelayAddress = "" } b, errOpen := ioutil.ReadFile(getConfigFile()) if errOpen == nil && !c.Bool("remember") { var rememberedOptions croc.Options err = json.Unmarshal(b, &rememberedOptions) if err != nil { log.Error(err) return } // update anything that isn't explicitly set if !c.IsSet("relay") { crocOptions.RelayAddress = rememberedOptions.RelayAddress } if !c.IsSet("no-local") { crocOptions.DisableLocal = rememberedOptions.DisableLocal } if !c.IsSet("ports") { crocOptions.RelayPorts = rememberedOptions.RelayPorts } if !c.IsSet("code") { crocOptions.SharedSecret = rememberedOptions.SharedSecret } if !c.IsSet("pass") { crocOptions.RelayPassword = rememberedOptions.RelayPassword } } var fnames []string stat, _ := os.Stdin.Stat() if ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool("ignore-stdin") { fnames, err = getStdin() if err != nil { return } defer func() { e := os.Remove(fnames[0]) if e != nil { log.Error(e) } }() } else if c.String("text") != "" { fnames, err = makeTempFileWithString(c.String("text")) if err != nil { return } defer func() { e := os.Remove(fnames[0]) if e != nil { log.Error(e) } }() } else { fnames = c.Args().Slice() } if len(fnames) == 0 { return errors.New("must specify file: croc send [filename]") } if len(crocOptions.SharedSecret) == 0 { // generate code phrase crocOptions.SharedSecret = utils.GetRandomName() } paths, haveFolder, err := getPaths(fnames) if err != nil { return } cr, err := croc.New(crocOptions) if err != nil { return } // save the config saveConfig(c, crocOptions) err = cr.Send(croc.TransferOptions{ PathToFiles: paths, KeepPathInRemote: haveFolder, }) return } func getStdin() (fnames []string, err error) { f, err := ioutil.TempFile(".", "croc-stdin-") if err != nil { return } _, err = io.Copy(f, os.Stdin) if err != nil { return } err = f.Close() if err != nil { return } fnames = []string{f.Name()} return } func makeTempFileWithString(s string) (fnames []string, err error) { f, err := ioutil.TempFile(".", "croc-stdin-") if err != nil { return } _, err = f.WriteString(s) if err != nil { return } err = f.Close() if err != nil { return } fnames = []string{f.Name()} return } func getPaths(fnames []string) (paths []string, haveFolder bool, err error) { haveFolder = false paths = []string{} for _, fname := range fnames { stat, errStat := os.Lstat(fname) if errStat != nil { err = errStat return } if stat.IsDir() { haveFolder = true err = filepath.Walk(fname, func(pathName string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { paths = append(paths, filepath.ToSlash(pathName)) } return nil }) if err != nil { return } } else { paths = append(paths, filepath.ToSlash(fname)) } } return } func saveConfig(c *cli.Context, crocOptions croc.Options) { if c.Bool("remember") { configFile := getConfigFile() log.Debug("saving config file") var bConfig []byte // if the code wasn't set, don't save it if c.String("code") == "" { crocOptions.SharedSecret = "" } bConfig, err := json.MarshalIndent(crocOptions, "", " ") if err != nil { log.Error(err) return } err = ioutil.WriteFile(configFile, bConfig, 0644) if err != nil { log.Error(err) return } log.Debugf("wrote %s", configFile) } } func receive(c *cli.Context) (err error) { comm.Socks5Proxy = c.String("socks5") crocOptions := croc.Options{ SharedSecret: c.String("code"), IsSender: false, Debug: c.Bool("debug"), NoPrompt: c.Bool("yes"), RelayAddress: c.String("relay"), RelayAddress6: c.String("relay6"), Stdout: c.Bool("stdout"), Ask: c.Bool("ask"), RelayPassword: determinePass(c), OnlyLocal: c.Bool("local"), IP: c.String("ip"), Overwrite: c.Bool("overwrite"), Curve: c.String("curve"), } if crocOptions.RelayAddress != models.DEFAULT_RELAY { crocOptions.RelayAddress6 = "" } else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 { crocOptions.RelayAddress = "" } switch c.Args().Len() { case 1: crocOptions.SharedSecret = c.Args().First() case 3: var phrase []string phrase = append(phrase, c.Args().First()) phrase = append(phrase, c.Args().Tail()...) crocOptions.SharedSecret = strings.Join(phrase, "-") } // load options here setDebugLevel(c) configFile, err := getConfigDir() if err != nil { log.Error(err) return } configFile = path.Join(configFile, "receive.json") b, errOpen := ioutil.ReadFile(configFile) if errOpen == nil && !c.Bool("remember") { var rememberedOptions croc.Options err = json.Unmarshal(b, &rememberedOptions) if err != nil { log.Error(err) return } // update anything that isn't expliciGlobalIsSettly set if !c.IsSet("relay") { crocOptions.RelayAddress = rememberedOptions.RelayAddress } if !c.IsSet("yes") { crocOptions.NoPrompt = rememberedOptions.NoPrompt } if crocOptions.SharedSecret == "" { crocOptions.SharedSecret = rememberedOptions.SharedSecret } if !c.IsSet("pass") { crocOptions.RelayPassword = rememberedOptions.RelayPassword } } if crocOptions.SharedSecret == "" { crocOptions.SharedSecret = utils.GetInput("Enter receive code: ") } if c.String("out") != "" { if err = os.Chdir(c.String("out")); err != nil { return err } } cr, err := croc.New(crocOptions) if err != nil { return } // save the config if c.Bool("remember") { log.Debug("saving config file") var bConfig []byte bConfig, err = json.MarshalIndent(crocOptions, "", " ") if err != nil { log.Error(err) return } err = ioutil.WriteFile(configFile, bConfig, 0644) if err != nil { log.Error(err) return } log.Debugf("wrote %s", configFile) } err = cr.Receive() return } func relay(c *cli.Context) (err error) { log.Infof("starting croc relay version %v", Version) debugString := "info" if c.Bool("debug") { debugString = "debug" } ports := strings.Split(c.String("ports"), ",") tcpPorts := strings.Join(ports[1:], ",") for i, port := range ports { if i == 0 { continue } go func(portStr string) { err = tcp.Run(debugString, portStr, determinePass(c)) if err != nil { panic(err) } }(port) } return tcp.Run(debugString, ports[0], determinePass(c), tcpPorts) }