croc/src/croc/croc.go

1518 lines
41 KiB
Go

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
Stdout bool
NoPrompt bool
NoMultiplexing bool
DisableLocal bool
OnlyLocal bool
IgnoreStdin bool
Ask bool
SendingText bool
NoCompress bool
IP string
Overwrite bool
Curve 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
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
}
// 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])
}
c.FilesToTransfer[i].Hash, err = utils.HashFile(fullPath)
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.RelayPassword, 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.RelayPassword, 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_PASSPHRASE {
flags.WriteString("--pass " + c.Options.RelayPassword + " ")
}
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 {
// 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.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.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.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
c.CurrentFile.Close()
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
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,
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,
})
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
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 = ""
}
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
}
log.Debugf("checking %+v", fileInfo)
if i < c.FilesToTransferCurrentNum {
continue
}
fileHash, errHash := utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name))
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("hashes are not equal %x != %x", fileHash, fileInfo.Hash)
if errHash == nil && !c.Options.Overwrite {
ans := utils.GetInput(fmt.Sprintf("\rOverwrite '%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++
break
}
// TODO: print out something about this file already existing
}
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
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 = ""
}
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.TotalChunksTransfered == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size {
log.Debug("finished receiving!")
if err := c.CurrentFile.Close(); err != nil {
log.Errorf("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)
}
}
}