croc/src/client.go

619 lines
16 KiB
Go

package croc
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/pake"
"github.com/schollz/progressbar"
)
var isPrinted bool
func (c *Croc) client(role int, channel string) (err error) {
defer log.Flush()
// initialize the channel data for this client
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
if role == 1 {
c.cs.Lock()
c.cs.channel.spin.Suffix = " connecting..."
c.cs.channel.spin.Start()
c.cs.Unlock()
}
// connect to the websocket
u := url.URL{Scheme: strings.Split(c.WebsocketAddress, "://")[0], Host: strings.Split(c.WebsocketAddress, "://")[1], Path: "/"}
log.Debugf("connecting to %s", u.String())
ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
// don't return error if sender can't connect, so
// that croc can be used locally without
// an internet connection
if role == 0 {
log.Debugf("dial %s error: %s", c.WebsocketAddress, err.Error())
err = nil
} else {
log.Error("dial:", err)
}
return
}
defer ws.Close()
// add websocket to locked channel
c.cs.Lock()
c.cs.channel.ws = ws
if role == 1 {
c.cs.channel.spin.Stop()
c.cs.channel.spin.Suffix = " waiting for other..."
c.cs.channel.spin.Start()
c.cs.channel.waitingForOther = true
}
c.cs.Unlock()
// read in the messages and process them
done := make(chan struct{})
go func() {
defer close(done)
for {
var cd channelData
err := ws.ReadJSON(&cd)
if err != nil {
log.Debugf("sender read error:", err)
return
}
log.Debugf("recv: %s", cd.String2())
err = c.processState(cd)
if err != nil {
log.Warn(err)
return
}
}
}()
// initialize by joining as corresponding role
// TODO:
// allowing suggesting a channel
p := channelData{
Open: true,
Role: role,
Channel: channel,
}
log.Debugf("sending opening payload: %+v", p)
c.cs.Lock()
err = c.cs.channel.ws.WriteJSON(p)
if err != nil {
log.Errorf("problem opening: %s", err.Error())
c.cs.Unlock()
return
}
c.cs.Unlock()
var wg sync.WaitGroup
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-done:
return
case <-interrupt:
// send Close signal to relay on interrupt
log.Debugf("interrupt")
c.cs.Lock()
channel := c.cs.channel.Channel
uuid := c.cs.channel.UUID
// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
log.Debug("sending close signal")
errWrite := ws.WriteJSON(channelData{
Channel: channel,
UUID: uuid,
Close: true,
})
c.cs.Unlock()
if errWrite != nil {
log.Debugf("write close:", err)
return
}
select {
case <-done:
case <-time.After(time.Second):
}
return
}
}
}(&wg)
wg.Wait()
log.Debug("waiting for unlock")
c.cs.Lock()
if c.cs.channel.finishedHappy {
log.Info("file recieved!")
log.Debug(float64(c.cs.channel.fileMetaData.Size))
log.Debug(c.cs.channel.transferTime.Seconds())
transferRate := float64(c.cs.channel.fileMetaData.Size) / 1000000.0 / c.cs.channel.transferTime.Seconds()
transferType := "MB/s"
if transferRate < 1 {
transferRate = float64(c.cs.channel.fileMetaData.Size) / 1000.0 / c.cs.channel.transferTime.Seconds()
transferType = "kB/s"
}
if c.cs.channel.Role == 0 {
fmt.Fprintf(os.Stderr, "\nTransfer complete (%2.1f %s)\n", transferRate, transferType)
} else {
folderOrFile := "file"
if c.cs.channel.fileMetaData.IsDir {
folderOrFile = "folder"
}
// push to stdout if required
if c.Stdout && !c.cs.channel.fileMetaData.IsDir {
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s (%2.1f %s)\n", folderOrFile, "stdout", transferRate, transferType)
var bFile []byte
bFile, err = ioutil.ReadFile(c.cs.channel.fileMetaData.Name)
if err != nil {
return
}
os.Stdout.Write(bFile)
os.Remove(c.cs.channel.fileMetaData.Name)
} else {
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s (%2.1f %s)\n", folderOrFile, c.cs.channel.fileMetaData.Name, transferRate, transferType)
}
}
} else {
if c.cs.channel.Error != "" {
err = errors.New(c.cs.channel.Error)
} else {
err = errors.New("one party canceled, file not transfered")
}
}
c.cs.Unlock()
log.Debug("returning")
return
}
func (c *Croc) processState(cd channelData) (err error) {
c.cs.Lock()
defer c.cs.Unlock()
// first check if there is relay reported error
if cd.Error != "" {
err = errors.New(cd.Error)
return
}
// TODO:
// check if the state is not aligned (i.e. have h(k) but no hh(k))
// throw error if not aligned so it can exit
// if file received, then you are all done
if cd.FileReceived {
c.cs.channel.FileReceived = true
c.cs.channel.finishedHappy = true
log.Debug("file recieved!")
log.Debug("sending close signal")
c.cs.channel.Close = true
c.cs.channel.ws.WriteJSON(c.cs.channel)
return
}
// otherwise, if ready to read, then set and return
if cd.ReadyToRead {
c.cs.channel.ReadyToRead = true
return
}
// otherwise, if transfer ready then send file
if cd.TransferReady {
c.cs.channel.TransferReady = true
return
}
// first update the channel data
// initialize if has UUID
if cd.UUID != "" {
c.cs.channel.UUID = cd.UUID
c.cs.channel.Channel = cd.Channel
c.cs.channel.Role = cd.Role
c.cs.channel.Curve = cd.Curve
c.cs.channel.Pake, err = pake.Init([]byte(c.cs.channel.passPhrase), cd.Role, getCurve(cd.Curve), 10*time.Millisecond)
c.cs.channel.Update = true
log.Debugf("updating channel")
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
if errWrite != nil {
log.Error(errWrite)
}
c.cs.channel.Update = false
log.Debugf("initialized client state")
return
}
// copy over the rest of the state
c.cs.channel.Ports = cd.Ports
c.cs.channel.EncryptedFileMetaData = cd.EncryptedFileMetaData
c.cs.channel.Addresses = cd.Addresses
if c.cs.channel.Role == 0 && c.isLocal {
c.cs.channel.Addresses[0] = getLocalIP()
log.Debugf("local IP: %s", c.cs.channel.Addresses[0])
}
c.bothConnected = cd.Addresses[0] != "" && cd.Addresses[1] != ""
if !c.cs.channel.waitingForPake && c.bothConnected {
c.cs.channel.waitingForOther = false
if c.cs.channel.spin.Active() {
c.cs.channel.spin.Stop()
}
c.cs.channel.waitingForPake = true
}
// update the Pake
if cd.Pake != nil && cd.Pake.Role != c.cs.channel.Role {
if c.cs.channel.Pake.HkA == nil {
log.Debugf("updating pake from %d", cd.Pake.Role)
err = c.cs.channel.Pake.Update(cd.Pake.Bytes())
if err != nil {
log.Error(err)
log.Debug("sending close signal")
c.cs.channel.Close = true
c.cs.channel.Error = err.Error()
c.cs.channel.ws.WriteJSON(c.cs.channel)
return
}
c.cs.channel.Update = true
log.Debugf("updating channel")
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
if errWrite != nil {
log.Error(errWrite)
}
c.cs.channel.Update = false
}
}
if c.cs.channel.Role == 0 && c.cs.channel.Pake.IsVerified() && !c.cs.channel.notSentMetaData && !c.cs.channel.filesReady {
go c.getFilesReady()
c.cs.channel.filesReady = true
}
if c.cs.channel.Pake.IsVerified() && c.cs.channel.waitingForPake {
c.cs.channel.waitingForPake = false
c.cs.channel.spin.Stop()
if c.cs.channel.Role == 0 {
c.cs.channel.waitingForRecipient = true
}
}
// process the client state
if c.cs.channel.Pake.IsVerified() && !c.cs.channel.isReady && c.cs.channel.EncryptedFileMetaData.Encrypted != nil {
// decrypt the meta data
log.Debugf("encrypted meta data: %+v", c.cs.channel.EncryptedFileMetaData)
var passphrase, metaDataBytes []byte
passphrase, err = c.cs.channel.Pake.SessionKey()
if err != nil {
log.Error(err)
return
}
metaDataBytes, err = c.cs.channel.EncryptedFileMetaData.decrypt(passphrase)
if err != nil {
log.Error(err)
return
}
err = json.Unmarshal(metaDataBytes, &c.cs.channel.fileMetaData)
if err != nil {
log.Error(err)
return
}
log.Debugf("meta data: %+v", c.cs.channel.fileMetaData)
// check if the user still wants to receive the file
if c.cs.channel.Role == 1 {
if !c.Yes {
if !promptOkayToRecieve(c.cs.channel.fileMetaData) {
log.Debug("sending close signal")
c.cs.channel.Close = true
c.cs.channel.Error = "refusing file"
c.cs.channel.ws.WriteJSON(c.cs.channel)
}
}
}
// spawn TCP connections
c.cs.channel.isReady = true
go c.spawnConnections(c.cs.channel.Role)
}
// process spinner
if !c.cs.channel.spin.Active() {
doStart := true
if c.cs.channel.waitingForOther {
c.cs.channel.spin.Suffix = " waiting for other..."
} else if c.cs.channel.waitingForPake {
c.cs.channel.spin.Suffix = " performing PAKE..."
} else if c.cs.channel.waitingForRecipient {
c.cs.channel.spin.Suffix = " waiting for ok..."
} else {
doStart = false
}
if doStart {
c.cs.channel.spin.Start()
}
}
return
}
func (c *Croc) spawnConnections(role int) (err error) {
err = c.dialUp()
if err == nil {
if role == 1 {
err = c.processReceivedFile()
}
} else {
log.Error(err)
}
return
}
func (c *Croc) dialUp() (err error) {
c.cs.Lock()
ports := c.cs.channel.Ports
channel := c.cs.channel.Channel
uuid := c.cs.channel.UUID
role := c.cs.channel.Role
c.cs.Unlock()
errorChan := make(chan error, len(ports))
if role == 1 {
// generate a receive filename
c.crocFileEncrypted = tempFileName("croc-received")
}
for i, port := range ports {
go func(channel, uuid, port string, i int, errorChan chan error) {
if i == 0 {
log.Debug("dialing up")
}
log.Debugf("connecting to %s", "localhost:"+port)
address := strings.Split(strings.Split(c.WebsocketAddress, "://")[1], ":")[0]
connection, err := net.Dial("tcp", address+":"+port)
if err != nil {
errorChan <- err
return
}
defer connection.Close()
connection.SetReadDeadline(time.Now().Add(1 * time.Hour))
connection.SetDeadline(time.Now().Add(1 * time.Hour))
connection.SetWriteDeadline(time.Now().Add(1 * time.Hour))
message, err := receiveMessage(connection)
if err != nil {
errorChan <- err
return
}
log.Debugf("relay says: %s", message)
err = sendMessage(channel, connection)
if err != nil {
errorChan <- err
return
}
err = sendMessage(uuid, connection)
if err != nil {
errorChan <- err
return
}
// wait for transfer to be ready
for {
c.cs.RLock()
ready := c.cs.channel.TransferReady
if role == 0 {
ready = ready && c.cs.channel.fileReady
}
c.cs.RUnlock()
if ready {
break
}
time.Sleep(10 * time.Millisecond)
}
if i == 0 {
c.cs.Lock()
if c.cs.channel.waitingForRecipient {
c.cs.channel.spin.Stop()
c.cs.channel.waitingForRecipient = false
fmt.Print(" ")
}
c.bar = progressbar.NewOptions(
c.cs.channel.fileMetaData.Size,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetBytes(c.cs.channel.fileMetaData.Size),
)
if role == 0 {
fmt.Fprintf(os.Stderr, "\nSending (->%s)...\n", c.cs.channel.Addresses[1])
} else {
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)...\n", c.cs.channel.Addresses[0])
}
c.cs.Unlock()
}
if role == 0 {
log.Debug("send file")
for {
c.cs.RLock()
ready := c.cs.channel.ReadyToRead
c.cs.RUnlock()
if ready {
break
}
time.Sleep(10 * time.Millisecond)
}
log.Debug("sending file")
filename := c.crocFileEncrypted + "." + strconv.Itoa(i)
if i == 0 {
c.cs.Lock()
c.cs.channel.startTransfer = time.Now()
c.cs.Unlock()
}
err = c.sendFile(filename, i, connection)
} else {
go func() {
time.Sleep(10 * time.Millisecond)
c.cs.Lock()
log.Debugf("updating channel with ready to read")
c.cs.channel.Update = true
c.cs.channel.ReadyToRead = true
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
if errWrite != nil {
log.Error(errWrite)
}
c.cs.channel.Update = false
c.cs.Unlock()
log.Debug("receive file")
}()
if i == 0 {
c.cs.Lock()
c.cs.channel.startTransfer = time.Now()
c.cs.Unlock()
}
receiveFileName := c.crocFileEncrypted + "." + strconv.Itoa(i)
log.Debugf("receiving file into %s", receiveFileName)
err = c.receiveFile(receiveFileName, i, connection)
}
c.bar.Finish()
errorChan <- err
}(channel, uuid, port, i, errorChan)
}
// collect errors
for i := 0; i < len(ports); i++ {
errOne := <-errorChan
if errOne != nil {
log.Warn(errOne)
log.Debug("sending close signal")
c.cs.channel.Close = true
c.cs.channel.ws.WriteJSON(c.cs.channel)
}
}
c.cs.Lock()
c.cs.channel.transferTime = time.Since(c.cs.channel.startTransfer)
c.cs.Unlock()
log.Debug("leaving dialup")
c.normalFinish = true
return
}
func (c *Croc) receiveFile(filename string, id int, connection net.Conn) error {
log.Debug("waiting for chunk size from sender")
fileSizeBuffer := make([]byte, 10)
connection.Read(fileSizeBuffer)
fileDataString := strings.Trim(string(fileSizeBuffer), ":")
fileSizeInt, _ := strconv.Atoi(fileDataString)
chunkSize := int64(fileSizeInt)
log.Debugf("chunk size: %d", chunkSize)
if chunkSize == 0 {
log.Debug(fileSizeBuffer)
return errors.New("chunk size is empty!")
}
os.Remove(filename)
log.Debug("making " + filename)
newFile, err := os.Create(filename)
if err != nil {
panic(err)
}
defer newFile.Close()
log.Debug(id, "waiting for file")
var receivedBytes int64
receivedFirstBytes := false
for {
if c.cleanupTime {
break
}
if (chunkSize - receivedBytes) < bufferSize {
log.Debugf("%d at the end: %d < %d", id, (chunkSize - receivedBytes), bufferSize)
io.CopyN(newFile, connection, (chunkSize - receivedBytes))
// Empty the remaining bytes that we don't need from the network buffer
if (receivedBytes+bufferSize)-chunkSize < bufferSize {
log.Debug(id, "empty remaining bytes from network buffer")
connection.Read(make([]byte, (receivedBytes+bufferSize)-chunkSize))
}
break
}
written, _ := io.CopyN(newFile, connection, bufferSize)
receivedBytes += written
c.bar.Add(int(written))
if !receivedFirstBytes {
receivedFirstBytes = true
log.Debug(id, "Received first bytes!")
}
}
log.Debug(id, "received file")
return nil
}
func (c *Croc) sendFile(filename string, id int, connection net.Conn) error {
// open encrypted file chunk, if it exists
log.Debug("opening encrypted file chunk: " + filename)
file, err := os.Open(filename)
if err != nil {
log.Error(err)
return nil
}
defer file.Close()
defer os.Remove(filename)
// determine and send the file size to client
fi, err := file.Stat()
if err != nil {
log.Error(err)
return err
}
log.Debugf("sending chunk size: %d", fi.Size())
log.Debug(connection.RemoteAddr())
_, err = connection.Write([]byte(fillString(strconv.FormatInt(int64(fi.Size()), 10), 10)))
if err != nil {
return errors.Wrap(err, "Problem sending chunk data: ")
}
// rate limit the bandwidth
log.Debug("determining rate limiting")
rate := 10000
throttle := time.NewTicker(time.Second / time.Duration(rate))
log.Debugf("rate: %+v", rate)
defer throttle.Stop()
// send the file
sendBuffer := make([]byte, bufferSize)
totalBytesSent := 0
for range throttle.C {
if c.cleanupTime {
break
}
_, err := file.Read(sendBuffer)
written, _ := connection.Write(sendBuffer)
totalBytesSent += written
c.bar.Add(written)
// if errWrite != nil {
// errWrite = errors.Wrap(errWrite, "problem writing to connection")
// return errWrite
// }
if err == io.EOF {
//End of file reached, break out of for loop
log.Debug("EOF")
err = nil // not really an error
break
}
}
log.Debug("file is sent")
log.Debug("removing piece")
return err
}