package cli import ( "context" "encoding/json" "errors" "fmt" "io" "os" "path" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/schollz/croc/v11/internal/cli" "github.com/schollz/croc/v11/src/codephrase" "github.com/schollz/croc/v11/src/comm" "github.com/schollz/croc/v11/src/croc" log "github.com/schollz/croc/v11/src/logger" "github.com/schollz/croc/v11/src/models" "github.com/schollz/croc/v11/src/publicrelay" "github.com/schollz/croc/v11/src/storeclient" "github.com/schollz/croc/v11/src/tcp" "github.com/schollz/croc/v11/src/termui" "github.com/schollz/croc/v11/src/utils" buildversion "github.com/schollz/croc/v11/src/version" "github.com/schollz/pake/v3" ) // Version specifies the version var Version = buildversion.Value // Run will run the command line program func Run() (err error) { return RunContext(context.Background()) } // RunContext runs the command line program and propagates cancellation to // context-aware commands such as croc ssh. func RunContext(ctx context.Context) (err error) { // use all of the processors runtime.GOMAXPROCS(runtime.NumCPU()) err = newApp().RunContext(ctx, os.Args) if ctxErr := ctx.Err(); ctxErr != nil { return ctxErr } return err } func newApp() *cli.App { app := cli.NewApp() app.Name = "croc" app.Version = Version app.Compiled = time.Now() app.Usage = "securely transfer files or share a terminal" app.UsageText = `croc [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder] USAGE EXAMPLES: Send a file: croc send file.txt -git to respect your .gitignore Send multiple files: croc send file1.txt file2.txt file3.txt or croc send *.jpg Send everything in a folder: croc send example-folder-name Send a file with a custom code: croc send --code secret-code file.txt Receive a file using code: croc secret-code Store files for later download: croc store --downloads 3 --expiration 3d file.txt Share a terminal: croc ssh Join a shared terminal: CROC_SECRET=six-word-invitation croc ssh` app.Commands = []*cli.Command{ { Name: "send", Usage: "send file(s), or folder (see options with croc send -h)", Description: "send file(s), or folder", ArgsUsage: "[filename(s) or folder]", Flags: append([]cli.Flag{ &cli.BoolFlag{Name: "zip", Usage: "zip folder before sending"}, &cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay (at least 6 characters)"}, &cli.StringFlag{Name: "transport", Value: string(croc.TransportAuto), Usage: "sender file data transport (auto, derp, relay)"}, &cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5, highway)"}, &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.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"}, &cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"}, &cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"}, &cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show the web receive URL as a qrcode"}, &cli.StringFlag{Name: "exclude", Value: "", Usage: "exclude files if they contain any of the comma separated strings"}, &cli.StringFlag{Name: "exclude-file", Value: "", Usage: "exclude files matching any of the comma separated relative paths exactly"}, &cli.StringFlag{Name: "socks5", Value: "", Usage: "SOCKS5 proxy address (relay DNS is resolved by the proxy)", EnvVars: []string{"SOCKS5_PROXY"}}, &cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}}, &cli.BoolFlag{Name: "store", Usage: "upload encrypted files for a finite lifetime or a limited number of verified downloads"}, }, storedUploadFlags("store-")...), HelpName: "croc send", Action: send, }, { Name: "store", Usage: "upload encrypted files for later download", Description: "store regular files with a download limit and expiration", ArgsUsage: "[filename(s)]", Flags: append(storedUploadFlags(""), &cli.StringFlag{Name: "socks5", Value: "", Usage: "SOCKS5 proxy address (relay DNS is resolved by the proxy)", EnvVars: []string{"SOCKS5_PROXY"}}, &cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}}, &cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show the web receive URL as a qrcode"}, ), HelpName: "croc store", Action: send, }, { Name: "ssh", Usage: "share or join a secure, collaborative terminal", Description: "start without a code to host; provide a six-word code to join", ArgsUsage: "[six-word-code]", Flags: []cli.Flag{ &cli.BoolFlag{Name: "headless", Usage: "host without attaching the local terminal"}, &cli.DurationFlag{Name: "duration", Value: 12 * time.Hour, Usage: "maximum hosted session lifetime"}, &cli.DurationFlag{Name: "reconnect-window", Value: 2 * time.Minute, Usage: "how long a guest retries after a connection loss"}, &cli.BoolFlag{Name: "no-reconnect", Usage: "disable automatic guest reconnection"}, &cli.StringFlag{Name: "transport", Value: "auto", Usage: "guest transport (auto, tailcat, relay)"}, &cli.StringFlag{Name: "dir", Value: ".", Usage: "working directory for a hosted shell"}, }, HelpName: "croc ssh", Action: sshSession, }, { Name: "update", Aliases: []string{"upgrade"}, Usage: "prepare croc-update to install the latest croc release", Description: "install or update the adjacent croc-update utility, then print the command to update croc; package-managed installs use their package manager", Flags: []cli.Flag{ &cli.BoolFlag{Name: "check", Usage: "check for a newer release without installing it"}, &cli.BoolFlag{Name: "yes", Usage: "install the updater without confirmation (OS authentication may still be required)"}, &cli.BoolFlag{Name: "register-installer", Hidden: true}, &cli.StringFlag{Name: "internal-install-version", Hidden: true}, }, HelpName: "croc update", Action: updateCommand, }, { Name: "relay", Usage: "start your own relay (optional)", Description: "start relay", HelpName: "croc relay", Action: relay, Flags: []cli.Flag{ &cli.StringFlag{Name: "host", Usage: "host of the relay"}, &cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay", EnvVars: []string{"CROC_PORTS"}}, &cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay", EnvVars: []string{"CROC_PORT"}}, &cli.IntFlag{Name: "transfers", Value: 5, Usage: "number of ports to use for relay"}, &cli.IntFlag{Name: "max-rooms-open", Value: tcp.DEFAULT_MAX_ROOMS_OPEN, Usage: "maximum waiting rooms per relay port", EnvVars: []string{"CROC_MAX_ROOMS_OPEN"}}, &cli.IntFlag{Name: "max-pending-handshakes", Value: tcp.DEFAULT_MAX_PENDING_HANDSHAKES, Usage: "maximum incomplete handshakes per relay port", EnvVars: []string{"CROC_MAX_PENDING_HANDSHAKES"}}, &cli.IntFlag{Name: "max-pending-handshakes-per-source", Value: tcp.DEFAULT_MAX_PENDING_HANDSHAKES_PER_SOURCE, Usage: "maximum incomplete handshakes per source IP per relay port", EnvVars: []string{"CROC_MAX_PENDING_HANDSHAKES_PER_SOURCE"}}, &cli.DurationFlag{Name: "first-message-timeout", Value: tcp.DEFAULT_FIRST_MESSAGE_TIMEOUT, Usage: "maximum time from accepting a connection to its first complete protocol message", EnvVars: []string{"CROC_FIRST_MESSAGE_TIMEOUT"}}, &cli.DurationFlag{Name: "handshake-timeout", Value: tcp.DEFAULT_HANDSHAKE_TIMEOUT, Usage: "maximum time for an initial relay handshake", EnvVars: []string{"CROC_HANDSHAKE_TIMEOUT"}}, &cli.IntFlag{Name: "source-join-limit", Value: tcp.DEFAULT_SOURCE_JOIN_LIMIT, Usage: "maximum room joins per source IP in the admission window", EnvVars: []string{"CROC_SOURCE_JOIN_LIMIT"}}, &cli.IntFlag{Name: "room-join-limit", Value: tcp.DEFAULT_ROOM_JOIN_LIMIT, Usage: "maximum joins per room in the admission window", EnvVars: []string{"CROC_ROOM_JOIN_LIMIT"}}, &cli.DurationFlag{Name: "join-limit-window", Value: tcp.DEFAULT_JOIN_LIMIT_WINDOW, Usage: "sliding window for relay admission limits", EnvVars: []string{"CROC_JOIN_LIMIT_WINDOW"}}, }, }, { Name: "generate-fish-completion", Usage: "generate fish completion and output to stdout", Hidden: true, Action: func(ctx *cli.Context) error { completion, err := ctx.App.ToFishCompletion() if err != nil { return err } fmt.Print(completion) return nil }, }, } app.Flags = []cli.Flag{ &cli.BoolFlag{Name: "internal-dns", Usage: "use a built-in DNS stub resolver rather than the host operating system"}, &cli.BoolFlag{Name: "classic", Usage: "toggle between the classic mode (insecure due to local attack vector) and new mode (secure)"}, &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 or resume"}, &cli.BoolFlag{Name: "rename", Usage: "receive files that already exist under a new name instead of prompting"}, &cli.BoolFlag{Name: "testing", Usage: "flag for testing purposes"}, &cli.BoolFlag{Name: "quiet", Usage: "disable all output"}, &cli.BoolFlag{Name: "disable-clipboard", Usage: "disable copy to clipboard"}, &cli.BoolFlag{Name: "extended-clipboard", Usage: "copy full command with secret as env variable to clipboard"}, &cli.BoolFlag{Name: "no-update-check", Usage: "skip the update check on send and receive", EnvVars: []string{"CROC_NO_UPDATE_CHECK"}}, &cli.StringFlag{Name: "revoke", Usage: "revoke a stored transfer using its local sender receipt"}, &cli.StringFlag{Name: "multicast", Value: "239.255.255.250", Usage: "multicast address to use for local discovery"}, &cli.StringFlag{Name: "curve", Value: "p256", 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: "web-url", Value: "https://getcroc.com", Usage: "web client origin for the receive link and QR code", EnvVars: []string{"CROC_WEB_URL"}}, &cli.StringFlag{Name: "socks5", Value: "", Usage: "SOCKS5 proxy address (relay DNS is resolved by the proxy)", EnvVars: []string{"SOCKS5_PROXY"}}, &cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}}, &cli.StringFlag{Name: "throttleUpload", Value: "", Usage: "throttle the upload speed e.g. 500k"}, } app.EnableBashCompletion = true app.HideHelp = false app.HideVersion = false app.Action = func(c *cli.Context) error { if c.Args().First() == "serve" { return errors.New("the web server has moved to the standalone croc-web binary") } if c.IsSet("revoke") { comm.Socks5Proxy = c.String("socks5") comm.HttpProxy = c.String("connect") return revokeStored(c, c.String("revoke")) } allStringsAreFiles := func(strs []string) bool { for _, str := range strs { if !utils.Exists(str) { return false } } return true } // check if "classic" is set classicFile := getClassicConfigFile(true) classicInsecureMode := utils.Exists(classicFile) if c.Bool("classic") { if classicInsecureMode { // classic mode not enabled output, colorEnabled := termui.Output(os.Stdout) fmt.Fprint(output, termui.PromptChoices(`Classic mode is currently ENABLED. Disabling this mode will prevent the shared secret from being visible on the host's process list when passed via the command line. On a multi-user system, this will help ensure that other local users cannot access the shared secret and receive the files instead of the intended recipient. Do you wish to continue to DISABLE the classic mode? (y/N) `, colorEnabled)) choice, inputErr := utils.GetInputContext(c.Context, "") if inputErr != nil { return inputErr } choice = strings.ToLower(choice) if choice == "y" || choice == "yes" { os.Remove(classicFile) fmt.Print("\nClassic mode DISABLED.\n\n") fmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase: Send: CROC_SECRET=*** croc send file.txt Receive: CROC_SECRET=*** croc` + "\n\n") } else { fmt.Print("\nClassic mode ENABLED.\n") } } else { // enable classic mode // touch the file output, colorEnabled := termui.Output(os.Stdout) fmt.Fprint(output, termui.PromptChoices(`Classic mode is currently DISABLED. Please note that enabling this mode will make the shared secret visible on the host's process list when passed via the command line. On a multi-user system, this could allow other local users to access the shared secret and receive the files instead of the intended recipient. Do you wish to continue to enable the classic mode? (y/N) `, colorEnabled)) choice, inputErr := utils.GetInputContext(c.Context, "") if inputErr != nil { return inputErr } choice = strings.ToLower(choice) if choice == "y" || choice == "yes" { fmt.Print("\nClassic mode ENABLED.\n\n") os.WriteFile(classicFile, []byte("enabled"), 0o644) fmt.Print(`To send and receive, use the code phrase: Send: croc send --code *** file.txt Receive: croc ***` + "\n\n") } else { fmt.Print("\nClassic mode DISABLED.\n") } } os.Exit(0) } // 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+"'") } promptMessage := fmt.Sprintf("Did you mean to send %s? (Y/n) ", strings.Join(fnames, ", ")) choice, errInput := utils.GetInputContext(c.Context, promptMessage) if errInput != nil { return fmt.Errorf("could not read confirmation (use 'croc send' to send without one): %w", errInput) } choice = strings.ToLower(choice) if choice == "" || choice == "y" || choice == "yes" { return send(c) } } return receive(c) } return app } func setDebugLevel(c *cli.Context) { if c.Bool("quiet") { log.SetLevel("error") } else if c.Bool("debug") { log.SetLevel("debug") log.Debug("debug mode on") // print the public IP address ip, err := utils.PublicIPContext(c.Context) if err == nil { log.Debugf("public IP address: %s", ip) } else { log.Debug(err) } } else { log.SetLevel("info") } } func getSendConfigFile(requireValidPath bool) string { configFile, err := utils.GetConfigDir(requireValidPath) if err != nil { log.Error(err) return "" } return path.Join(configFile, "send.json") } func getClassicConfigFile(requireValidPath bool) string { configFile, err := utils.GetConfigDir(requireValidPath) if err != nil { log.Error(err) return "" } return path.Join(configFile, "classic_enabled") } func getBestRelayCacheFile(requireValidPath bool) (string, error) { configDir, err := utils.GetConfigDir(requireValidPath) if err != nil { return "", err } return path.Join(configDir, "best-relay"), nil } func getReceiveConfigFile(requireValidPath bool) (string, error) { configFile, err := utils.GetConfigDir(requireValidPath) if err != nil { log.Error(err) return "", err } return path.Join(configFile, "receive.json"), nil } func determinePass(c *cli.Context) (pass string) { pass = c.String("pass") b, err := os.ReadFile(pass) if err == nil { pass = string(b) } pass = strings.TrimSpace(pass) return } func resolveSendSharedSecret(sharedSecret, envSecret string) string { if envSecret != "" { return envSecret } return sharedSecret } func usesPublicRelay(c *cli.Context, options croc.Options) bool { return !c.IsSet("relay") && !c.IsSet("relay6") && !options.OnlyLocal && options.IP == "" && options.RelayAddress == models.DEFAULT_RELAY && options.RelayAddress6 == models.DEFAULT_RELAY6 } func assignPublicRelay(options *croc.Options, relayIndex int) error { relays := publicrelay.Relays() if relayIndex < 0 || relayIndex >= len(relays) { return codephrase.ErrInvalidRelayIndex } options.RelayAddress = relays[relayIndex] options.RelayAddress6 = "" options.PublicRelay = true log.Debugf("public relay index %d selected: %s", relayIndex, options.RelayAddress) return nil } func assignPublicRelayForCode(options *croc.Options) error { relays := publicrelay.Relays() relayIndex, err := codephrase.RelayIndex(options.SharedSecret, len(relays)) if err != nil { return err } log.Debugf("code maps to public relay index %d", relayIndex) return assignPublicRelay(options, relayIndex) } func selectBestPublicRelay(ctx context.Context, probe publicrelay.Probe) (int, error) { relays := publicrelay.Relays() best, duration, err := publicrelay.SelectFirst( ctx, relays, publicrelay.ProbeTimeout, probe, ) if err == nil { log.Debugf("public relay %d (%s) won probe race in %s", best, relays[best], duration) } return best, err } func loadBestPublicRelay(relays []string) (int, error) { cacheFile, err := getBestRelayCacheFile(false) if err != nil { return 0, err } contents, err := os.ReadFile(cacheFile) if err != nil { return 0, err } address := string(contents) for index, relay := range relays { if address == relay { log.Debugf("using cached public relay %d (%s) from %s", index, address, cacheFile) return index, nil } } return 0, fmt.Errorf("cached public relay %q is not in the configured pool", address) } func saveBestPublicRelay(address string) error { cacheFile, err := getBestRelayCacheFile(true) if err != nil { return err } if err = writePrivateConfigFile(cacheFile, []byte(address)); err != nil { return err } log.Debugf("cached public relay %s in %s", address, cacheFile) return nil } func clearBestPublicRelay() { cacheFile, err := getBestRelayCacheFile(false) if err != nil { log.Debugf("could not locate public relay cache: %v", err) return } if err = os.Remove(cacheFile); err != nil && !os.IsNotExist(err) { log.Warnf("could not clear public relay cache %s: %v", cacheFile, err) return } log.Debugf("cleared public relay cache %s", cacheFile) } func clearBestPublicRelayOnSendError(generatedPublicCode bool, err error) { if generatedPublicCode && errors.Is(err, croc.ErrRelayConnection) { clearBestPublicRelay() } } func selectPublicRelay(ctx context.Context, probe publicrelay.Probe) (int, error) { if err := ctx.Err(); err != nil { return 0, err } relays := publicrelay.Relays() if relayIndex, err := loadBestPublicRelay(relays); err == nil { return relayIndex, nil } else if !os.IsNotExist(err) { log.Debugf("ignoring invalid public relay cache: %v", err) } relayIndex, err := selectBestPublicRelay(ctx, probe) if err != nil { return 0, err } if err = ctx.Err(); err != nil { return 0, err } if err = saveBestPublicRelay(relays[relayIndex]); err != nil { log.Warnf("could not cache public relay selection: %v", err) } return relayIndex, nil } func shouldExitForUnixSendCode(goos string, codeFlagSet, classicInsecureMode bool, envSecret string) bool { return goos != "windows" && codeFlagSet && !classicInsecureMode && envSecret == "" } func applyRememberedSendOptions(c *cli.Context, options *croc.Options, remembered croc.Options) { // Update anything that isn't explicitly set. if !c.IsSet("no-local") { options.DisableLocal = remembered.DisableLocal } if !c.IsSet("ports") && len(remembered.RelayPorts) > 0 { options.RelayPorts = remembered.RelayPorts } if !c.IsSet("code") { options.SharedSecret = remembered.SharedSecret } if !c.IsSet("pass") && remembered.RelayPassword != "" { options.RelayPassword = remembered.RelayPassword } if !c.IsSet("overwrite") { options.Overwrite = remembered.Overwrite } if !c.IsSet("rename") { options.Rename = remembered.Rename } if !c.IsSet("curve") && remembered.Curve != "" { options.Curve = remembered.Curve } if !c.IsSet("local") { options.OnlyLocal = remembered.OnlyLocal } if !c.IsSet("hash") { options.HashAlgorithm = remembered.HashAlgorithm } if !c.IsSet("git") { options.GitIgnore = remembered.GitIgnore } if !c.IsSet("disable-clipboard") { options.DisableClipboard = remembered.DisableClipboard } if !c.IsSet("transport") && remembered.Transport != "" { options.Transport = remembered.Transport } if !c.IsSet("relay") && strings.HasPrefix(remembered.RelayAddress, "non-default:") { rememberedAddr := strings.TrimPrefix(remembered.RelayAddress, "non-default:") options.RelayAddress = strings.TrimSpace(rememberedAddr) } if !c.IsSet("relay6") && strings.HasPrefix(remembered.RelayAddress6, "non-default:") { rememberedAddr := strings.TrimPrefix(remembered.RelayAddress6, "non-default:") options.RelayAddress6 = strings.TrimSpace(rememberedAddr) } } const tailcatRelayFallbackWarning = "Tailcat is unavailable in this build; using the croc relay instead." func resolveSendTransport(options *croc.Options) (downgraded bool, err error) { options.Transport, downgraded, err = croc.ResolveTransportMode(string(options.Transport)) return downgraded, err } func writeTailcatRelayFallbackWarning(output io.Writer, quiet, downgraded bool) { if quiet || !downgraded { return } fmt.Fprintln(output, tailcatRelayFallbackWarning) } // parseRelayPorts splits a comma-separated --ports value, trimming whitespace // around each entry and dropping empties. This keeps "9009, 9010," working the // same as "9009,9010" instead of producing invalid port strings like " 9010". func parseRelayPorts(portsFlag string) []string { var ports []string for p := range strings.SplitSeq(portsFlag, ",") { if p = strings.TrimSpace(p); p != "" { ports = append(ports, p) } } return ports } func send(c *cli.Context) (err error) { finishVersionCheck := startTransferVersionCheck(c, getSendConfigFile(false)) defer finishVersionCheck() setDebugLevel(c) comm.Socks5Proxy = c.String("socks5") comm.HttpProxy = c.String("connect") transport, err := croc.ParseTransportMode(c.String("transport")) if err != nil { return err } stored := c.Bool("store") || (c.Command != nil && c.Command.Name == "store") if stored && transport != croc.TransportAuto { return errors.New("--transport must be auto for stored transfers") } if stored { return sendStored(c) } portParam := c.Int("port") if portParam == 0 { portParam = 9009 } transfersParam := c.Int("transfers") if transfersParam == 0 { transfersParam = 4 } excludeStrings := []string{} for v := range strings.SplitSeq(c.String("exclude"), ",") { v = strings.ToLower(strings.TrimSpace(v)) if v != "" { excludeStrings = append(excludeStrings, v) } } excludeFiles := []string{} for v := range strings.SplitSeq(c.String("exclude-file"), ",") { v = utils.NormalizeRelativePath(strings.TrimSpace(v)) if v != "" && v != "." { excludeFiles = append(excludeFiles, v) } } ports := make([]string, transfersParam+1) for i := 0; i <= transfersParam; i++ { ports[i] = strconv.Itoa(portParam + i) } 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: ports, Ask: c.Bool("ask"), NoMultiplexing: c.Bool("no-multi"), RelayPassword: determinePass(c), WebOrigin: c.String("web-url"), SendingText: c.String("text") != "", NoCompress: c.Bool("no-compress"), Overwrite: c.Bool("overwrite"), Rename: c.Bool("rename"), Curve: c.String("curve"), HashAlgorithm: c.String("hash"), ThrottleUpload: c.String("throttleUpload"), ZipFolder: c.Bool("zip"), GitIgnore: c.Bool("git"), ShowQrCode: c.Bool("qrcode"), MulticastAddress: c.String("multicast"), Exclude: excludeStrings, ExcludeFile: excludeFiles, Quiet: c.Bool("quiet"), DisableClipboard: c.Bool("disable-clipboard"), ExtendedClipboard: c.Bool("extended-clipboard"), NoUpdateCheck: c.Bool("no-update-check"), Transport: transport, } if crocOptions.RelayAddress != models.DEFAULT_RELAY { crocOptions.RelayAddress6 = "" } else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 { crocOptions.RelayAddress = "" } b, errOpen := os.ReadFile(getSendConfigFile(false)) if errOpen == nil && !c.Bool("remember") { var rememberedOptions croc.Options err = json.Unmarshal(b, &rememberedOptions) if err != nil { log.Error(err) return } applyRememberedSendOptions(c, &crocOptions, rememberedOptions) } downgradedTransport, err := resolveSendTransport(&crocOptions) if err != nil { return err } if crocOptions.OnlyLocal && crocOptions.Transport != croc.TransportAuto { return errors.New("--transport must be auto for local-only transfers") } if crocOptions.ShowQrCode && crocOptions.Transport == croc.TransportDERP { return errors.New("--transport derp cannot be combined with --qrcode") } writeTailcatRelayFallbackWarning(os.Stderr, crocOptions.Quiet, downgradedTransport) publicRelayMode := usesPublicRelay(c, crocOptions) var fnames []string stdinIsPipe := false if !c.Bool("ignore-stdin") && os.Stdin != nil { stat, statErr := os.Stdin.Stat() stdinIsPipe = statErr == nil && stat != nil && (stat.Mode()&os.ModeCharDevice) == 0 } if stdinIsPipe { fnames, err = getStdinContext(c.Context) if err != nil { return } utils.MarkFileForRemoval(fnames[0]) 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 } utils.MarkFileForRemoval(fnames[0]) 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(s) or folder]") } classicInsecureMode := utils.Exists(getClassicConfigFile(true)) envSecret := os.Getenv("CROC_SECRET") crocOptions.SharedSecret = resolveSendSharedSecret(crocOptions.SharedSecret, envSecret) if shouldExitForUnixSendCode(runtime.GOOS, c.IsSet("code"), classicInsecureMode, envSecret) { fmt.Printf(`On UNIX systems, to send with a custom code phrase, you need to set the environmental variable CROC_SECRET: CROC_SECRET=**** croc send file.txt Or you can have the code phrase automatically generated: croc send file.txt Or you can go back to the classic croc behavior by enabling classic mode: croc --classic `) os.Exit(0) } generatedPublicCode := len(crocOptions.SharedSecret) == 0 && publicRelayMode if len(crocOptions.SharedSecret) == 0 { if publicRelayMode { var relayIndex int relayIndex, err = selectPublicRelay(c.Context, tcp.MeasureServerLatencyContext) if err != nil { return err } crocOptions.SharedSecret, err = codephrase.GenerateForRelay(relayIndex, len(publicrelay.Relays())) if err == nil { err = assignPublicRelay(&crocOptions, relayIndex) } } else { crocOptions.SharedSecret, err = codephrase.Generate() } if err != nil { return fmt.Errorf("could not generate code phrase: %w", err) } } else if publicRelayMode { if err = assignPublicRelayForCode(&crocOptions); err != nil { return fmt.Errorf("could not select public relay: %w", err) } } minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfoWithExactExclusionsContext(c.Context, fnames, crocOptions.ZipFolder, crocOptions.GitIgnore, crocOptions.Exclude, crocOptions.ExcludeFile) if err != nil { return } if len(crocOptions.Exclude) > 0 { minimalFileInfosInclude := []croc.FileInfo{} emptyFoldersToTransferInclude := []croc.FileInfo{} for _, f := range minimalFileInfos { if err = c.Context.Err(); err != nil { return err } exclude := false for _, exclusion := range crocOptions.Exclude { if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) { exclude = true break } } if !exclude { minimalFileInfosInclude = append(minimalFileInfosInclude, f) } } for _, f := range emptyFoldersToTransfer { if err = c.Context.Err(); err != nil { return err } exclude := false for _, exclusion := range crocOptions.Exclude { if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) { exclude = true break } } if !exclude { emptyFoldersToTransferInclude = append(emptyFoldersToTransferInclude, f) } } totalNumberFolders = 0 folderMap := make(map[string]bool) for _, f := range minimalFileInfosInclude { folderMap[f.FolderRemote] = true } for _, f := range emptyFoldersToTransferInclude { folderMap[f.FolderRemote] = true } totalNumberFolders = len(folderMap) minimalFileInfos = minimalFileInfosInclude emptyFoldersToTransfer = emptyFoldersToTransferInclude } cr, err := croc.NewCtx(c.Context, crocOptions) if err != nil { return } // save the config saveConfig(c, crocOptions) err = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders) clearBestPublicRelayOnSendError(generatedPublicCode, err) return } func getStdin() (fnames []string, err error) { return getStdinContext(context.Background()) } func getStdinContext(ctx context.Context) (fnames []string, err error) { return copyStdinContext(ctx, os.Stdin) } func copyStdinContext(ctx context.Context, input io.Reader) (fnames []string, err error) { if ctx == nil { ctx = context.Background() } f, err := os.CreateTemp(".", "croc-stdin-") if err != nil { return } name := f.Name() copyDone := make(chan error, 1) go func() { _, copyErr := io.Copy(f, input) copyDone <- copyErr }() select { case <-ctx.Done(): _ = f.Close() _ = os.Remove(name) return nil, ctx.Err() case err = <-copyDone: if err != nil { _ = f.Close() _ = os.Remove(name) return nil, err } } if err = f.Close(); err != nil { _ = os.Remove(name) return nil, err } fnames = []string{name} return } func makeTempFileWithString(s string) (fnames []string, err error) { f, err := os.CreateTemp(".", "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 } // writePrivateConfigFile writes configuration only after enforcing owner-only // permissions. os.WriteFile's permission argument applies only to newly created // files, so it does not harden configs created by older croc versions. func writePrivateConfigFile(name string, data []byte) (err error) { f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { return err } defer func() { if closeErr := f.Close(); err == nil { err = closeErr } }() if err = f.Chmod(0o600); err != nil { return err } if err = f.Truncate(0); err != nil { return err } _, err = f.Write(data) return err } func saveConfig(c *cli.Context, crocOptions croc.Options) { if c.Bool("remember") { configFile := getSendConfigFile(true) log.Debug("saving config file") var bConfig []byte // if the code wasn't set, don't save it if c.String("code") == "" { crocOptions.SharedSecret = "" } if c.String("relay") != models.DEFAULT_RELAY { crocOptions.RelayAddress = "non-default: " + c.String("relay") } else { crocOptions.RelayAddress = "default" } if c.String("relay6") != models.DEFAULT_RELAY6 { crocOptions.RelayAddress6 = "non-default: " + c.String("relay6") } else { crocOptions.RelayAddress6 = "default" } bConfig, err := json.MarshalIndent(crocOptions, "", " ") if err != nil { log.Error(err) return } err = writePrivateConfigFile(configFile, bConfig) if err != nil { log.Error(err) return } log.Debugf("wrote %s", configFile) } } func receive(c *cli.Context) (err error) { rememberedConfigFile, _ := getReceiveConfigFile(false) finishVersionCheck := startTransferVersionCheck(c, rememberedConfigFile) defer finishVersionCheck() comm.Socks5Proxy = c.String("socks5") comm.HttpProxy = c.String("connect") if storedToken := strings.TrimSpace(os.Getenv("CROC_STORE_TOKEN")); storedToken != "" { setDebugLevel(c) return receiveStored(c, storedToken) } 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"), Rename: c.Bool("rename"), Curve: c.String("curve"), TestFlag: c.Bool("testing"), MulticastAddress: c.String("multicast"), Quiet: c.Bool("quiet"), DisableClipboard: c.Bool("disable-clipboard"), ExtendedClipboard: c.Bool("extended-clipboard"), NoUpdateCheck: c.Bool("no-update-check"), Transport: croc.TransportAuto, } 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: fallthrough case 4: var phrase []string phrase = append(phrase, c.Args().First()) phrase = append(phrase, c.Args().Tail()...) crocOptions.SharedSecret = strings.Join(phrase, "-") } if storeclient.IsStoredValue(crocOptions.SharedSecret) { setDebugLevel(c) if runtime.GOOS != "windows" && !utils.Exists(getClassicConfigFile(true)) { fmt.Print(`For security, stored-transfer links are not accepted as command-line arguments on UNIX systems because their decryption key would be visible in the process list. Run croc with no argument and paste the link at the prompt, or use: CROC_STORE_TOKEN='croc-store-v1....' croc `) return nil } return receiveStored(c, crocOptions.SharedSecret) } // load options here setDebugLevel(c) doRemember := c.Bool("remember") configFile, err := getReceiveConfigFile(doRemember) if err != nil && doRemember { return } b, errOpen := os.ReadFile(configFile) if errOpen == nil && !doRemember { var rememberedOptions croc.Options err = json.Unmarshal(b, &rememberedOptions) if err != nil { log.Error(err) return } // update anything that isn't explicitly Globally set if !c.IsSet("yes") { crocOptions.NoPrompt = rememberedOptions.NoPrompt } if crocOptions.SharedSecret == "" { crocOptions.SharedSecret = rememberedOptions.SharedSecret } if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" { crocOptions.RelayPassword = rememberedOptions.RelayPassword } if !c.IsSet("overwrite") { crocOptions.Overwrite = rememberedOptions.Overwrite } if !c.IsSet("rename") { crocOptions.Rename = rememberedOptions.Rename } if !c.IsSet("curve") && rememberedOptions.Curve != "" { crocOptions.Curve = rememberedOptions.Curve } if !c.IsSet("local") { crocOptions.OnlyLocal = rememberedOptions.OnlyLocal } if !c.IsSet("relay") && strings.HasPrefix(rememberedOptions.RelayAddress, "non-default:") { var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, "non-default:") rememberedAddr = strings.TrimSpace(rememberedAddr) crocOptions.RelayAddress = rememberedAddr } if !c.IsSet("relay6") && strings.HasPrefix(rememberedOptions.RelayAddress6, "non-default:") { var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, "non-default:") rememberedAddr = strings.TrimSpace(rememberedAddr) crocOptions.RelayAddress6 = rememberedAddr } } publicRelayMode := usesPublicRelay(c, crocOptions) classicInsecureMode := utils.Exists(getClassicConfigFile(true)) if crocOptions.SharedSecret == "" && os.Getenv("CROC_SECRET") != "" { crocOptions.SharedSecret = os.Getenv("CROC_SECRET") } else if !(runtime.GOOS == "windows") && crocOptions.SharedSecret != "" && !classicInsecureMode { commandLineSecret := crocOptions.SharedSecret crocOptions.SharedSecret = os.Getenv("CROC_SECRET") if crocOptions.SharedSecret == "" { output, colorEnabled := termui.Output(os.Stdout) fmt.Fprint(output, formatUnixReceiveCodeMessage(commandLineSecret, colorEnabled)) return nil } } if crocOptions.SharedSecret == "" { crocOptions.SharedSecret, err = utils.GetInputContext(c.Context, "Enter receive code: ") if err != nil { return fmt.Errorf("could not read receive code: %w", err) } } if storeclient.IsStoredValue(crocOptions.SharedSecret) { return receiveStored(c, crocOptions.SharedSecret) } if publicRelayMode { if err = assignPublicRelayForCode(&crocOptions); err != nil { return fmt.Errorf("could not select public relay: %w", err) } } if c.String("out") != "" { if err = os.Chdir(c.String("out")); err != nil { return err } } cr, err := croc.NewCtx(c.Context, crocOptions) if err != nil { return } // save the config if doRemember { log.Debug("saving config file") var bConfig []byte if c.String("relay") != models.DEFAULT_RELAY { crocOptions.RelayAddress = "non-default: " + c.String("relay") } else { crocOptions.RelayAddress = "default" } if c.String("relay6") != models.DEFAULT_RELAY6 { crocOptions.RelayAddress6 = "non-default: " + c.String("relay6") } else { crocOptions.RelayAddress6 = "default" } bConfig, err = json.MarshalIndent(crocOptions, "", " ") if err != nil { log.Error(err) return } err = writePrivateConfigFile(configFile, bConfig) 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) maxRoomsOpen := c.Int("max-rooms-open") if maxRoomsOpen <= 0 { return fmt.Errorf("--max-rooms-open must be positive") } maxPendingHandshakes := c.Int("max-pending-handshakes") if maxPendingHandshakes <= 0 { return fmt.Errorf("--max-pending-handshakes must be positive") } maxPendingHandshakesPerSource := c.Int("max-pending-handshakes-per-source") if maxPendingHandshakesPerSource <= 0 { return fmt.Errorf("--max-pending-handshakes-per-source must be positive") } firstMessageTimeout := c.Duration("first-message-timeout") if firstMessageTimeout <= 0 { return fmt.Errorf("--first-message-timeout must be positive") } handshakeTimeout := c.Duration("handshake-timeout") if handshakeTimeout <= 0 { return fmt.Errorf("--handshake-timeout must be positive") } sourceJoinLimit := c.Int("source-join-limit") if sourceJoinLimit <= 0 { return fmt.Errorf("--source-join-limit must be positive") } roomJoinLimit := c.Int("room-join-limit") if roomJoinLimit <= 0 { return fmt.Errorf("--room-join-limit must be positive") } joinLimitWindow := c.Duration("join-limit-window") if joinLimitWindow <= 0 { return fmt.Errorf("--join-limit-window must be positive") } debugString := "info" if c.Bool("debug") { debugString = "debug" } host := c.String("host") var ports []string if c.IsSet("ports") { ports = parseRelayPorts(c.String("ports")) } else { portString := c.Int("port") if portString == 0 { portString = 9009 } transfersString := c.Int("transfers") if transfersString == 0 { transfersString = 4 } ports = make([]string, transfersString) for i := range ports { ports[i] = strconv.Itoa(portString + i) } } if len(ports) < 2 { return fmt.Errorf("relay requires at least two ports; specify --ports with two or more ports or set --transfers to 2+") } var roomPaired func() var roomProtocol func(tcp.RoomProtocol) umamiURL := strings.TrimSpace(os.Getenv("UMAMI_URL")) umamiWebsiteID := strings.TrimSpace(os.Getenv("UMAMI_WEBSITE_ID")) umamiSrc := strings.TrimSpace(os.Getenv("UMAMI_SRC")) siteURL := strings.TrimSpace(os.Getenv("SITE_URL")) if umamiURL != "" && umamiWebsiteID != "" && siteURL != "" { reporter, reporterErr := publicrelay.NewUmamiReporter(umamiURL, umamiWebsiteID, siteURL, Version) if reporterErr != nil { log.Warnf("relay analytics disabled: %v", reporterErr) } else { defer reporter.Close() eventName := func(event string) string { if umamiSrc != "" { return event + "-" + umamiSrc } return event } roomPaired = func() { reporter.Track(eventName("relay-session")) } roomProtocol = func(protocol tcp.RoomProtocol) { if protocol == tcp.RoomProtocolSSH { reporter.Track(eventName("ssh-rendezvous")) } } } } tcpPorts := strings.Join(ports[1:], ",") capabilitySet, capabilityErr := tcp.NewRelayCapabilitySet(min(len(ports)-1, 8)) if capabilityErr != nil { return capabilityErr } for i, port := range ports { if i == 0 { continue } go func(portStr string) { err := tcp.RunWithOptionsAsync( host, portStr, determinePass(c), tcp.WithLogLevel(debugString), tcp.WithMaxRoomsOpen(maxRoomsOpen), tcp.WithMaxPendingHandshakes(maxPendingHandshakes), tcp.WithMaxPendingHandshakesPerSource(maxPendingHandshakesPerSource), tcp.WithFirstMessageTimeout(firstMessageTimeout), tcp.WithHandshakeTimeout(handshakeTimeout), tcp.WithAdmissionLimits(sourceJoinLimit, roomJoinLimit, joinLimitWindow), tcp.WithFastAdmission(capabilitySet), tcp.WithCtx(c.Context), ) if err != nil { panic(err) } }(port) } return tcp.RunWithOptionsAsync( host, ports[0], determinePass(c), tcp.WithBanner(tcpPorts), tcp.WithLogLevel(debugString), tcp.WithMaxRoomsOpen(maxRoomsOpen), tcp.WithMaxPendingHandshakes(maxPendingHandshakes), tcp.WithMaxPendingHandshakesPerSource(maxPendingHandshakesPerSource), tcp.WithFirstMessageTimeout(firstMessageTimeout), tcp.WithHandshakeTimeout(handshakeTimeout), tcp.WithAdmissionLimits(sourceJoinLimit, roomJoinLimit, joinLimitWindow), tcp.WithFastAdmission(capabilitySet), tcp.WithCtx(c.Context), tcp.WithRoomPairedCallback(roomPaired), tcp.WithRoomProtocolCallback(roomProtocol), ) }