package croc import ( "bytes" "crypto/rand" "encoding/binary" "encoding/json" "fmt" "io" "io/ioutil" "math" "net" "os" "path" "path/filepath" "strconv" "strings" "sync" "time" "github.com/denisbrodbeck/machineid" log "github.com/schollz/logger" "github.com/schollz/pake/v3" "github.com/schollz/peerdiscovery" "github.com/schollz/progressbar/v3" "github.com/schollz/croc/v9/src/comm" "github.com/schollz/croc/v9/src/compress" "github.com/schollz/croc/v9/src/crypt" "github.com/schollz/croc/v9/src/message" "github.com/schollz/croc/v9/src/models" "github.com/schollz/croc/v9/src/tcp" "github.com/schollz/croc/v9/src/utils" ) func init() { log.SetLevel("debug") } // Debug toggles debug mode func Debug(debug bool) { if debug { log.SetLevel("debug") } else { log.SetLevel("warn") } } // Options specifies user specific options type Options struct { IsSender bool SharedSecret string Debug bool RelayAddress string RelayAddress6 string RelayPorts []string RelayPassword string RelayKeyPrivate string RelayKeyPublic string LocalRelayPassword string LocalRelayKeyPrivate string LocalRelayKeyPublic string Stdout bool NoPrompt bool NoMultiplexing bool DisableLocal bool OnlyLocal bool IgnoreStdin bool Ask bool SendingText bool NoCompress bool IP string Overwrite bool Curve string HashAlgorithm string } // Client holds the state of the croc transfer type Client struct { Options Options Pake *pake.Pake Key []byte ExternalIP, ExternalIPConnected string // steps involved in forming relationship Step1ChannelSecured bool Step2FileInfoTransfered bool Step3RecipientRequestFile bool Step4FileTransfer bool Step5CloseChannels bool SuccessfulTransfer bool // send / receive information of all files FilesToTransfer []FileInfo FilesToTransferCurrentNum int FilesHasFinished map[int]struct{} // send / receive information of current file CurrentFile *os.File CurrentFileChunkRanges []int64 CurrentFileChunks []int64 CurrentFileIsClosed bool LastFolder string TotalSent int64 TotalChunksTransfered int chunkMap map[uint64]struct{} // tcp connections conn []*comm.Comm bar *progressbar.ProgressBar longestFilename int firstSend bool mutex *sync.Mutex fread *os.File numfinished int quit chan bool finishedNum int numberOfTransferedFiles int } // Chunk contains information about the // needed bytes type Chunk struct { Bytes []byte `json:"b,omitempty"` Location int64 `json:"l,omitempty"` } // FileInfo registers the information about the file type FileInfo struct { Name string `json:"n,omitempty"` FolderRemote string `json:"fr,omitempty"` FolderSource string `json:"fs,omitempty"` Hash []byte `json:"h,omitempty"` Size int64 `json:"s,omitempty"` ModTime time.Time `json:"m,omitempty"` IsCompressed bool `json:"c,omitempty"` IsEncrypted bool `json:"e,omitempty"` Symlink string `json:"sy,omitempty"` } // RemoteFileRequest requests specific bytes type RemoteFileRequest struct { CurrentFileChunkRanges []int64 FilesToTransferCurrentNum int MachineID string } // SenderInfo lists the files to be transferred type SenderInfo struct { FilesToTransfer []FileInfo MachineID string Ask bool SendingText bool NoCompress bool HashAlgorithm string } // New establishes a new connection for transferring files between two instances. func New(ops Options) (c *Client, err error) { c = new(Client) c.FilesHasFinished = make(map[int]struct{}) // setup basic info c.Options = ops Debug(c.Options.Debug) log.Debugf("options: %+v", c.Options) if len(c.Options.SharedSecret) < 6 { err = fmt.Errorf("code is too short") return } c.conn = make([]*comm.Comm, 16) // initialize pake for recipient if !c.Options.IsSender { c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve) } if err != nil { return } c.mutex = &sync.Mutex{} return } // TransferOptions for sending type TransferOptions struct { PathToFiles []string KeepPathInRemote bool } func (c *Client) sendCollectFiles(options TransferOptions) (err error) { c.FilesToTransfer = make([]FileInfo, len(options.PathToFiles)) totalFilesSize := int64(0) for i, pathToFile := range options.PathToFiles { var fstats os.FileInfo var fullPath string fullPath, err = filepath.Abs(pathToFile) if err != nil { return } fullPath = filepath.Clean(fullPath) var folderName string folderName, _ = filepath.Split(fullPath) fstats, err = os.Lstat(fullPath) if err != nil { return } if len(fstats.Name()) > c.longestFilename { c.longestFilename = len(fstats.Name()) } c.FilesToTransfer[i] = FileInfo{ Name: fstats.Name(), FolderRemote: ".", FolderSource: folderName, Size: fstats.Size(), ModTime: fstats.ModTime(), } if fstats.Mode()&os.ModeSymlink != 0 { log.Debugf("%s is symlink", fstats.Name()) c.FilesToTransfer[i].Symlink, err = os.Readlink(pathToFile) if err != nil { log.Debugf("error getting symlink: %s", err.Error()) } log.Debugf("%+v", c.FilesToTransfer[i]) } if c.Options.HashAlgorithm == "" { c.Options.HashAlgorithm = "xxhash" } c.FilesToTransfer[i].Hash, err = utils.HashFile(fullPath, c.Options.HashAlgorithm) log.Debugf("hashed %s to %x using %s", fullPath, c.FilesToTransfer[i].Hash, c.Options.HashAlgorithm) totalFilesSize += fstats.Size() if err != nil { return } if options.KeepPathInRemote { var curFolder string curFolder, err = os.Getwd() if err != nil { return } curFolder, err = filepath.Abs(curFolder) if err != nil { return } if !strings.HasPrefix(folderName, curFolder) { err = fmt.Errorf("remote directory must be relative to current") return } c.FilesToTransfer[i].FolderRemote = strings.TrimPrefix(folderName, curFolder) c.FilesToTransfer[i].FolderRemote = filepath.ToSlash(c.FilesToTransfer[i].FolderRemote) c.FilesToTransfer[i].FolderRemote = strings.TrimPrefix(c.FilesToTransfer[i].FolderRemote, "/") if c.FilesToTransfer[i].FolderRemote == "" { c.FilesToTransfer[i].FolderRemote = "." } } log.Debugf("file %d info: %+v", i, c.FilesToTransfer[i]) } log.Debugf("longestFilename: %+v", c.longestFilename) fname := fmt.Sprintf("%d files", len(c.FilesToTransfer)) if len(c.FilesToTransfer) == 1 { fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name) } if strings.HasPrefix(fname, "'croc-stdin-") { fname = "'stdin'" if c.Options.SendingText { fname = "'text'" } } fmt.Fprintf(os.Stderr, "Sending %s (%s)\n", fname, utils.ByteCountDecimal(totalFilesSize)) return } func (c *Client) setupLocalRelay() { // setup the relay locally firstPort, _ := strconv.Atoi(c.Options.RelayPorts[0]) openPorts := utils.FindOpenPorts("localhost", firstPort, len(c.Options.RelayPorts)) if len(openPorts) < len(c.Options.RelayPorts) { panic("not enough open ports to run local relay") } for i, port := range openPorts { c.Options.RelayPorts[i] = fmt.Sprint(port) } for _, port := range c.Options.RelayPorts { go func(portStr string) { debugString := "warn" if c.Options.Debug { debugString = "debug" } err := tcp.Run(debugString, portStr, c.Options.LocalRelayPassword, c.Options.LocalRelayKeyPublic, c.Options.LocalRelayKeyPrivate, strings.Join(c.Options.RelayPorts[1:], ",")) if err != nil { panic(err) } }(port) } } func (c *Client) broadcastOnLocalNetwork(useipv6 bool) { // look for peers first settings := peerdiscovery.Settings{ Limit: -1, Payload: []byte("croc" + c.Options.RelayPorts[0]), Delay: 20 * time.Millisecond, TimeLimit: 30 * time.Second, } if useipv6 { settings.IPVersion = peerdiscovery.IPv6 } discoveries, err := peerdiscovery.Discover(settings) log.Debugf("discoveries: %+v", discoveries) if err != nil { log.Debug(err) } } func (c *Client) transferOverLocalRelay(options TransferOptions, errchan chan<- error) { time.Sleep(500 * time.Millisecond) log.Debug("establishing connection") var banner string conn, banner, ipaddr, err := tcp.ConnectToTCPServer("localhost:"+c.Options.RelayPorts[0], c.Options.LocalRelayPassword, c.Options.LocalRelayKeyPublic, c.Options.SharedSecret[:3]) log.Debugf("banner: %s", banner) if err != nil { err = fmt.Errorf("could not connect to localhost:%s: %w", c.Options.RelayPorts[0], err) log.Debug(err) // not really an error because it will try to connect over the actual relay return } log.Debugf("local connection established: %+v", conn) for { data, _ := conn.Receive() if bytes.Equal(data, []byte("handshake")) { break } else if bytes.Equal(data, []byte{1}) { log.Debug("got ping") } else { log.Debugf("instead of handshake got: %s", data) } } c.conn[0] = conn log.Debug("exchanged header message") c.Options.RelayAddress = "localhost" c.Options.RelayPorts = strings.Split(banner, ",") if c.Options.NoMultiplexing { log.Debug("no multiplexing") c.Options.RelayPorts = []string{c.Options.RelayPorts[0]} } c.ExternalIP = ipaddr errchan <- c.transfer(options) } // Send will send the specified file func (c *Client) Send(options TransferOptions) (err error) { err = c.sendCollectFiles(options) if err != nil { return } flags := &strings.Builder{} if c.Options.RelayAddress != models.DEFAULT_RELAY { flags.WriteString("--relay " + c.Options.RelayAddress + " ") } if c.Options.RelayPassword != models.DEFAULT_RELAY_PASSWORD || c.Options.RelayKeyPublic != models.DEFAULT_RELAY_KEYPUBLIC { flags.WriteString("--relay-pass " + c.Options.RelayPassword + " ") flags.WriteString("--relay-key " + c.Options.RelayKeyPublic + " ") } fmt.Fprintf(os.Stderr, "Code is: %[1]s\nOn the other computer run\n\ncroc %[2]s%[1]s\n", c.Options.SharedSecret, flags.String()) if c.Options.Ask { machid, _ := machineid.ID() fmt.Fprintf(os.Stderr, "\rYour machine ID is '%s'\n", machid) } // // c.spinner.Suffix = " waiting for recipient..." // c.spinner.Start() // create channel for quitting // connect to the relay for messaging errchan := make(chan error, 1) if !c.Options.DisableLocal { c.Options.LocalRelayKeyPublic, c.Options.LocalRelayKeyPrivate, _ = crypt.NewAge() c.Options.LocalRelayPassword, _ = crypt.GenerateRandomString(6) // add two things to the error channel errchan = make(chan error, 2) c.setupLocalRelay() // broadcast on ipv4 go c.broadcastOnLocalNetwork(false) // broadcast on ipv6 go c.broadcastOnLocalNetwork(true) go c.transferOverLocalRelay(options, errchan) } if !c.Options.OnlyLocal { go func() { var ipaddr, banner string var conn *comm.Comm durations := []time.Duration{100 * time.Millisecond, 5 * time.Second} for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} { if address == "" { continue } host, port, _ := net.SplitHostPort(address) log.Debugf("host: '%s', port: '%s'", host, port) // Default port to :9009 if port == "" { host = address port = "9009" } log.Debugf("got host '%v' and port '%v'", host, port) address = net.JoinHostPort(host, port) log.Debugf("trying connection to %s", address) conn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], durations[i]) if err == nil { c.Options.RelayAddress = address break } log.Debugf("could not establish '%s'", address) } if conn == nil && err == nil { err = fmt.Errorf("could not connect") } if err != nil { err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err) log.Debug(err) errchan <- err return } log.Debugf("banner: %s", banner) log.Debugf("connection established: %+v", conn) for { log.Debug("waiting for bytes") data, errConn := conn.Receive() if errConn != nil { log.Debugf("[%+v] had error: %s", conn, errConn.Error()) } if bytes.Equal(data, []byte("ips?")) { // recipient wants to try to connect to local ips var ips []string // only get local ips if the local is enabled if !c.Options.DisableLocal { // get list of local ips ips, err = utils.GetLocalIPs() if err != nil { log.Debugf("error getting local ips: %v", err) } // prepend the port that is being listened to ips = append([]string{c.Options.RelayPorts[0]}, ips...) } bips, _ := json.Marshal(ips) if err := conn.Send(bips); err != nil { log.Errorf("error sending: %v", err) } } else if bytes.Equal(data, []byte("handshake")) { break } else if bytes.Equal(data, []byte{1}) { log.Debug("got ping") continue } else { log.Debugf("[%+v] got weird bytes: %+v", conn, data) // throttle the reading errchan <- fmt.Errorf("gracefully refusing using the public relay") return } } c.conn[0] = conn c.Options.RelayPorts = strings.Split(banner, ",") if c.Options.NoMultiplexing { log.Debug("no multiplexing") c.Options.RelayPorts = []string{c.Options.RelayPorts[0]} } c.ExternalIP = ipaddr log.Debug("exchanged header message") errchan <- c.transfer(options) }() } err = <-errchan if err == nil { // return if no error return } else { log.Debugf("error from errchan: %v", err) if strings.Contains(err.Error(), "could not secure channel") { return err } } if !c.Options.DisableLocal { if strings.Contains(err.Error(), "refusing files") || strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "bad password") { errchan <- err } err = <-errchan } return err } // Receive will receive a file func (c *Client) Receive() (err error) { fmt.Fprintf(os.Stderr, "connecting...") // recipient will look for peers first // and continue if it doesn't find any within 100 ms usingLocal := false isIPset := false if c.Options.OnlyLocal || c.Options.IP != "" { c.Options.RelayAddress = "" c.Options.RelayAddress6 = "" } if c.Options.IP != "" { // check ip version if strings.Count(c.Options.IP, ":") >= 2 { log.Debug("assume ipv6") c.Options.RelayAddress6 = c.Options.IP } if strings.Contains(c.Options.IP, ".") { log.Debug("assume ipv4") c.Options.RelayAddress = c.Options.IP } isIPset = true } if !c.Options.DisableLocal && !isIPset { log.Debug("attempt to discover peers") var discoveries []peerdiscovery.Discovered var wgDiscovery sync.WaitGroup var dmux sync.Mutex wgDiscovery.Add(2) go func() { defer wgDiscovery.Done() ipv4discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{ Limit: 1, Payload: []byte("ok"), Delay: 20 * time.Millisecond, TimeLimit: 200 * time.Millisecond, }) if err1 == nil && len(ipv4discoveries) > 0 { dmux.Lock() err = err1 discoveries = append(discoveries, ipv4discoveries...) dmux.Unlock() } }() go func() { defer wgDiscovery.Done() ipv6discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{ Limit: 1, Payload: []byte("ok"), Delay: 20 * time.Millisecond, TimeLimit: 200 * time.Millisecond, IPVersion: peerdiscovery.IPv6, }) if err1 == nil && len(ipv6discoveries) > 0 { dmux.Lock() err = err1 discoveries = append(discoveries, ipv6discoveries...) dmux.Unlock() } }() wgDiscovery.Wait() if err == nil && len(discoveries) > 0 { log.Debugf("all discoveries: %+v", discoveries) for i := 0; i < len(discoveries); i++ { log.Debugf("discovery %d has payload: %+v", i, discoveries[i]) if !bytes.HasPrefix(discoveries[i].Payload, []byte("croc")) { log.Debug("skipping discovery") continue } log.Debug("switching to local") portToUse := string(bytes.TrimPrefix(discoveries[0].Payload, []byte("croc"))) if portToUse == "" { portToUse = "9009" } address := net.JoinHostPort(discoveries[0].Address, portToUse) if tcp.PingServer(address) == nil { log.Debugf("succesfully pinged '%s'", address) c.Options.RelayAddress = address c.ExternalIPConnected = c.Options.RelayAddress c.Options.RelayAddress6 = "" usingLocal = true break } } } log.Debugf("discoveries: %+v", discoveries) log.Debug("establishing connection") } var banner string durations := []time.Duration{100 * time.Millisecond, 5 * time.Second} err = fmt.Errorf("found no addresses to connect") for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} { if address == "" { continue } var host, port string host, port, _ = net.SplitHostPort(address) // Default port to :9009 if port == "" { host = address port = "9009" } log.Debugf("got host '%v' and port '%v'", host, port) address = net.JoinHostPort(host, port) log.Debugf("trying connection to %s", address) c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], durations[i]) if err == nil { c.Options.RelayAddress = address break } log.Debugf("could not establish '%s'", address) } if err != nil { err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err) log.Debug(err) return } log.Debugf("receiver connection established: %+v", c.conn[0]) log.Debugf("banner: %s", banner) if !usingLocal && !c.Options.DisableLocal && !isIPset { // ask the sender for their local ips and port // and try to connect to them log.Debug("sending ips?") var data []byte if err := c.conn[0].Send([]byte("ips?")); err != nil { log.Errorf("ips send error: %v", err) } data, err = c.conn[0].Receive() if err != nil { return } log.Debugf("ips data: %s", data) var ips []string if err := json.Unmarshal(data, &ips); err != nil { log.Debugf("ips unmarshal error: %v", err) } if len(ips) > 1 { port := ips[0] ips = ips[1:] for _, ip := range ips { ipv4Addr, ipv4Net, errNet := net.ParseCIDR(fmt.Sprintf("%s/24", ip)) log.Debugf("ipv4Add4: %+v, ipv4Net: %+v, err: %+v", ipv4Addr, ipv4Net, errNet) localIps, _ := utils.GetLocalIPs() haveLocalIP := false for _, localIP := range localIps { localIPparsed := net.ParseIP(localIP) if ipv4Net.Contains(localIPparsed) { haveLocalIP = true break } } if !haveLocalIP { log.Debugf("%s is not a local IP, skipping", ip) continue } serverTry := fmt.Sprintf("%s:%s", ip, port) conn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], 50*time.Millisecond) if errConn != nil { log.Debugf("could not connect to " + serverTry) continue } log.Debugf("local connection established to %s", serverTry) log.Debugf("banner: %s", banner2) // reset to the local port banner = banner2 c.Options.RelayAddress = serverTry c.ExternalIP = externalIP c.conn[0].Close() c.conn[0] = nil c.conn[0] = conn break } } } if err := c.conn[0].Send([]byte("handshake")); err != nil { log.Errorf("handshake send error: %v", err) } c.Options.RelayPorts = strings.Split(banner, ",") if c.Options.NoMultiplexing { log.Debug("no multiplexing") c.Options.RelayPorts = []string{c.Options.RelayPorts[0]} } log.Debug("exchanged header message") fmt.Fprintf(os.Stderr, "\rsecuring channel...") err = c.transfer(TransferOptions{}) if err == nil { if c.numberOfTransferedFiles == 0 { fmt.Fprintf(os.Stderr, "\rNo files transferred.") } } return } func (c *Client) transfer(options TransferOptions) (err error) { // connect to the server // quit with c.quit <- true c.quit = make(chan bool) // if recipient, initialize with sending pake information log.Debug("ready") if !c.Options.IsSender && !c.Step1ChannelSecured { err = message.Send(c.conn[0], c.Key, message.Message{ Type: "pake", Bytes: c.Pake.Bytes(), Bytes2: []byte(c.Options.Curve), }) if err != nil { return } } // listen for incoming messages and process them for { var data []byte var done bool data, err = c.conn[0].Receive() if err != nil { log.Debugf("got error receiving: %v", err) if !c.Step1ChannelSecured { err = fmt.Errorf("could not secure channel") } break } done, err = c.processMessage(data) if err != nil { log.Debugf("got error processing: %v", err) break } if done { break } } // purge errors that come from successful transfer if c.SuccessfulTransfer { if err != nil { log.Debugf("purging error: %s", err) } err = nil } if c.Options.Stdout && !c.Options.IsSender { pathToFile := path.Join( c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name, ) log.Debugf("pathToFile: %s", pathToFile) // close if not closed already if !c.CurrentFileIsClosed { c.CurrentFile.Close() c.CurrentFileIsClosed = true } if err := os.Remove(pathToFile); err != nil { log.Warnf("error removing %s: %v", pathToFile, err) } fmt.Print("\n") } if err != nil && strings.Contains(err.Error(), "pake not successful") { log.Debugf("pake error: %s", err.Error()) err = fmt.Errorf("password mismatch") } if err != nil && strings.Contains(err.Error(), "unexpected end of JSON input") { log.Debugf("error: %s", err.Error()) err = fmt.Errorf("room not ready") } return } func (c *Client) processMessageFileInfo(m message.Message) (done bool, err error) { var senderInfo SenderInfo err = json.Unmarshal(m.Bytes, &senderInfo) if err != nil { log.Debug(err) return } c.Options.SendingText = senderInfo.SendingText c.Options.NoCompress = senderInfo.NoCompress c.Options.HashAlgorithm = senderInfo.HashAlgorithm if c.Options.HashAlgorithm == "" { c.Options.HashAlgorithm = "imohash" } log.Debugf("using hash algorithm: %s", c.Options.HashAlgorithm) if c.Options.NoCompress { log.Debug("disabling compression") } if c.Options.SendingText { c.Options.Stdout = true } c.FilesToTransfer = senderInfo.FilesToTransfer fname := fmt.Sprintf("%d files", len(c.FilesToTransfer)) if len(c.FilesToTransfer) == 1 { fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name) } totalSize := int64(0) for i, fi := range c.FilesToTransfer { totalSize += fi.Size if len(fi.Name) > c.longestFilename { c.longestFilename = len(fi.Name) } if strings.HasPrefix(fi.Name, "croc-stdin-") { c.FilesToTransfer[i].Name, err = utils.RandomFileName() if err != nil { return } } } // c.spinner.Stop() if strings.HasPrefix(fname, "'croc-stdin") { fname = "'stdin'" if c.Options.SendingText { fname = "'text'" } } if !c.Options.NoPrompt || c.Options.Ask || senderInfo.Ask { if c.Options.Ask || senderInfo.Ask { machID, _ := machineid.ID() fmt.Fprintf(os.Stderr, "\rYour machine id is '%s'.\nAccept %s (%s) from '%s'? (y/n) ", machID, fname, utils.ByteCountDecimal(totalSize), senderInfo.MachineID) } else { fmt.Fprintf(os.Stderr, "\rAccept %s (%s)? (y/n) ", fname, utils.ByteCountDecimal(totalSize)) } if strings.ToLower(strings.TrimSpace(utils.GetInput(""))) != "y" { err = message.Send(c.conn[0], c.Key, message.Message{ Type: "error", Message: "refusing files", }) if err != nil { return false, err } return true, fmt.Errorf("refused files") } } else { fmt.Fprintf(os.Stderr, "\rReceiving %s (%s) \n", fname, utils.ByteCountDecimal(totalSize)) } fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)\n", c.ExternalIPConnected) log.Debug(c.FilesToTransfer) c.Step2FileInfoTransfered = true return } func (c *Client) processMessagePake(m message.Message) (err error) { log.Debug("received pake payload") var salt []byte if c.Options.IsSender { // initialize curve based on the recipient's choice log.Debugf("using curve %s", string(m.Bytes2)) c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, string(m.Bytes2)) if err != nil { log.Error(err) return } // update the pake err = c.Pake.Update(m.Bytes) if err != nil { return } // generate salt and send it back to recipient log.Debug("generating salt") salt = make([]byte, 8) if _, rerr := rand.Read(salt); err != nil { log.Errorf("can't generate random numbers: %v", rerr) return } log.Debug("sender sending pake+salt") err = message.Send(c.conn[0], c.Key, message.Message{ Type: "pake", Bytes: c.Pake.Bytes(), Bytes2: salt, }) } else { err = c.Pake.Update(m.Bytes) if err != nil { return } salt = m.Bytes2 } // generate key key, err := c.Pake.SessionKey() if err != nil { return err } c.Key, _, err = crypt.New(key, salt) if err != nil { return err } log.Debugf("generated key = %+x with salt %x", c.Key, salt) // connects to the other ports of the server for transfer var wg sync.WaitGroup wg.Add(len(c.Options.RelayPorts)) for i := 0; i < len(c.Options.RelayPorts); i++ { log.Debugf("port: [%s]", c.Options.RelayPorts[i]) go func(j int) { defer wg.Done() var host string if c.Options.RelayAddress == "localhost" { host = c.Options.RelayAddress } else { host, _, err = net.SplitHostPort(c.Options.RelayAddress) if err != nil { log.Errorf("bad relay address %s", c.Options.RelayAddress) return } } server := net.JoinHostPort(host, c.Options.RelayPorts[j]) log.Debugf("connecting to %s", server) c.conn[j+1], _, _, err = tcp.ConnectToTCPServer( server, c.Options.RelayPassword, c.Options.RelayKeyPublic, fmt.Sprintf("%s-%d", utils.SHA256(c.Options.SharedSecret[:5])[:6], j), ) if err != nil { panic(err) } log.Debugf("connected to %s", server) if !c.Options.IsSender { go c.receiveData(j) } }(i) } wg.Wait() if !c.Options.IsSender { log.Debug("sending external IP") err = message.Send(c.conn[0], c.Key, message.Message{ Type: "externalip", Message: c.ExternalIP, Bytes: m.Bytes, }) } return } func (c *Client) processExternalIP(m message.Message) (done bool, err error) { log.Debugf("received external IP: %+v", m) if c.Options.IsSender { err = message.Send(c.conn[0], c.Key, message.Message{ Type: "externalip", Message: c.ExternalIP, }) if err != nil { return true, err } } if c.ExternalIPConnected == "" { // it can be preset by the local relay c.ExternalIPConnected = m.Message } log.Debugf("connected as %s -> %s", c.ExternalIP, c.ExternalIPConnected) c.Step1ChannelSecured = true return } func (c *Client) processMessage(payload []byte) (done bool, err error) { m, err := message.Decode(c.Key, payload) if err != nil { err = fmt.Errorf("problem with decoding: %w", err) log.Debug(err) return } // only "pake" messages should be unencrypted // if a non-"pake" message is received unencrypted something // is weird if m.Type != "pake" && c.Key == nil { err = fmt.Errorf("unencrypted communication rejected") done = true return } switch m.Type { case "finished": err = message.Send(c.conn[0], c.Key, message.Message{ Type: "finished", }) done = true c.SuccessfulTransfer = true return case "pake": err = c.processMessagePake(m) if err != nil { err = fmt.Errorf("pake not successful: %w", err) log.Debug(err) } case "externalip": done, err = c.processExternalIP(m) case "error": // c.spinner.Stop() fmt.Print("\r") err = fmt.Errorf("peer error: %s", m.Message) return true, err case "fileinfo": done, err = c.processMessageFileInfo(m) case "recipientready": var remoteFile RemoteFileRequest err = json.Unmarshal(m.Bytes, &remoteFile) if err != nil { return } c.FilesToTransferCurrentNum = remoteFile.FilesToTransferCurrentNum c.CurrentFileChunkRanges = remoteFile.CurrentFileChunkRanges c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges) log.Debugf("current file chunks: %+v", c.CurrentFileChunks) c.mutex.Lock() c.chunkMap = make(map[uint64]struct{}) for _, chunk := range c.CurrentFileChunks { c.chunkMap[uint64(chunk)] = struct{}{} } c.mutex.Unlock() c.Step3RecipientRequestFile = true if c.Options.Ask { fmt.Fprintf(os.Stderr, "Send to machine '%s'? (y/n) ", remoteFile.MachineID) if strings.ToLower(strings.TrimSpace(utils.GetInput(""))) != "y" { err = message.Send(c.conn[0], c.Key, message.Message{ Type: "error", Message: "refusing files", }) done = true return } } case "close-sender": c.bar.Finish() log.Debug("close-sender received...") c.Step4FileTransfer = false c.Step3RecipientRequestFile = false log.Debug("sending close-recipient") err = message.Send(c.conn[0], c.Key, message.Message{ Type: "close-recipient", }) case "close-recipient": c.Step4FileTransfer = false c.Step3RecipientRequestFile = false } if err != nil { log.Debugf("got error from processing message: %v", err) return } err = c.updateState() if err != nil { log.Debugf("got error from updating state: %v", err) return } return } func (c *Client) updateIfSenderChannelSecured() (err error) { if c.Options.IsSender && c.Step1ChannelSecured && !c.Step2FileInfoTransfered { var b []byte machID, _ := machineid.ID() b, err = json.Marshal(SenderInfo{ FilesToTransfer: c.FilesToTransfer, MachineID: machID, Ask: c.Options.Ask, SendingText: c.Options.SendingText, NoCompress: c.Options.NoCompress, HashAlgorithm: c.Options.HashAlgorithm, }) if err != nil { log.Error(err) return } err = message.Send(c.conn[0], c.Key, message.Message{ Type: "fileinfo", Bytes: b, }) if err != nil { return } c.Step2FileInfoTransfered = true } return } func (c *Client) recipientInitializeFile() (err error) { // start initiating the process to receive a new file log.Debugf("working on file %d", c.FilesToTransferCurrentNum) // recipient sets the file pathToFile := path.Join( c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name, ) folderForFile, _ := filepath.Split(pathToFile) folderForFileBase := filepath.Base(folderForFile) if folderForFileBase != "." && folderForFileBase != "" { if err := os.MkdirAll(folderForFile, os.ModePerm); err != nil { log.Errorf("can't create %s: %v", folderForFile, err) } } var errOpen error c.CurrentFile, errOpen = os.OpenFile( pathToFile, os.O_WRONLY, 0666) var truncate bool // default false c.CurrentFileChunks = []int64{} c.CurrentFileChunkRanges = []int64{} if errOpen == nil { stat, _ := c.CurrentFile.Stat() truncate = stat.Size() != c.FilesToTransfer[c.FilesToTransferCurrentNum].Size if truncate == false { // recipient requests the file and chunks (if empty, then should receive all chunks) // TODO: determine the missing chunks c.CurrentFileChunkRanges = utils.MissingChunks( pathToFile, c.FilesToTransfer[c.FilesToTransferCurrentNum].Size, models.TCP_BUFFER_SIZE/2, ) } } else { c.CurrentFile, errOpen = os.Create(pathToFile) if errOpen != nil { errOpen = fmt.Errorf("could not create %s: %w", pathToFile, errOpen) log.Error(errOpen) return errOpen } truncate = true } if truncate { err := c.CurrentFile.Truncate(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) if err != nil { err = fmt.Errorf("could not truncate %s: %w", pathToFile, err) log.Error(err) return err } } return } func (c *Client) recipientGetFileReady(finished bool) (err error) { if finished { // TODO: do the last finishing stuff log.Debug("finished") err = message.Send(c.conn[0], c.Key, message.Message{ Type: "finished", }) if err != nil { panic(err) } c.SuccessfulTransfer = true c.FilesHasFinished[c.FilesToTransferCurrentNum] = struct{}{} } err = c.recipientInitializeFile() if err != nil { return } c.TotalSent = 0 c.CurrentFileIsClosed = false machID, _ := machineid.ID() bRequest, _ := json.Marshal(RemoteFileRequest{ CurrentFileChunkRanges: c.CurrentFileChunkRanges, FilesToTransferCurrentNum: c.FilesToTransferCurrentNum, MachineID: machID, }) log.Debug("converting to chunk range") c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges) if !finished { // setup the progressbar c.setBar() } log.Debugf("sending recipient ready with %d chunks", len(c.CurrentFileChunks)) err = message.Send(c.conn[0], c.Key, message.Message{ Type: "recipientready", Bytes: bRequest, }) if err != nil { return } c.Step3RecipientRequestFile = true return } func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error) { log.Debugf("touching file with folder / name") if !utils.Exists(fileInfo.FolderRemote) { err = os.MkdirAll(fileInfo.FolderRemote, os.ModePerm) if err != nil { log.Error(err) return } } pathToFile := path.Join(fileInfo.FolderRemote, fileInfo.Name) if fileInfo.Symlink != "" { log.Debug("creating symlink") // remove symlink if it exists if _, errExists := os.Lstat(pathToFile); errExists == nil { os.Remove(pathToFile) } err = os.Symlink(fileInfo.Symlink, pathToFile) if err != nil { return } } else { emptyFile, errCreate := os.Create(pathToFile) if errCreate != nil { log.Error(errCreate) err = errCreate return } emptyFile.Close() } // setup the progressbar description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name) if len(c.FilesToTransfer) == 1 { // description = c.FilesToTransfer[i].Name description = "" } else { description = " " + description } c.bar = progressbar.NewOptions64(1, progressbar.OptionOnCompletion(func() { c.fmtPrintUpdate() }), progressbar.OptionSetWidth(20), progressbar.OptionSetDescription(description), progressbar.OptionSetRenderBlankState(true), progressbar.OptionShowBytes(true), progressbar.OptionShowCount(), progressbar.OptionSetWriter(os.Stderr), progressbar.OptionSetVisibility(!c.Options.SendingText), ) c.bar.Finish() return } func (c *Client) updateIfRecipientHasFileInfo() (err error) { if !(!c.Options.IsSender && c.Step2FileInfoTransfered && !c.Step3RecipientRequestFile) { return } // find the next file to transfer and send that number // if the files are the same size, then look for missing chunks finished := true for i, fileInfo := range c.FilesToTransfer { if _, ok := c.FilesHasFinished[i]; ok { continue } if i < c.FilesToTransferCurrentNum { continue } log.Debugf("checking %+v", fileInfo) recipientFileInfo, errRecipientFile := os.Lstat(path.Join(fileInfo.FolderRemote, fileInfo.Name)) var errHash error var fileHash []byte if errRecipientFile == nil && recipientFileInfo.Size() == fileInfo.Size { // the file exists, but is same size, so hash it fileHash, errHash = utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name), c.Options.HashAlgorithm) } if fileInfo.Size == 0 || fileInfo.Symlink != "" { err = c.createEmptyFileAndFinish(fileInfo, i) if err != nil { return } else { c.numberOfTransferedFiles++ } continue } log.Debugf("%s %+x %+x %+v", fileInfo.Name, fileHash, fileInfo.Hash, errHash) if !bytes.Equal(fileHash, fileInfo.Hash) { log.Debugf("hashed %s to %x using %s", fileInfo.Name, fileHash, c.Options.HashAlgorithm) log.Debugf("hashes are not equal %x != %x", fileHash, fileInfo.Hash) if errHash == nil && !c.Options.Overwrite && errRecipientFile == nil { log.Debug("asking to overwrite") ans := utils.GetInput(fmt.Sprintf("\nOverwrite '%s'? (y/n) ", path.Join(fileInfo.FolderRemote, fileInfo.Name))) if strings.TrimSpace(strings.ToLower(ans)) != "y" { fmt.Fprintf(os.Stderr, "skipping '%s'", path.Join(fileInfo.FolderRemote, fileInfo.Name)) continue } } } else { log.Debugf("hashes are equal %x == %x", fileHash, fileInfo.Hash) } if errHash != nil { // probably can't find, its okay log.Debug(errHash) } if errHash != nil || !bytes.Equal(fileHash, fileInfo.Hash) { finished = false c.FilesToTransferCurrentNum = i c.numberOfTransferedFiles++ newFolder, _ := filepath.Split(fileInfo.FolderRemote) if newFolder != c.LastFolder && len(c.FilesToTransfer) > 0 { fmt.Fprintf(os.Stderr, "\r%s\n", newFolder) } c.LastFolder = newFolder break } } err = c.recipientGetFileReady(finished) return } func (c *Client) fmtPrintUpdate() { c.finishedNum++ if len(c.FilesToTransfer) > 1 { fmt.Fprintf(os.Stderr, " %d/%d\n", c.finishedNum, len(c.FilesToTransfer)) } else { fmt.Fprintf(os.Stderr, "\n") } } func (c *Client) updateState() (err error) { err = c.updateIfSenderChannelSecured() if err != nil { return } err = c.updateIfRecipientHasFileInfo() if err != nil { return } if c.Options.IsSender && c.Step3RecipientRequestFile && !c.Step4FileTransfer { log.Debug("start sending data!") if !c.firstSend { fmt.Fprintf(os.Stderr, "\nSending (->%s)\n", c.ExternalIPConnected) c.firstSend = true // if there are empty files, show them as already have been transferred now for i := range c.FilesToTransfer { if c.FilesToTransfer[i].Size == 0 { // setup the progressbar and takedown the progress bar for empty files description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name) if len(c.FilesToTransfer) == 1 { // description = c.FilesToTransfer[i].Name description = "" } c.bar = progressbar.NewOptions64(1, progressbar.OptionOnCompletion(func() { c.fmtPrintUpdate() }), progressbar.OptionSetWidth(20), progressbar.OptionSetDescription(description), progressbar.OptionSetRenderBlankState(true), progressbar.OptionShowBytes(true), progressbar.OptionShowCount(), progressbar.OptionSetWriter(os.Stderr), progressbar.OptionSetVisibility(!c.Options.SendingText), ) c.bar.Finish() } } } c.Step4FileTransfer = true // setup the progressbar c.setBar() c.TotalSent = 0 c.CurrentFileIsClosed = false log.Debug("beginning sending comms") pathToFile := path.Join( c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderSource, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name, ) c.fread, err = os.Open(pathToFile) c.numfinished = 0 if err != nil { return } for i := 0; i < len(c.Options.RelayPorts); i++ { log.Debugf("starting sending over comm %d", i) go c.sendData(i) } } return } func (c *Client) setBar() { description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name) if len(c.FilesToTransfer) == 1 { // description = c.FilesToTransfer[c.FilesToTransferCurrentNum].Name description = "" } else if !c.Options.IsSender { description = " " + description } c.bar = progressbar.NewOptions64( c.FilesToTransfer[c.FilesToTransferCurrentNum].Size, progressbar.OptionOnCompletion(func() { c.fmtPrintUpdate() }), progressbar.OptionSetWidth(20), progressbar.OptionSetDescription(description), progressbar.OptionSetRenderBlankState(true), progressbar.OptionShowBytes(true), progressbar.OptionShowCount(), progressbar.OptionSetWriter(os.Stderr), progressbar.OptionThrottle(100*time.Millisecond), progressbar.OptionSetVisibility(!c.Options.SendingText), ) byteToDo := int64(len(c.CurrentFileChunks) * models.TCP_BUFFER_SIZE / 2) if byteToDo > 0 { bytesDone := c.FilesToTransfer[c.FilesToTransferCurrentNum].Size - byteToDo log.Debug(byteToDo) log.Debug(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) log.Debug(bytesDone) if bytesDone > 0 { c.bar.Add64(bytesDone) } } } func (c *Client) receiveData(i int) { log.Debugf("%d receiving data", i) for { data, err := c.conn[i+1].Receive() if err != nil { break } if bytes.Equal(data, []byte{1}) { log.Debug("got ping") continue } data, err = crypt.Decrypt(data, c.Key) if err != nil { panic(err) } if !c.Options.NoCompress { data = compress.Decompress(data) } // get position var position uint64 rbuf := bytes.NewReader(data[:8]) err = binary.Read(rbuf, binary.LittleEndian, &position) if err != nil { panic(err) } positionInt64 := int64(position) c.mutex.Lock() _, err = c.CurrentFile.WriteAt(data[8:], positionInt64) c.mutex.Unlock() if err != nil { panic(err) } c.bar.Add(len(data[8:])) c.TotalSent += int64(len(data[8:])) c.TotalChunksTransfered++ if !c.CurrentFileIsClosed && (c.TotalChunksTransfered == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) { c.CurrentFileIsClosed = true log.Debug("finished receiving!") if err := c.CurrentFile.Close(); err != nil { log.Debugf("error closing %s: %v", c.CurrentFile.Name(), err) } if c.Options.Stdout || c.Options.SendingText { pathToFile := path.Join( c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name, ) b, _ := ioutil.ReadFile(pathToFile) fmt.Print(string(b)) } log.Debug("sending close-sender") err = message.Send(c.conn[0], c.Key, message.Message{ Type: "close-sender", }) if err != nil { panic(err) } } } } func (c *Client) sendData(i int) { defer func() { log.Debugf("finished with %d", i) c.numfinished++ if c.numfinished == len(c.Options.RelayPorts) { log.Debug("closing file") if err := c.fread.Close(); err != nil { log.Errorf("error closing file: %v", err) } } }() var readingPos int64 pos := uint64(0) curi := float64(0) for { // Read file data := make([]byte, models.TCP_BUFFER_SIZE/2) // log.Debugf("%d trying to read", i) n, errRead := c.fread.ReadAt(data, readingPos) // log.Debugf("%d read %d bytes", i, n) readingPos += int64(n) if math.Mod(curi, float64(len(c.Options.RelayPorts))) == float64(i) { // check to see if this is a chunk that the recipient wants usableChunk := true c.mutex.Lock() if len(c.chunkMap) != 0 { if _, ok := c.chunkMap[pos]; !ok { usableChunk = false } else { delete(c.chunkMap, pos) } } c.mutex.Unlock() if usableChunk { // log.Debugf("sending chunk %d", pos) posByte := make([]byte, 8) binary.LittleEndian.PutUint64(posByte, pos) var err error var dataToSend []byte if c.Options.NoCompress { dataToSend, err = crypt.Encrypt( append(posByte, data[:n]...), c.Key, ) } else { dataToSend, err = crypt.Encrypt( compress.Compress( append(posByte, data[:n]...), ), c.Key, ) } if err != nil { panic(err) } err = c.conn[i+1].Send(dataToSend) if err != nil { panic(err) } c.bar.Add(n) c.TotalSent += int64(n) // time.Sleep(100 * time.Millisecond) } } curi++ pos += uint64(n) if errRead != nil { if errRead == io.EOF { break } panic(errRead) } } }