completely rewrite

This commit is contained in:
Zack Scholl 2018-09-21 20:17:57 -07:00
parent 6af10ad871
commit b9dad87526
32 changed files with 850 additions and 2845 deletions

20
go.mod
View File

@ -1,21 +1,3 @@
module github.com/schollz/croc
require (
github.com/briandowns/spinner v0.0.0-20180822135157-9f016caa1359
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d
github.com/fatih/color v1.7.0 // indirect
github.com/frankenbeanies/uuid4 v0.0.0-20180313125435-68b799ec299a
github.com/gorilla/websocket v1.4.0
github.com/mars9/crypt v0.0.0-20150406101210-65899cf653ff
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/pkg/errors v0.8.0
github.com/schollz/mnemonicode v1.0.1
github.com/schollz/pake v1.0.2
github.com/schollz/peerdiscovery v1.2.1
github.com/schollz/progressbar/v2 v2.5.3
github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937
github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
)
require github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575

242
main.go
View File

@ -1,131 +1,131 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
croc "github.com/schollz/croc/src"
"github.com/urfave/cli"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/logger"
)
var version string
var codePhrase string
var cr *croc.Croc
func main() {
app := cli.NewApp()
app.Name = "croc"
if version == "" {
version = "dev"
}
app.Version = version
app.Compiled = time.Now()
app.Usage = "easily and securely transfer stuff from one computer to another"
app.UsageText = "croc allows any two computers to directly and securely transfer files"
// app.ArgsUsage = "[args and such]"
app.Commands = []cli.Command{
{
Name: "send",
Usage: "send a file",
Description: "send a file over the relay",
ArgsUsage: "[filename]",
Flags: []cli.Flag{
cli.BoolFlag{Name: "no-compress, o", Usage: "disable compression"},
cli.BoolFlag{Name: "no-encrypt, e", Usage: "disable encryption"},
cli.StringFlag{Name: "code, c", Usage: "codephrase used to connect to relay"},
},
HelpName: "croc send",
Action: func(c *cli.Context) error {
return send(c)
},
},
{
Name: "relay",
Usage: "start a croc relay",
Description: "the croc relay will handle websocket and TCP connections",
Flags: []cli.Flag{
cli.StringFlag{Name: "tcp", Value: "27130,27131,27132,27133", Usage: "ports for the tcp connections"},
cli.StringFlag{Name: "port", Value: "8130", Usage: "port that the websocket listens on"},
cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use (p224, p256, p384, p521, siec)"},
},
HelpName: "croc relay",
Action: func(c *cli.Context) error {
return relay(c)
},
},
}
app.Flags = []cli.Flag{
cli.StringFlag{Name: "relay", Value: "wss://croc3.schollz.com"},
cli.BoolFlag{Name: "no-local", Usage: "disable local mode"},
cli.BoolFlag{Name: "local", Usage: "use only local mode"},
cli.BoolFlag{Name: "debug", Usage: "increase verbosity (a lot)"},
cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
}
app.EnableBashCompletion = true
app.HideHelp = false
app.HideVersion = false
app.BashComplete = func(c *cli.Context) {
fmt.Fprintf(c.App.Writer, "send\nreceive\relay")
}
app.Action = func(c *cli.Context) error {
return receive(c)
}
app.Before = func(c *cli.Context) error {
cr = croc.Init()
cr.AllowLocalDiscovery = true
cr.WebsocketAddress = c.GlobalString("relay")
cr.SetDebug(c.GlobalBool("debug"))
cr.Yes = c.GlobalBool("yes")
cr.Stdout = c.GlobalBool("stdout")
cr.LocalOnly = c.GlobalBool("local")
cr.NoLocal = c.GlobalBool("no-local")
return nil
}
err := app.Run(os.Args)
if err != nil {
fmt.Printf("\nerror: %s", err.Error())
}
defer log.Flush()
logger.SetLogLevel("debug")
log.Debug("hi")
}
func send(c *cli.Context) error {
stat, _ := os.Stdin.Stat()
var fname string
if (stat.Mode() & os.ModeCharDevice) == 0 {
fname = "stdin"
} else {
fname = c.Args().First()
}
if fname == "" {
return errors.New("must specify file: croc send [filename]")
}
cr.UseCompression = !c.Bool("no-compress")
cr.UseEncryption = !c.Bool("no-encrypt")
if c.String("code") != "" {
codePhrase = c.String("code")
}
return cr.Send(fname, codePhrase)
}
// var version string
// var codePhrase string
func receive(c *cli.Context) error {
if c.GlobalString("code") != "" {
codePhrase = c.GlobalString("code")
}
if c.Args().First() != "" {
codePhrase = c.Args().First()
}
return cr.Receive(codePhrase)
}
// var cr *croc.Croc
func relay(c *cli.Context) error {
cr.TcpPorts = strings.Split(c.String("tcp"), ",")
cr.ServerPort = c.String("port")
cr.CurveType = c.String("curve")
return cr.Relay()
}
// func main() {
// app := cli.NewApp()
// app.Name = "croc"
// if version == "" {
// version = "dev"
// }
// app.Version = version
// app.Compiled = time.Now()
// app.Usage = "easily and securely transfer stuff from one computer to another"
// app.UsageText = "croc allows any two computers to directly and securely transfer files"
// // app.ArgsUsage = "[args and such]"
// app.Commands = []cli.Command{
// {
// Name: "send",
// Usage: "send a file",
// Description: "send a file over the relay",
// ArgsUsage: "[filename]",
// Flags: []cli.Flag{
// cli.BoolFlag{Name: "no-compress, o", Usage: "disable compression"},
// cli.BoolFlag{Name: "no-encrypt, e", Usage: "disable encryption"},
// cli.StringFlag{Name: "code, c", Usage: "codephrase used to connect to relay"},
// },
// HelpName: "croc send",
// Action: func(c *cli.Context) error {
// return send(c)
// },
// },
// {
// Name: "relay",
// Usage: "start a croc relay",
// Description: "the croc relay will handle websocket and TCP connections",
// Flags: []cli.Flag{
// cli.StringFlag{Name: "tcp", Value: "27130,27131,27132,27133", Usage: "ports for the tcp connections"},
// cli.StringFlag{Name: "port", Value: "8130", Usage: "port that the websocket listens on"},
// cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use (p224, p256, p384, p521, siec)"},
// },
// HelpName: "croc relay",
// Action: func(c *cli.Context) error {
// return relay(c)
// },
// },
// }
// app.Flags = []cli.Flag{
// cli.StringFlag{Name: "relay", Value: "wss://croc3.schollz.com"},
// cli.BoolFlag{Name: "no-local", Usage: "disable local mode"},
// cli.BoolFlag{Name: "local", Usage: "use only local mode"},
// cli.BoolFlag{Name: "debug", Usage: "increase verbosity (a lot)"},
// cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
// cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
// }
// app.EnableBashCompletion = true
// app.HideHelp = false
// app.HideVersion = false
// app.BashComplete = func(c *cli.Context) {
// fmt.Fprintf(c.App.Writer, "send\nreceive\relay")
// }
// app.Action = func(c *cli.Context) error {
// return receive(c)
// }
// app.Before = func(c *cli.Context) error {
// cr = croc.Init()
// cr.AllowLocalDiscovery = true
// cr.WebsocketAddress = c.GlobalString("relay")
// cr.SetDebug(c.GlobalBool("debug"))
// cr.Yes = c.GlobalBool("yes")
// cr.Stdout = c.GlobalBool("stdout")
// cr.LocalOnly = c.GlobalBool("local")
// cr.NoLocal = c.GlobalBool("no-local")
// return nil
// }
// err := app.Run(os.Args)
// if err != nil {
// fmt.Printf("\nerror: %s", err.Error())
// }
// }
// func send(c *cli.Context) error {
// stat, _ := os.Stdin.Stat()
// var fname string
// if (stat.Mode() & os.ModeCharDevice) == 0 {
// fname = "stdin"
// } else {
// fname = c.Args().First()
// }
// if fname == "" {
// return errors.New("must specify file: croc send [filename]")
// }
// cr.UseCompression = !c.Bool("no-compress")
// cr.UseEncryption = !c.Bool("no-encrypt")
// if c.String("code") != "" {
// codePhrase = c.String("code")
// }
// return cr.Send(fname, codePhrase)
// }
// func receive(c *cli.Context) error {
// if c.GlobalString("code") != "" {
// codePhrase = c.GlobalString("code")
// }
// if c.Args().First() != "" {
// codePhrase = c.Args().First()
// }
// return cr.Receive(codePhrase)
// }
// func relay(c *cli.Context) error {
// cr.TcpPorts = strings.Split(c.String("tcp"), ",")
// cr.ServerPort = c.String("port")
// cr.CurveType = c.String("curve")
// return cr.Relay()
// }

View File

@ -1,186 +0,0 @@
package croc
import (
"net"
"time"
log "github.com/cihub/seelog"
"github.com/pkg/errors"
"github.com/schollz/peerdiscovery"
)
func init() {
SetLogLevel("debug")
}
// Relay initiates a relay
func (c *Croc) Relay() error {
// start relay
go c.startRelay()
// start server
return c.startServer()
}
// Send will take an existing file or folder and send it through the croc relay
func (c *Croc) Send(fname string, codePhrase string) (err error) {
log.Debugf("sending %s with compression, encryption: (%v, %v)", fname, c.UseCompression, c.UseEncryption)
// prepare code phrase
defer c.cleanup()
c.cs.Lock()
c.cs.channel.codePhrase = codePhrase
if len(codePhrase) == 0 {
// generate code phrase
codePhrase = getRandomName()
}
if len(codePhrase) < 4 {
err = errors.New("code phrase must be more than 4 characters")
c.cs.Unlock()
return
}
c.cs.channel.codePhrase = codePhrase
c.cs.channel.Channel = codePhrase[:3]
c.cs.channel.passPhrase = codePhrase[3:]
log.Debugf("codephrase: '%s'", codePhrase)
log.Debugf("channel: '%s'", c.cs.channel.Channel)
log.Debugf("passPhrase: '%s'", c.cs.channel.passPhrase)
channel := c.cs.channel.Channel
c.cs.Unlock()
// start peer discovery
go func() {
if c.NoLocal {
return
}
log.Debug("listening for local croc relay...")
go peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
TimeLimit: 600 * time.Second,
Delay: 50 * time.Millisecond,
Payload: []byte(codePhrase[:3]),
})
}()
if len(fname) == 0 {
err = errors.New("must include filename")
return
}
err = c.processFile(fname)
if err != nil {
return
}
// start relay for listening
type runInfo struct {
err error
bothConnected bool
}
runClientError := make(chan runInfo, 2)
go func() {
if c.NoLocal {
return
}
d := Init()
d.ServerPort = "8140"
d.TcpPorts = []string{"27140", "27141"}
go d.startRelay()
go d.startServer()
time.Sleep(100 * time.Millisecond)
ce := Init()
ce.WebsocketAddress = "ws://127.0.0.1:8140"
// copy over the information
c.cs.Lock()
ce.cs.Lock()
ce.cs.channel.codePhrase = codePhrase
ce.cs.channel.Channel = codePhrase[:3]
ce.cs.channel.passPhrase = codePhrase[3:]
ce.cs.channel.fileMetaData = c.cs.channel.fileMetaData
ce.crocFile = c.crocFile
ce.crocFileEncrypted = ce.crocFileEncrypted
ce.isLocal = true
ce.cs.Unlock()
c.cs.Unlock()
defer func() {
// delete croc files
ce.cleanup()
}()
var ri runInfo
ri.err = ce.client(0, channel)
ri.bothConnected = ce.bothConnected
runClientError <- ri
}()
// start main client
go func() {
if c.LocalOnly {
return
}
var ri runInfo
ri.err = c.client(0, channel)
ri.bothConnected = c.bothConnected
runClientError <- ri
}()
var ri runInfo
ri = <-runClientError
if ri.bothConnected || c.LocalOnly || c.NoLocal {
return ri.err
}
ri = <-runClientError
return ri.err
}
// Receive will receive something through the croc relay
func (c *Croc) Receive(codePhrase string) (err error) {
defer c.cleanup()
log.Debugf("receiving with code phrase: %s", codePhrase)
if !c.NoLocal {
// try to discovery codephrase and server through peer network
discovered, errDiscover := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
TimeLimit: 300 * time.Millisecond,
Delay: 50 * time.Millisecond,
Payload: []byte("checking"),
})
if errDiscover != nil {
log.Debug(errDiscover)
}
if len(discovered) > 0 {
log.Debugf("discovered %s on %s", discovered[0].Payload, discovered[0].Address)
_, connectTimeout := net.DialTimeout("tcp", discovered[0].Address+":27140", 1*time.Second)
if connectTimeout == nil {
log.Debug("connected")
c.WebsocketAddress = "ws://" + discovered[0].Address + ":8140"
c.isLocal = true
log.Debug(discovered[0].Address)
// codePhrase = string(discovered[0].Payload)
} else {
log.Debug("but could not connect to ports")
}
} else {
log.Debug("discovered no peers")
}
}
// prepare codephrase
c.cs.Lock()
if len(codePhrase) == 0 {
// prompt codephrase
codePhrase = promptCodePhrase()
}
if len(codePhrase) < 4 {
err = errors.New("code phrase must be more than 4 characters")
c.cs.Unlock()
return
}
c.cs.channel.codePhrase = codePhrase
c.cs.channel.Channel = codePhrase[:3]
c.cs.channel.passPhrase = codePhrase[3:]
log.Debugf("codephrase: '%s'", codePhrase)
log.Debugf("channel: '%s'", c.cs.channel.Channel)
log.Debugf("passPhrase: '%s'", c.cs.channel.passPhrase)
channel := c.cs.channel.Channel
c.cs.Unlock()
return c.client(1, channel)
}

View File

@ -1,31 +0,0 @@
package croc
import (
"os"
"strconv"
"time"
)
func (c *Croc) cleanup() {
c.cleanupTime = true
if !c.normalFinish {
time.Sleep(1000 * time.Millisecond) // race condition, wait for
// sending/receiving to finish
}
// erase all the croc files and their possible numbers
for i := 0; i < 16; i++ {
fname := c.crocFile + "." + strconv.Itoa(i)
os.Remove(fname)
}
for i := 0; i < 16; i++ {
fname := c.crocFileEncrypted + "." + strconv.Itoa(i)
os.Remove(fname)
}
os.Remove(c.crocFile)
os.Remove(c.crocFileEncrypted)
c.cs.Lock()
if c.cs.channel.fileMetaData.DeleteAfterSending {
os.Remove(c.cs.channel.fileMetaData.Name)
}
defer c.cs.Unlock()
}

View File

@ -1,620 +0,0 @@
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/v2"
)
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(3 * time.Hour))
connection.SetDeadline(time.Now().Add(3 * time.Hour))
connection.SetWriteDeadline(time.Now().Add(3 * 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)
}
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)
}
}
// close bar
c.bar.Finish()
// measure transfer time
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
}

36
src/compress/compress.go Normal file
View File

@ -0,0 +1,36 @@
package compress
import (
"bytes"
"compress/flate"
"io"
)
// Compress returns a compressed byte slice.
func Compress(src []byte) []byte {
compressedData := new(bytes.Buffer)
compress(src, compressedData, 9)
return compressedData.Bytes()
}
// Decompress returns a decompressed byte slice.
func Decompress(src []byte) []byte {
compressedData := bytes.NewBuffer(src)
deCompressedData := new(bytes.Buffer)
decompress(compressedData, deCompressedData)
return deCompressedData.Bytes()
}
// compress uses flate to compress a byte slice to a corresponding level
func compress(src []byte, dest io.Writer, level int) {
compressor, _ := flate.NewWriter(dest, level)
compressor.Write(src)
compressor.Close()
}
// compress uses flate to decompress an io.Reader
func decompress(src io.Reader, dest io.Writer) {
decompressor := flate.NewReader(src)
io.Copy(dest, decompressor)
decompressor.Close()
}

34
src/croc/croc.go Normal file
View File

@ -0,0 +1,34 @@
package croc
import "time"
// Croc options
type Croc struct {
// Options for all
Debug bool
// Options for relay
ServerPort string
CurveType string
// Options for connecting to server
WebsocketAddress string
Timeout time.Duration
LocalOnly bool
NoLocal bool
// Options for file transfering
UseEncryption bool
UseCompression bool
AllowLocalDiscovery bool
Yes bool
Stdout bool
// private variables
// localIP address
localIP string
// is using local relay
isLocal bool
normalFinish bool
}

61
src/crypt/crypt.go Normal file
View File

@ -0,0 +1,61 @@
package crypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"golang.org/x/crypto/pbkdf2"
)
// Encryption stores the data
type Encryption struct {
Encrypted, Salt, IV []byte
}
// Encrypt will generate an encryption
func Encrypt(plaintext []byte, passphrase []byte, dontencrypt ...bool) Encryption {
if len(dontencrypt) > 0 && dontencrypt[0] {
return Encryption{
Encrypted: plaintext,
Salt: []byte("salt"),
IV: []byte("iv"),
}
}
key, saltBytes := deriveKey(passphrase, nil)
ivBytes := make([]byte, 12)
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
// Section 8.2
rand.Read(ivBytes)
b, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(b)
encrypted := aesgcm.Seal(nil, ivBytes, plaintext, nil)
return Encryption{
Encrypted: encrypted,
Salt: saltBytes,
IV: ivBytes,
}
}
// Decrypt an encryption
func (e Encryption) Decrypt(passphrase []byte, dontencrypt ...bool) (plaintext []byte, err error) {
if len(dontencrypt) > 0 && dontencrypt[0] {
return e.Encrypted, nil
}
key, _ := deriveKey(passphrase, e.Salt)
b, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(b)
plaintext, err = aesgcm.Open(nil, e.IV, e.Encrypted, nil)
return
}
func deriveKey(passphrase []byte, salt []byte) ([]byte, []byte) {
if salt == nil {
salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
// Salt.
rand.Read(salt)
}
return pbkdf2.Key([]byte(passphrase), salt, 100, 32, sha256.New), salt
}

View File

@ -1,131 +0,0 @@
package croc
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"fmt"
mathrand "math/rand"
"os"
"strings"
"time"
log "github.com/cihub/seelog"
"github.com/mars9/crypt"
"github.com/schollz/mnemonicode"
"golang.org/x/crypto/pbkdf2"
)
func init() {
mathrand.Seed(time.Now().UTC().UnixNano())
}
func getRandomName() string {
result := []string{}
bs := make([]byte, 4)
rand.Read(bs)
result = mnemonicode.EncodeWordList(result, bs)
return strings.Join(result, "-")
}
type encryption struct {
Encrypted, Salt, IV []byte
}
func encrypt(plaintext []byte, passphrase []byte, dontencrypt ...bool) encryption {
if len(dontencrypt) > 0 && dontencrypt[0] {
return encryption{
Encrypted: plaintext,
Salt: []byte("salt"),
IV: []byte("iv"),
}
}
key, saltBytes := deriveKey(passphrase, nil)
ivBytes := make([]byte, 12)
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
// Section 8.2
rand.Read(ivBytes)
b, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(b)
encrypted := aesgcm.Seal(nil, ivBytes, plaintext, nil)
return encryption{
Encrypted: encrypted,
Salt: saltBytes,
IV: ivBytes,
}
}
func (e encryption) decrypt(passphrase []byte, dontencrypt ...bool) (plaintext []byte, err error) {
if len(dontencrypt) > 0 && dontencrypt[0] {
return e.Encrypted, nil
}
key, _ := deriveKey(passphrase, e.Salt)
b, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(b)
plaintext, err = aesgcm.Open(nil, e.IV, e.Encrypted, nil)
return
}
func deriveKey(passphrase []byte, salt []byte) ([]byte, []byte) {
if salt == nil {
salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
// Salt.
rand.Read(salt)
}
return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
}
func hash(data string) string {
return hashBytes([]byte(data))
}
func hashBytes(data []byte) string {
sum := sha256.Sum256(data)
return fmt.Sprintf("%x", sum)
}
func encryptFile(inputFilename string, outputFilename string, password []byte) error {
return cryptFile(inputFilename, outputFilename, password, true)
}
func decryptFile(inputFilename string, outputFilename string, password []byte) error {
return cryptFile(inputFilename, outputFilename, password, false)
}
func cryptFile(inputFilename string, outputFilename string, password []byte, encrypt bool) error {
in, err := os.Open(inputFilename)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(outputFilename)
if err != nil {
return err
}
defer func() {
if err := out.Sync(); err != nil {
log.Error(err)
}
if err := out.Close(); err != nil {
log.Error(err)
}
}()
c := &crypt.Crypter{
HashFunc: sha1.New,
HashSize: sha1.Size,
Key: crypt.NewPbkdf2Key(password, 32),
}
if encrypt {
if err := c.Encrypt(out, in); err != nil {
return err
}
} else {
if err := c.Decrypt(out, in); err != nil {
return err
}
}
return nil
}

View File

@ -1,43 +0,0 @@
package croc
// func TestEncrypt(t *testing.T) {
// key := getRandomName()
// encrypted, salt, iv := encrypt([]byte("hello, world"), key)
// decrypted, err := decrypt(encrypted, key, salt, iv)
// if err != nil {
// t.Error(err)
// }
// if string(decrypted) != "hello, world" {
// t.Error("problem decrypting")
// }
// _, err = decrypt(encrypted, "wrong passphrase", salt, iv)
// if err == nil {
// t.Error("should not work!")
// }
// }
// func TestEncryptFiles(t *testing.T) {
// key := getRandomName()
// if err := ioutil.WriteFile("temp", []byte("hello, world!"), 0644); err != nil {
// t.Error(err)
// }
// if err := encryptFile("temp", "temp.enc", key); err != nil {
// t.Error(err)
// }
// if err := decryptFile("temp.enc", "temp.dec", key); err != nil {
// t.Error(err)
// }
// data, err := ioutil.ReadFile("temp.dec")
// if string(data) != "hello, world!" {
// t.Errorf("Got something weird: " + string(data))
// }
// if err != nil {
// t.Error(err)
// }
// if err := decryptFile("temp.enc", "temp.dec", key+"wrong password"); err == nil {
// t.Error("should throw error!")
// }
// os.Remove("temp.dec")
// os.Remove("temp.enc")
// os.Remove("temp")
// }

View File

@ -1,247 +0,0 @@
package croc
import (
"encoding/json"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"time"
log "github.com/cihub/seelog"
"github.com/pkg/errors"
)
func (c *Croc) processFile(src string) (err error) {
log.Debug("processing file")
defer func() {
log.Debug("finished processing file")
}()
fd := FileMetaData{}
// pathToFile and filename are the files that should be used internally
var pathToFile, filename string
// first check if it is stdin
if src == "stdin" {
var f *os.File
f, err = ioutil.TempFile(".", "croc-stdin-")
if err != nil {
return
}
_, err = io.Copy(f, os.Stdin)
if err != nil {
return
}
pathToFile = "."
filename = f.Name()
err = f.Close()
if err != nil {
return
}
// fd.Name is what the user will see
fd.Name = filename
fd.DeleteAfterSending = true
} else {
if !exists(src) {
err = errors.Errorf("file/folder '%s' does not exist", src)
return
}
pathToFile, filename = filepath.Split(filepath.Clean(src))
fd.Name = filename
}
// check wether the file is a dir
info, err := os.Stat(path.Join(pathToFile, filename))
if err != nil {
log.Error(err)
return
}
fd.IsDir = info.Mode().IsDir()
// zip file
log.Debug("zipping file")
c.crocFile, err = zipFile(path.Join(pathToFile, filename), c.UseCompression)
if err != nil {
log.Error(err)
return
}
log.Debug("...finished zipping")
fd.IsCompressed = c.UseCompression
fd.IsEncrypted = c.UseEncryption
fd.Hash, err = hashFile(c.crocFile)
if err != nil {
log.Error(err)
return err
}
fd.Size, err = fileSize(c.crocFile)
if err != nil {
err = errors.Wrap(err, "could not determine filesize")
log.Error(err)
return err
}
c.cs.Lock()
defer c.cs.Unlock()
c.cs.channel.fileMetaData = fd
go showIntro(c.cs.channel.codePhrase, fd)
return
}
func (c *Croc) getFilesReady() (err error) {
log.Debug("getting files ready")
defer func() {
log.Debug("files ready")
}()
c.cs.Lock()
defer c.cs.Unlock()
c.cs.channel.notSentMetaData = true
// send metadata
// wait until data is ready
for {
if c.cs.channel.fileMetaData.Name != "" {
break
}
c.cs.Unlock()
time.Sleep(10 * time.Millisecond)
c.cs.Lock()
}
// get passphrase
var passphrase []byte
passphrase, err = c.cs.channel.Pake.SessionKey()
if err != nil {
log.Error(err)
return
}
if c.UseEncryption {
// encrypt file data
c.crocFileEncrypted = tempFileName("croc-encrypted")
err = encryptFile(c.crocFile, c.crocFileEncrypted, passphrase)
if err != nil {
log.Error(err)
return
}
// remove the unencrypted versoin
if err = os.Remove(c.crocFile); err != nil {
log.Error(err)
return
}
c.cs.channel.fileMetaData.IsEncrypted = true
} else {
c.crocFileEncrypted = c.crocFile
}
// split into pieces to send
log.Debugf("splitting %s", c.crocFileEncrypted)
if err = splitFile(c.crocFileEncrypted, len(c.cs.channel.Ports)); err != nil {
log.Error(err)
return
}
// remove the file now since we still have pieces
if err = os.Remove(c.crocFileEncrypted); err != nil {
log.Error(err)
return
}
// encrypt meta data
var metaDataBytes []byte
metaDataBytes, err = json.Marshal(c.cs.channel.fileMetaData)
if err != nil {
log.Error(err)
return
}
c.cs.channel.EncryptedFileMetaData = encrypt(metaDataBytes, passphrase)
c.cs.channel.Update = true
log.Debugf("updating channel with file information")
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
if errWrite != nil {
log.Error(errWrite)
}
c.cs.channel.Update = false
go func() {
c.cs.Lock()
c.cs.channel.fileReady = true
c.cs.Unlock()
}()
return
}
func (c *Croc) processReceivedFile() (err error) {
// cat the file received
c.cs.Lock()
defer c.cs.Unlock()
c.cs.channel.FileReceived = true
defer func() {
c.cs.channel.Update = true
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
if errWrite != nil {
log.Error(errWrite)
return
}
c.cs.channel.Update = false
}()
filesToCat := make([]string, len(c.cs.channel.Ports))
for i := range c.cs.channel.Ports {
filesToCat[i] = c.crocFileEncrypted + "." + strconv.Itoa(i)
log.Debugf("going to cat file %s", filesToCat[i])
}
// defer os.Remove(c.crocFile)
log.Debugf("catting file into %s", c.crocFile)
err = catFiles(filesToCat, c.crocFileEncrypted, true)
if err != nil {
log.Error(err)
return
}
// unencrypt
c.crocFile = tempFileName("croc-unencrypted")
var passphrase []byte
passphrase, err = c.cs.channel.Pake.SessionKey()
if err != nil {
log.Error(err)
return
}
// decrypt if was encrypted on the other side
if c.cs.channel.fileMetaData.IsEncrypted {
err = decryptFile(c.crocFileEncrypted, c.crocFile, passphrase)
if err != nil {
log.Error(err)
return
}
os.Remove(c.crocFileEncrypted)
} else {
c.crocFile = c.crocFileEncrypted
}
// check hash
log.Debug("checking hash")
var hashString string
hashString, err = hashFile(c.crocFile)
if err != nil {
log.Error(err)
return
}
if hashString == c.cs.channel.fileMetaData.Hash {
log.Debug("hashes match")
} else {
err = errors.Errorf("hashes do not match, %s != %s", c.cs.channel.fileMetaData.Hash, hashString)
log.Error(err)
return
}
// unzip file
err = unzipFile(c.crocFile, ".")
if err != nil {
log.Error(err)
return
}
os.Remove(c.crocFile)
c.cs.channel.finishedHappy = true
return
}

View File

@ -1,4 +1,4 @@
package croc
package logger
import (
log "github.com/cihub/seelog"

View File

@ -1,201 +0,0 @@
package croc
import (
"encoding/json"
"net"
"sync"
"time"
"github.com/briandowns/spinner"
"github.com/gorilla/websocket"
"github.com/schollz/pake"
"github.com/schollz/progressbar/v2"
)
const (
// maximum buffer size for initial TCP communication
bufferSize = 1024
)
type Croc struct {
// Options for all
Debug bool
// Options for relay
ServerPort string
CurveType string
// Options for connecting to server
TcpPorts []string
WebsocketAddress string
Timeout time.Duration
LocalOnly bool
NoLocal bool
// Options for file transfering
UseEncryption bool
UseCompression bool
AllowLocalDiscovery bool
Yes bool
Stdout bool
// private variables
// localIP address
localIP string
// is using local relay
isLocal bool
// rs relay state is only for the relay
rs relayState
// cs keeps the client state
cs clientState
bar *progressbar.ProgressBar
// crocFile is the name of the file that is prepared to sent
crocFile string
// crocFileEncrypted is the name of the encrypted file
crocFileEncrypted string
// bothConnected
bothConnected bool
// cleanupTime tells processes to close up
cleanupTime bool
normalFinish bool
}
// Init will initialize the croc relay
func Init() (c *Croc) {
c = new(Croc)
c.TcpPorts = []string{"27030", "27031", "27032", "27033"}
c.Timeout = 3 * time.Hour
c.UseEncryption = true
c.UseCompression = true
c.AllowLocalDiscovery = true
c.CurveType = "p521"
c.WebsocketAddress = "wss://croc3.schollz.com"
c.ServerPort = "8130"
c.rs.Lock()
c.rs.channel = make(map[string]*channelData)
c.rs.ips = make(map[string]string)
c.cs.channel = new(channelData)
c.cs.channel.spin = spinner.New(spinner.CharSets[9], 100*time.Millisecond)
c.rs.Unlock()
c.localIP = getLocalIP()
return
}
func (c *Croc) SetDebug(debug bool) {
if debug {
SetLogLevel("debug")
} else {
SetLogLevel("error")
}
}
type relayState struct {
ips map[string]string
channel map[string]*channelData
sync.RWMutex
}
type clientState struct {
channel *channelData
sync.RWMutex
}
type FileMetaData struct {
Name string
Size int
Hash string
IsDir bool
IsEncrypted bool
IsCompressed bool
DeleteAfterSending bool
}
type channelData struct {
// Relay actions
// Open set to true when trying to open
Open bool `json:"open"`
// Update set to true when updating
Update bool `json:"update"`
// Close set to true when closing:
Close bool `json:"close"`
// Public
// Channel is the name of the channel
Channel string `json:"channel,omitempty"`
// Pake contains the information for
// generating the session key over an insecure channel
Pake *pake.Pake `json:"pake"`
// TransferReady is set by the relaying when both parties have connected
// with their credentials
TransferReady bool `json:"transfer_ready"`
// Ports returns which TCP ports to connect to
Ports []string `json:"ports"`
// Curve is the type of elliptic curve to use
Curve string `json:"curve"`
// FileMetaData is sent after confirmed
EncryptedFileMetaData encryption `json:"encrypted_meta_data"`
// FileReceived specifies that everything was done right
FileReceived bool `json:"file_received"`
// ReadyToRead means that the recipient is ready to read
ReadyToRead bool `json:"ready_to_read"`
// Error is sent if there is an error
Error string `json:"error"`
// Addresses of the sender and recipient, as determined by the relay
Addresses [2]string `json:"addresses"`
// Sent on initialization, specific to a single user
// UUID is sent out only to one person at a time
UUID string `json:"uuid"`
// Role is the role the person will play
Role int `json:"role"`
// Private
// client parameters
// codePhrase uses the first 3 characters to establish a channel, and the rest
// to form the passphrase
codePhrase string
// passPhrase is used to generate a session key
passPhrase string
// sessionKey
sessionKey []byte
// isReady specifies whether the current client
isReady bool
fileReady bool
fileMetaData FileMetaData
notSentMetaData bool
finishedHappy bool
filesReady bool
startTransfer time.Time
transferTime time.Duration
// spin is the spinner for the recipient
spin *spinner.Spinner
waitingForConnection bool
waitingForOther bool
waitingForPake bool
waitingForRecipient bool
// ws is the connection that the client has to the relay
ws *websocket.Conn
// relay parameters
// isopen determine whether or not the channel has been opened
isopen bool
// store a UUID of the parties to prevent other parties from joining
uuids [2]string // 0 is sender, 1 is recipient
// connection information is stored when the clients do connect over TCP
connection map[string][2]net.Conn
// websocket connections
websocketConn [2]*websocket.Conn
// startTime is the time that the channel was opened
startTime time.Time
}
func (cd channelData) String2() string {
cdb, _ := json.Marshal(cd)
return string(cdb)
}

10
src/models/filestats.go Normal file
View File

@ -0,0 +1,10 @@
package models
import "time"
type FileStats struct {
Name string
Size int64
ModTime time.Time
IsDir bool
}

View File

@ -1,60 +0,0 @@
package croc
import (
"bufio"
"fmt"
"os"
"strings"
humanize "github.com/dustin/go-humanize"
)
func promptCodePhrase() string {
return getInput("Enter receive code: ")
}
func promptOkayToRecieve(f FileMetaData) (ok bool) {
overwritingOrReceiving := "Receiving"
if exists(f.Name) {
overwritingOrReceiving = "Overwriting"
}
fileOrFolder := "file"
if f.IsDir {
fileOrFolder = "folder"
}
return "y" == getInput(fmt.Sprintf(
`%s %s (%s) into: %s
ok? (y/N): `,
overwritingOrReceiving,
fileOrFolder,
humanize.Bytes(uint64(f.Size)),
f.Name,
))
}
func showIntro(code string, f FileMetaData) {
fileOrFolder := "file"
if f.IsDir {
fileOrFolder = "folder"
}
fmt.Fprintf(os.Stderr,
`Sending %s %s named '%s'
Code is: %s
On the other computer, please run:
croc %s
`,
humanize.Bytes(uint64(f.Size)),
fileOrFolder,
f.Name,
code,
code,
)
}
func getInput(prompt string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stderr, "%s", prompt)
text, _ := reader.ReadString('\n')
return strings.TrimSpace(text)
}

170
src/recipient/recipient.go Normal file
View File

@ -0,0 +1,170 @@
package recipient
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/schollz/croc/src/compress"
"github.com/schollz/croc/src/crypt"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/models"
"github.com/schollz/croc/src/utils"
"github.com/schollz/pake"
"github.com/schollz/progressbar"
"github.com/tscholl2/siec"
)
var DebugLevel string
// Receive is the async operation to receive a file
func Receive(done chan struct{}, c *websocket.Conn) {
logger.SetLogLevel(DebugLevel)
err := receive(c)
if err != nil {
log.Error(err)
}
done <- struct{}{}
}
func receive(c *websocket.Conn) (err error) {
var fstats models.FileStats
var sessionKey []byte
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte{1, 2, 3}
// initialize recipient Q ("1" indicates recipient)
Q, err := pake.Init(pw, 1, curve, 100*time.Millisecond)
if err != nil {
return
}
step := 0
for {
messageType, message, err := c.ReadMessage()
if err != nil {
return err
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
// Q receives u
log.Debugf("[%d] Q computes k, sends H(k), v back to P", step)
if err := Q.Update(message); err != nil {
return err
}
c.WriteMessage(websocket.BinaryMessage, Q.Bytes())
case 1:
log.Debugf("[%d] Q recieves H(k) from P", step)
if err := Q.Update(message); err != nil {
return err
}
sessionKey, err = Q.SessionKey()
if err != nil {
return err
}
log.Debugf("%x\n", sessionKey)
c.WriteMessage(websocket.BinaryMessage, []byte("ready"))
case 2:
log.Debugf("[%d] recieve file info", step)
err = json.Unmarshal(message, &fstats)
if err != nil {
return err
}
// await file
f, err := os.Create("out")
if err != nil {
return err
}
bytesWritten := 0
bar := progressbar.NewOptions(
int(fstats.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(fstats.Size)),
)
c.WriteMessage(websocket.BinaryMessage, []byte("ready"))
for {
messageType, message, err := c.ReadMessage()
if err != nil {
return err
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
if messageType == websocket.BinaryMessage {
// tell the sender that we recieved this packet
c.WriteMessage(websocket.BinaryMessage, []byte("ok"))
// do decryption
var enc crypt.Encryption
err = json.Unmarshal(message, &enc)
if err != nil {
return err
}
decrypted, err := enc.Decrypt(sessionKey, true)
if err != nil {
return err
}
// do decompression
decompressed := compress.Decompress(decrypted)
// decompressed := decrypted
// write to file
n, err := f.Write(decompressed)
if err != nil {
return err
}
// update the bytes written
bytesWritten += n
// update the progress bar
bar.Add(n)
} else {
// we are finished
// close file
err = f.Close()
if err != nil {
return err
}
// finish bar
bar.Finish()
// check hash
hash256, err := utils.HashFile("out")
if err != nil {
return err
}
// check success hash(myfile) == hash(theirfile)
log.Debugf("got hash: %x", message)
if bytes.Equal(hash256, message) {
c.WriteMessage(websocket.BinaryMessage, []byte("ok"))
return nil
} else {
c.WriteMessage(websocket.BinaryMessage, []byte("not"))
return errors.New("file corrupted")
}
}
}
default:
return fmt.Errorf("unknown step")
}
step++
}
}

View File

@ -1,218 +0,0 @@
package croc
import (
"net"
"strings"
"sync"
"time"
log "github.com/cihub/seelog"
"github.com/pkg/errors"
)
func (c *Croc) startRelay() {
ports := c.TcpPorts
var wg sync.WaitGroup
wg.Add(len(ports))
for _, port := range ports {
go func(port string, wg *sync.WaitGroup) {
defer wg.Done()
log.Debugf("listening on port %s", port)
if err := c.listener(port); err != nil {
log.Error(err)
return
}
}(port, &wg)
}
wg.Wait()
}
func (c *Croc) listener(port string) (err error) {
server, err := net.Listen("tcp", "0.0.0.0:"+port)
if err != nil {
return errors.Wrap(err, "Error listening on :"+port)
}
defer server.Close()
// spawn a new goroutine whenever a client connects
for {
connection, err := server.Accept()
if err != nil {
return errors.Wrap(err, "problem accepting connection")
}
log.Debugf("client %s connected", connection.RemoteAddr().String())
go func(port string, connection net.Conn) {
errCommunication := c.clientCommuncation(port, connection)
if errCommunication != nil {
log.Warnf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
}
}(port, connection)
}
}
func (c *Croc) clientCommuncation(port string, connection net.Conn) (err error) {
var con1, con2 net.Conn
// get the channel and UUID from the client
err = sendMessage("channel and uuid?", connection)
if err != nil {
return
}
channel, err := receiveMessage(connection)
if err != nil {
return
}
uuid, err := receiveMessage(connection)
if err != nil {
return
}
log.Debugf("%s connected to port %s on channel %s and uuid %s", connection.RemoteAddr().String(), port, channel, uuid)
// validate channel and UUID
c.rs.Lock()
if _, ok := c.rs.channel[channel]; !ok {
c.rs.Unlock()
err = errors.Errorf("channel %s does not exist", channel)
return
}
if uuid != c.rs.channel[channel].uuids[0] &&
uuid != c.rs.channel[channel].uuids[1] {
c.rs.Unlock()
err = errors.Errorf("uuid '%s' is invalid", uuid)
return
}
role := 0
if uuid == c.rs.channel[channel].uuids[1] {
role = 1
}
if _, ok := c.rs.channel[channel].connection[port]; !ok {
c.rs.channel[channel].connection[port] = [2]net.Conn{nil, nil}
}
con1 = c.rs.channel[channel].connection[port][0]
con2 = c.rs.channel[channel].connection[port][1]
if role == 0 {
con1 = connection
} else {
con2 = connection
}
log.Debug(c.rs.channel[channel].connection[port])
c.rs.channel[channel].connection[port] = [2]net.Conn{con1, con2}
ports := c.rs.channel[channel].Ports
c.rs.Unlock()
if con1 != nil && con2 != nil {
log.Debugf("beginning the piping")
var wg sync.WaitGroup
wg.Add(1)
// start piping
go func(con1 net.Conn, con2 net.Conn, wg *sync.WaitGroup) {
pipe(con1, con2)
wg.Done()
log.Debug("done piping")
}(con1, con2, &wg)
if port == ports[0] {
// then set transfer ready
c.rs.Lock()
c.rs.channel[channel].TransferReady = true
c.rs.channel[channel].websocketConn[0].WriteJSON(c.rs.channel[channel])
c.rs.channel[channel].websocketConn[1].WriteJSON(c.rs.channel[channel])
c.rs.Unlock()
log.Debugf("sent ready signal")
}
wg.Wait()
log.Debugf("finished transfer")
}
log.Debug("finished client communication")
return
}
func sendMessage(message string, connection net.Conn) (err error) {
message = fillString(message, bufferSize)
_, err = connection.Write([]byte(message))
return
}
func receiveMessage(connection net.Conn) (s string, err error) {
messageByte := make([]byte, bufferSize)
err = connection.SetReadDeadline(time.Now().Add(60 * time.Minute))
if err != nil {
return
}
err = connection.SetDeadline(time.Now().Add(60 * time.Minute))
if err != nil {
return
}
err = connection.SetWriteDeadline(time.Now().Add(60 * time.Minute))
if err != nil {
return
}
_, err = connection.Read(messageByte)
if err != nil {
return
}
s = strings.TrimRight(string(messageByte), ":")
return
}
func fillString(returnString string, toLength int) string {
for {
lengthString := len(returnString)
if lengthString < toLength {
returnString = returnString + ":"
continue
}
break
}
return returnString
}
// chanFromConn creates a channel from a Conn object, and sends everything it
// Read()s from the socket to the channel.
func chanFromConn(conn net.Conn) chan []byte {
c := make(chan []byte)
go func() {
b := make([]byte, bufferSize)
for {
n, err := conn.Read(b)
if n > 0 {
res := make([]byte, n)
// Copy the buffer so it doesn't get changed while read by the recipient.
copy(res, b[:n])
c <- res
}
if err != nil {
c <- nil
break
}
}
}()
return c
}
// pipe creates a full-duplex pipe between the two sockets and
// transfers data from one to the other.
func pipe(conn1 net.Conn, conn2 net.Conn) {
chan1 := chanFromConn(conn1)
chan2 := chanFromConn(conn2)
for {
select {
case b1 := <-chan1:
if b1 == nil {
return
}
conn2.Write(b1)
case b2 := <-chan2:
if b2 == nil {
return
}
conn1.Write(b2)
}
}
}

116
src/relay/conn.go Normal file
View File

@ -0,0 +1,116 @@
package relay
import (
"net/http"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 6000 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 1024 * 1024
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024 * 1024,
WriteBufferSize: 1024 * 1024,
}
// connection is an middleman between the websocket connection and the hub.
type connection struct {
// The websocket connection.
ws *websocket.Conn
// Buffered channel of outbound messages.
send chan messageChannel
}
type messageChannel struct {
data []byte
messageType int
}
// readPump pumps messages from the websocket connection to the hub.
func (s subscription) readPump() {
c := s.conn
defer func() {
h.unregister <- s
c.ws.Close()
}()
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
messageType, msg, err := c.ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Errorf("error: %v", err)
}
break
}
h.broadcast <- message{messageChannel{msg, messageType}, s.room, c.ws.RemoteAddr().String()}
}
}
// write writes a message with the given message type and payload.
func (c *connection) write(mt int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(mt, payload)
}
// writePump pumps messages from the hub to the websocket connection.
func (s *subscription) writePump() {
c := s.conn
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.ws.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.write(websocket.CloseMessage, []byte{})
return
}
if err := c.write(message.messageType, message.data); err != nil {
return
}
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
// serveWs handles websocket requests from the peer.
func serveWs(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.ErrorStr(err)
}
vals := r.URL.Query()
room := "default"
rooms, ok := vals["room"]
if ok {
room = rooms[0]
}
c := &connection{send: make(chan messageChannel, 256), ws: ws}
s := subscription{c, room}
h.register <- s
go s.writePump()
s.readPump()
}

96
src/relay/hub.go Normal file
View File

@ -0,0 +1,96 @@
package relay
import (
"sync"
log "github.com/cihub/seelog"
)
type message struct {
msg messageChannel
room string
remoteOrigin string
}
type subscription struct {
conn *connection
room string
}
// hub maintains the set of active connections and broadcasts messages to the
// connections.
type hub struct {
// Registered connections.
rooms roomMap
// Inbound messages from the connections.
broadcast chan message
// Register requests from the connections.
register chan subscription
// Unregister requests from connections.
unregister chan subscription
}
type roomMap struct {
rooms map[string]map[*connection]bool
sync.Mutex
}
var h = hub{
broadcast: make(chan message),
register: make(chan subscription),
unregister: make(chan subscription),
rooms: roomMap{rooms: make(map[string]map[*connection]bool)},
}
func (h *hub) run() {
for {
select {
case s := <-h.register:
log.Debugf("adding connection to %s", s.room)
h.rooms.Lock()
connections := h.rooms.rooms[s.room]
if connections == nil {
connections = make(map[*connection]bool)
h.rooms.rooms[s.room] = connections
}
h.rooms.rooms[s.room][s.conn] = true
if len(h.rooms.rooms) > 2 {
// if more than three, close all of them
for connection := range h.rooms.rooms[s.room] {
close(connection.send)
}
delete(h.rooms.rooms, s.room)
}
h.rooms.Unlock()
case s := <-h.unregister:
// if one leaves, close all of them
h.rooms.Lock()
for connection := range h.rooms.rooms[s.room] {
close(connection.send)
}
delete(h.rooms.rooms, s.room)
h.rooms.Unlock()
case m := <-h.broadcast:
h.rooms.Lock()
connections := h.rooms.rooms[m.room]
for c := range connections {
if c.ws.RemoteAddr().String() == m.remoteOrigin {
continue
}
select {
case c.send <- m.msg:
default:
close(c.send)
delete(connections, c)
if len(connections) == 0 {
delete(h.rooms.rooms, m.room)
}
}
}
h.rooms.Unlock()
}
}
}

23
src/relay/relay.go Normal file
View File

@ -0,0 +1,23 @@
package relay
import (
"net/http"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/logger"
)
var DebugLevel string
// Run is the async operation for running a server
func Run(port string) (err error) {
logger.SetLogLevel(DebugLevel)
go h.run()
log.Debug("running")
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(w, r)
})
err = http.ListenAndServe(":"+port, nil)
return
}

158
src/sender/sender.go Normal file
View File

@ -0,0 +1,158 @@
package sender
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/croc/src/compress"
"github.com/schollz/croc/src/crypt"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/models"
"github.com/schollz/croc/src/utils"
"github.com/schollz/pake"
"github.com/schollz/progressbar"
"github.com/tscholl2/siec"
)
var DebugLevel string
// Send is the async call to send data
func Send(done chan struct{}, c *websocket.Conn, fname string) {
logger.SetLogLevel(DebugLevel)
err := send(c, fname)
if err != nil {
log.Error(err)
}
done <- struct{}{}
}
func send(c *websocket.Conn, fname string) (err error) {
var f *os.File
var fstats models.FileStats
var sessionKey []byte
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte{1, 2, 3}
// initialize sender P ("0" indicates sender)
P, err := pake.Init(pw, 0, curve, 100*time.Millisecond)
if err != nil {
return
}
step := 0
for {
messageType, message, errRead := c.ReadMessage()
if errRead != nil {
return errRead
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
log.Debugf("[%d] first, P sends u to Q", step)
c.WriteMessage(websocket.BinaryMessage, P.Bytes())
case 1:
// P recieves H(k),v from Q
log.Debugf("[%d] P computes k, H(k), sends H(k) to Q", step)
if err := P.Update(message); err != nil {
return err
}
c.WriteMessage(websocket.BinaryMessage, P.Bytes())
sessionKey, _ = P.SessionKey()
// check(err)
log.Debugf("%x\n", sessionKey)
case 2:
log.Debugf("[%d] recipient declares readiness for file info", step)
if !bytes.Equal(message, []byte("ready")) {
return errors.New("recipient refused file")
}
f, err = os.Open(fname)
if err != nil {
return
}
fstat, err := f.Stat()
if err != nil {
return err
}
fstats = models.FileStats{fstat.Name(), fstat.Size(), fstat.ModTime(), fstat.IsDir()}
fstatsBytes, err := json.Marshal(fstats)
if err != nil {
return err
}
log.Debugf("%s\n", fstatsBytes)
c.WriteMessage(websocket.BinaryMessage, fstatsBytes)
case 3:
log.Debugf("[%d] recipient declares readiness for file data", step)
if !bytes.Equal(message, []byte("ready")) {
return errors.New("recipient refused file")
}
// send file, compure hash simultaneously
buffer := make([]byte, 1024*512)
bar := progressbar.NewOptions(
int(fstats.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(fstats.Size)),
)
for {
bytesread, err := f.Read(buffer)
bar.Add(bytesread)
if bytesread > 0 {
// do compression
compressedBytes := compress.Compress(buffer[:bytesread])
// compressedBytes := buffer[:bytesread]
// do encryption
enc := crypt.Encrypt(compressedBytes, sessionKey, true)
encBytes, err := json.Marshal(enc)
if err != nil {
return err
}
// send message
err = c.WriteMessage(websocket.BinaryMessage, encBytes)
if err != nil {
err = errors.Wrap(err, "problem writing message")
return err
}
// wait for ok
c.ReadMessage()
}
if err != nil {
if err != io.EOF {
fmt.Println(err)
}
break
}
}
bar.Finish()
log.Debug("send hash to finish file")
fileHash, err := utils.HashFile(fname)
if err != nil {
return err
}
c.WriteMessage(websocket.TextMessage, fileHash)
case 4:
log.Debugf("[%d] determing whether it went ok", step)
if bytes.Equal(message, []byte("ok")) {
log.Debug("file transfered successfully")
} else {
return errors.New("file not transfered succesfully")
}
default:
return fmt.Errorf("unknown step")
}
step++
}
}

View File

@ -1,291 +0,0 @@
package croc
import (
"fmt"
"net"
"net/http"
"time"
log "github.com/cihub/seelog"
"github.com/frankenbeanies/uuid4"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/pake"
)
// startServer initiates the server which listens for websocket connections
func (c *Croc) startServer() (err error) {
// start cleanup on dangling channels
go c.channelCleanup()
var upgrader = websocket.Upgrader{} // use default options
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// check if HEAD request
if r.Method == "HEAD" {
fmt.Fprintf(w, "ok")
return
}
// incoming websocket request
log.Debugf("connecting remote addr: %+v", r.RemoteAddr)
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Debugf("err in websocket: %s", err.Error())
fmt.Fprintf(w, "?")
return
}
address := r.RemoteAddr
if _, ok := r.Header["X-Forwarded-For"]; ok {
address = r.Header["X-Forwarded-For"][0]
}
if _, ok := r.Header["X-Real-Ip"]; ok {
address = r.Header["X-Real-Ip"][0]
}
log.Debugf("ws address: %s", ws.RemoteAddr().String())
log.Debug("getting lock")
c.rs.Lock()
c.rs.ips[ws.RemoteAddr().String()] = address
c.rs.Unlock()
log.Debugf("connecting remote addr: %s", address)
if err != nil {
log.Error("upgrade:", err)
return
}
defer ws.Close()
var channel string
for {
log.Debug("waiting for next message")
var cd channelData
err := ws.ReadJSON(&cd)
if err != nil {
if _, ok := err.(*websocket.CloseError); ok {
// on forced close, delete the channel
log.Debug("closed channel")
c.closeChannel(channel)
} else {
log.Debugf("read:", err)
}
break
}
channel, err = c.processPayload(ws, cd)
if err != nil {
// if error, send the error back and then delete the channel
log.Warn("problem processing payload %+v: %s", cd, err.Error())
ws.WriteJSON(channelData{Error: err.Error()})
c.closeChannel(channel)
return
}
}
})
log.Debugf("listening on port %s", c.ServerPort)
err = http.ListenAndServe(":"+c.ServerPort, nil)
return
}
func (c *Croc) updateChannel(cd channelData) (err error) {
c.rs.Lock()
defer c.rs.Unlock()
// determine if channel is invalid
if _, ok := c.rs.channel[cd.Channel]; !ok {
err = errors.Errorf("channel '%s' does not exist", cd.Channel)
return
}
// determine if UUID is invalid for channel
if cd.UUID != c.rs.channel[cd.Channel].uuids[0] &&
cd.UUID != c.rs.channel[cd.Channel].uuids[1] {
err = errors.Errorf("uuid '%s' is invalid", cd.UUID)
return
}
// update each
c.rs.channel[cd.Channel].Error = cd.Error
c.rs.channel[cd.Channel].FileReceived = cd.FileReceived
c.rs.channel[cd.Channel].EncryptedFileMetaData = cd.EncryptedFileMetaData
c.rs.channel[cd.Channel].ReadyToRead = cd.ReadyToRead
if c.rs.channel[cd.Channel].Pake == nil {
c.rs.channel[cd.Channel].Pake = new(pake.Pake)
}
c.rs.channel[cd.Channel].Pake.HkA = cd.Pake.HkA
c.rs.channel[cd.Channel].Pake.HkB = cd.Pake.HkB
c.rs.channel[cd.Channel].Pake.Role = cd.Pake.Role
c.rs.channel[cd.Channel].Pake.Uᵤ = cd.Pake.Uᵤ
c.rs.channel[cd.Channel].Pake.Uᵥ = cd.Pake.Uᵥ
c.rs.channel[cd.Channel].Pake.Vᵤ = cd.Pake.Vᵤ
c.rs.channel[cd.Channel].Pake.Vᵥ = cd.Pake.Vᵥ
c.rs.channel[cd.Channel].Pake.Xᵤ = cd.Pake.Xᵤ
c.rs.channel[cd.Channel].Pake.Xᵥ = cd.Pake.Xᵥ
c.rs.channel[cd.Channel].Pake.Yᵤ = cd.Pake.Yᵤ
c.rs.channel[cd.Channel].Pake.Yᵥ = cd.Pake.Yᵥ
if cd.Addresses[0] != "" {
c.rs.channel[cd.Channel].Addresses[0] = cd.Addresses[0]
}
if cd.Addresses[1] != "" {
c.rs.channel[cd.Channel].Addresses[1] = cd.Addresses[1]
}
return
}
func (c *Croc) joinChannel(ws *websocket.Conn, cd channelData) (channel string, err error) {
log.Debugf("joining channel %s", ws.RemoteAddr().String())
c.rs.Lock()
defer c.rs.Unlock()
// determine if sender or recipient
if cd.Role != 0 && cd.Role != 1 {
err = errors.Errorf("no such role of %d", cd.Role)
return
}
// determine channel
if cd.Channel == "" {
// TODO:
// find an empty channel
cd.Channel = "chou"
}
if _, ok := c.rs.channel[cd.Channel]; ok {
// channel is not empty
if c.rs.channel[cd.Channel].uuids[cd.Role] != "" {
err = errors.Errorf("channel '%s' already occupied by role %d", cd.Channel, cd.Role)
return
}
}
log.Debug("creating new channel")
if _, ok := c.rs.channel[cd.Channel]; !ok {
c.rs.channel[cd.Channel] = new(channelData)
c.rs.channel[cd.Channel].connection = make(map[string][2]net.Conn)
}
channel = cd.Channel
// assign UUID for the role in the channel
c.rs.channel[cd.Channel].uuids[cd.Role] = uuid4.New().String()
log.Debugf("(%s) %s has joined as role %d", cd.Channel, c.rs.channel[cd.Channel].uuids[cd.Role], cd.Role)
// send Channel+UUID back to the current person
err = ws.WriteJSON(channelData{
Channel: cd.Channel,
UUID: c.rs.channel[cd.Channel].uuids[cd.Role],
Role: cd.Role,
})
if err != nil {
return
}
// if channel is not open, set initial parameters
if !c.rs.channel[cd.Channel].isopen {
c.rs.channel[cd.Channel].isopen = true
c.rs.channel[cd.Channel].Ports = c.TcpPorts
c.rs.channel[cd.Channel].startTime = time.Now()
c.rs.channel[cd.Channel].Curve = "p256"
}
c.rs.channel[cd.Channel].websocketConn[cd.Role] = ws
// assign the name
c.rs.channel[cd.Channel].Addresses[cd.Role] = c.rs.ips[ws.RemoteAddr().String()]
log.Debugf("assigned role %d in channel '%s'", cd.Role, cd.Channel)
return
}
// closeChannel will shut down current open websockets and delete the channel information
func (c *Croc) closeChannel(channel string) {
c.rs.Lock()
defer c.rs.Unlock()
// check if channel exists
if _, ok := c.rs.channel[channel]; !ok {
return
}
// close open connections
for _, wsConn := range c.rs.channel[channel].websocketConn {
if wsConn != nil {
wsConn.Close()
delete(c.rs.ips, wsConn.RemoteAddr().String())
}
}
// delete
delete(c.rs.channel, channel)
}
func (c *Croc) processPayload(ws *websocket.Conn, cd channelData) (channel string, err error) {
log.Debugf("processing payload from %s", ws.RemoteAddr().String())
channel = cd.Channel
// if the request is to close, delete the channel
if cd.Close {
log.Debugf("closing channel %s", cd.Channel)
c.closeChannel(cd.Channel)
return
}
// if request is to Open, try to open
if cd.Open {
channel, err = c.joinChannel(ws, cd)
if err != nil {
return
}
}
// check if open, otherwise return error
c.rs.Lock()
if _, ok := c.rs.channel[channel]; ok {
if !c.rs.channel[channel].isopen {
err = errors.Errorf("channel %s is not open, need to open first", channel)
c.rs.Unlock()
return
}
}
c.rs.Unlock()
// if the request is to Update, then update the state
if cd.Update {
// update
err = c.updateChannel(cd)
if err != nil {
return
}
}
// TODO:
// relay state logic here
// send out the data to both sender + receiver each time
c.rs.Lock()
if _, ok := c.rs.channel[channel]; ok {
for role, wsConn := range c.rs.channel[channel].websocketConn {
if wsConn == nil {
continue
}
log.Debugf("writing latest data %+v to %d", c.rs.channel[channel].String2(), role)
err = wsConn.WriteJSON(c.rs.channel[channel])
if err != nil {
log.Debugf("problem writing to role %d: %s", role, err.Error())
}
}
}
c.rs.Unlock()
return
}
func (c *Croc) channelCleanup() {
maximumWait := 3 * time.Hour
for {
c.rs.Lock()
keys := make([]string, len(c.rs.channel))
i := 0
for key := range c.rs.channel {
keys[i] = key
i++
}
channelsToDelete := []string{}
for _, key := range keys {
if time.Since(c.rs.channel[key].startTime) > maximumWait {
channelsToDelete = append(channelsToDelete, key)
}
}
c.rs.Unlock()
for _, channel := range channelsToDelete {
log.Debugf("channel %s has exceeded time, deleting", channel)
c.closeChannel(channel)
}
time.Sleep(1 * time.Minute)
}
}

View File

@ -1,135 +0,0 @@
<p align="center">
<img
src="https://user-images.githubusercontent.com/6550035/31846899-2b8a7034-b5cf-11e7-9643-afe552226c59.png"
width="100%" border="0" alt="croc">
<br>
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-β2.0.0-brightgreen.svg?style=flat-square" alt="Version"></a>
<a href="https://saythanks.io/to/schollz"><img src="https://img.shields.io/badge/Say%20Thanks-!-yellow.svg?style=flat-square" alt="Go Report Card"></a>
</p>
<p align="center">Easily and securely transfer stuff from one computer to another.</p>
*croc* allows any two computers to directly and securely transfer files and folders. When sending a file, *croc* generates a random code phrase which must be shared with the recipient so they can receive the file. The code phrase encrypts all data and metadata and also serves to authorize the connection between the two computers in a intermediary relay. The relay connects the TCP ports between the two computers and does not store any information (and all information passing through it is encrypted).
## New version released June 24th, 2018 - please upgrade if you are using the public relay.
I hear you asking, *Why another open-source peer-to-peer file transfer utilities?* [There](https://github.com/cowbell/sharedrop) [are](https://github.com/webtorrent/instant.io) [great](https://github.com/kern/filepizza) [tools](https://github.com/warner/magic-wormhole) [that](https://github.com/zerotier/toss) [already](https://github.com/ipfs/go-ipfs) [do](https://github.com/zerotier/toss) [this](https://github.com/nils-werner/zget). But, after review, [I found it was useful to make another](https://schollz.github.io/sending-a-file/). Namely, *croc* has no dependencies (just [download a binary and run](https://github.com/schollz/croc/releases/latest)), it works on any operating system, and its blazingly fast because it does parallel transfer over multiple TCP ports.
# Example
_These two gifs should run in sync if you force-reload (Ctl+F5)_
**Sender:**
![send](https://raw.githubusercontent.com/schollz/croc/master/logo/sender.gif)
**Receiver:**
![receive](https://raw.githubusercontent.com/schollz/croc/master/logo/receiver.gif)
**Sender:**
```
$ croc -send some-file-or-folder
Sending 4.4 MB file named 'some-file-or-folder'
Code is: cement-galaxy-alpha
Your public key: F9Ky3WU2yG4y7KKppF4KnEhrmtY9ZlTsEMkqXfC1
Send to public key: xHVRlQ2Yp6otQXBoLMcUJmmtNPXl7z8tOf019sGw
ok? (y/n): y
Sending (->[1]63982)..
89% |███████████████████████████████████ | [12s:1s]
File sent (2.6 MB/s)
```
**Receiver:**
```
$ croc
Your public key: xHVRlQ2Yp6otQXBoLMcUJmmtNPXl7z8tOf019sGw
Enter receive code: cement-galaxy-alpha
Receiving file (4.4 MB) into: some-file-or-folder
from public key: F9Ky3WU2yG4y7KKppF4KnEhrmtY9ZlTsEMkqXfC1
ok? (y/n): y
Receiving (<-[1]63975)..
97% |██████████████████████████████████████ | [13s:0s]
Received file written to some-file-or-folder (2.6 MB/s)
```
Note, by default, you don't need any arguments for receiving! This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
## Using *croc* in pipes
You can easily use *croc* in pipes when you need to send data through stdin or get data from stdout.
**Sender:**
```
$ cat some_file_or_folder | croc
```
In this case *croc* will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789".
**Receiver:**
```
$ croc --code code-phrase --yes --stdout | more
```
Here the reciever specified the code (`--code`) so it will not be prompted, and also specified `--yes` so the file will be automatically accepted. The output goes to stdout when flagged with `--stdout`.
# Install
[Download the latest release for your system](https://github.com/schollz/croc/releases/latest).
Or, you can [install Go](https://golang.org/dl/) and build from source with `go get github.com/schollz/croc`.
# How does it work?
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit and design. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transfering of files and folders through a intermediary relay that connects the TCP ports between the two computers. The standard relay is on a public IP address (default `cowyo.com`), but before transmitting the file the two instances of *croc* send out UDP broadcasts to determine if they are both on the local network, and use a local relay instead of the cloud relay in the case that they are both local.
The code phrase for transfering files is just three words which are 16 random bits that are [menemonic encoded](http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/). This code phrase is hashed using sha256 and sent to the relay which maps that hashed code phrase to that connection. When the relay finds a matching code phrase hash for both the receiver and the sender (i.e. they both have the same code phrase), then the sender transmits the encrypted metadata to the receiver through the relay. Then the receiver decrypts and reviews the metadata (file name, size), and chooses whether to consent to the transfer.
After the receiver consents to the transfer, the sender transmits encrypted data through the relay. The relay setups up [Go channels](https://golang.org/doc/effective_go.html?h=chan#channels) for each connection which pipes all the data incoming from that sender's connection out to the receiver's connection. After the transmission the channels are destroyed and all the connection and meta data information is wiped from the relay server. The encrypted file data never is stored on the relay.
**Encryption**
Encryption uses AES-256 with a pbkdf2 derived key (see [RFC2898](http://www.ietf.org/rfc/rfc2898.txt)) where the code phrase shared between the sender and receiver is used as the passphrase. For each of the two encrypted data blocks (metadata stored on relay server, and file data transmitted), a random 8-byte salt is used and a IV is generated according to [NIST Recommendation for Block ciphers, Section 8.2](http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf).
**Decryption**
On the receiver's computer, each piece of received encrypted data is written to a separate file. These files are concatenated and then decrypted. The hash of the decrypted file is then checked against the hash transmitted from the sender (part of the meta data block).
## Run your own relay
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `cowyo.com`, which has a 30-day uptime of 99.989% ([click here to check the current status of the public relay](https://stats.uptimerobot.com/lOwJYIgRm)).
You can also run your own relay, it is very easy. On your server, `your-server.com`, just run
```
$ croc -relay
```
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server. Make sure to open up TCP ports 27001-27009.
# Contribute
I am awed by all the [great contributions](#acknowledgements) made! If you feel like contributing, in any way, by all means you can send an Issue, a PR, ask a question, or tweet me ([@yakczar](http://ctt.ec/Rq054)).
# License
MIT
# Acknowledgements
Thanks...
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu)!

View File

@ -1 +0,0 @@
Some simple text to see if it works

View File

@ -1 +0,0 @@
More data to see if it 100% works

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,216 +0,0 @@
package croc
import (
"crypto/elliptic"
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"math"
"net"
"os"
"strconv"
"github.com/schollz/pake"
"github.com/pkg/errors"
"github.com/tscholl2/siec"
)
// catFiles copies data from n files to a single one and removes source files
// if Debug mode is set to false
func catFiles(files []string, outfile string, remove bool) error {
finished, err := os.Create(outfile)
if err != nil {
return errors.Wrap(err, "CatFiles create: ")
}
defer finished.Close()
for _, file := range files {
fh, err := os.Open(file)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("CatFiles open %v: ", file))
}
_, err = io.Copy(finished, fh)
if err != nil {
fh.Close()
return errors.Wrap(err, "CatFiles copy: ")
}
fh.Close()
if remove {
os.Remove(file)
}
}
return nil
}
// splitFile creates a bunch of smaller files with the data from source splited into them
func splitFile(fileName string, numPieces int) (err error) {
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return err
}
bytesPerPiece := int(math.Ceil(float64(fi.Size()) / float64(numPieces)))
buf := make([]byte, bytesPerPiece)
for i := 0; i < numPieces; i++ {
out, err := os.Create(fileName + "." + strconv.Itoa(i))
if err != nil {
return err
}
n, err := file.Read(buf)
out.Write(buf[:n])
out.Close()
if err == io.EOF {
break
}
}
return nil
}
func getCurve(s string) (curve pake.EllipticCurve) {
switch s {
case "p224":
curve = elliptic.P224()
case "p256":
curve = elliptic.P256()
case "p384":
curve = elliptic.P384()
case "p521":
curve = elliptic.P521()
default:
curve = siec.SIEC255()
}
return
}
// copyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherise, attempt to create a hard link
// between the two files. If that fail, copy the file contents from src to dst.
func copyFile(src, dst string) (err error) {
sfi, err := os.Stat(src)
if err != nil {
return
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories,
// symlinks, devices, etc.)
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return
}
}
if err = os.Link(src, dst); err == nil {
return
}
err = copyFileContents(src, dst)
return
}
// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
// hashFile does a md5 hash on the file
// from https://golang.org/pkg/crypto/md5/#example_New_file
func hashFile(filename string) (hash string, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
h := md5.New()
if _, err = io.Copy(h, f); err != nil {
return
}
hash = fmt.Sprintf("%x", h.Sum(nil))
return
}
// fileSize returns the size of a file
func fileSize(filename string) (int, error) {
fi, err := os.Stat(filename)
if err != nil {
return -1, err
}
size := int(fi.Size())
return size, nil
}
// getLocalIP returns the local ip address
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
bestIP := ""
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return bestIP
}
// exists reports whether the named file or directory exists.
func exists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func tempFileName(prefix string) string {
var f *os.File
f, _ = ioutil.TempFile(".", prefix)
name := f.Name()
f.Close()
os.Remove(name)
return name
}

23
src/utils/hash.go Normal file
View File

@ -0,0 +1,23 @@
package utils
import (
"crypto/md5"
"io"
"os"
)
func HashFile(fname string) (hash256 []byte, err error) {
f, err := os.Open("file.txt")
if err != nil {
return
}
defer f.Close()
h := md5.New()
if _, err = io.Copy(h, f); err != nil {
return
}
hash256 = h.Sum(nil)
return
}

View File

@ -1,119 +0,0 @@
package croc
import (
"os"
"testing"
)
func TestSplitFile(t *testing.T) {
err := splitFile("testing_data/README.md", 3)
if err != nil {
t.Error(err)
}
os.Remove("testing_data/README.md.0")
os.Remove("testing_data/README.md.1")
}
func TestFileSize(t *testing.T) {
t.Run("File is ok ", func(t *testing.T) {
_, err := fileSize("testing_data/README.md")
if err != nil {
t.Errorf("should pass with no error, got: %v", err)
}
})
t.Run("File does not exist", func(t *testing.T) {
s, err := fileSize("testing_data/someStrangeFile")
if err == nil {
t.Error("should return an error")
}
if s != -1 {
t.Errorf("size should be -1, got: %d", s)
}
})
}
func TestHashFile(t *testing.T) {
t.Run("Hash created successfully", func(t *testing.T) {
h, err := hashFile("testing_data/README.md")
if err != nil {
t.Errorf("should pass with no error, got: %v", err)
}
if len(h) != 32 {
t.Errorf("invalid md5 hash, length should be 32 got: %d", len(h))
}
})
t.Run("File does not exist", func(t *testing.T) {
h, err := hashFile("testing_data/someStrangeFile")
if err == nil {
t.Error("should return an error")
}
if len(h) > 0 {
t.Errorf("hash length should be 0, got: %d", len(h))
}
if h != "" {
t.Errorf("hash should be empty string, got: %s", h)
}
})
}
func TestCopyFileContents(t *testing.T) {
t.Run("Content copied successfully", func(t *testing.T) {
f1 := "testing_data/README.md"
f2 := "testing_data/CopyOfREADME.md"
err := copyFileContents(f1, f2)
if err != nil {
t.Errorf("should pass with no error, got: %v", err)
}
f1Length, err := fileSize(f1)
if err != nil {
t.Errorf("can't get file nr1 size: %v", err)
}
f2Length, err := fileSize(f2)
if err != nil {
t.Errorf("can't get file nr2 size: %v", err)
}
if f1Length != f2Length {
t.Errorf("size of both files should be same got: file1: %d file2: %d", f1Length, f2Length)
}
os.Remove(f2)
})
}
func TestCopyFile(t *testing.T) {
t.Run("Files copied successfully", func(t *testing.T) {
f1 := "testing_data/README.md"
f2 := "testing_data/CopyOfREADME.md"
err := copyFile(f1, f2)
if err != nil {
t.Errorf("should pass with no error, got: %v", err)
}
f1Length, err := fileSize(f1)
if err != nil {
t.Errorf("can't get file nr1 size: %v", err)
}
f2Length, err := fileSize(f2)
if err != nil {
t.Errorf("can't get file nr2 size: %v", err)
}
if f1Length != f2Length {
t.Errorf("size of both files should be same got: file1: %d file2: %d", f1Length, f2Length)
}
os.Remove(f2)
})
}
func TestCatFiles(t *testing.T) {
t.Run("CatFiles passing", func(t *testing.T) {
files := []string{"testing_data/catFile1.txt", "testing_data/catFile2.txt"}
err := catFiles(files, "testing_data/CatFile.txt", false)
if err != nil {
t.Errorf("should pass with no error, got: %v", err)
}
if _, err := os.Stat("testing_data/CatFile.txt"); os.IsNotExist(err) {
t.Errorf("file were not created: %v", err)
}
os.Remove("testing_data/CatFile.txt")
})
}

View File

@ -1,183 +0,0 @@
package croc
import (
"archive/zip"
"compress/flate"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
log "github.com/cihub/seelog"
)
func unzipFile(src, dest string) (err error) {
r, err := zip.OpenReader(src)
if err != nil {
return
}
defer r.Close()
for _, f := range r.File {
var rc io.ReadCloser
rc, err = f.Open()
if err != nil {
return
}
defer rc.Close()
// Store filename/path for returning and using later on
fpath := filepath.Join(dest, f.Name)
log.Debugf("unzipping %s", fpath)
fpath = filepath.FromSlash(fpath)
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(fpath, os.ModePerm)
} else {
// Make File
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return
}
var outFile *os.File
outFile, err = os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return
}
_, err = io.Copy(outFile, rc)
// Close the file without defer to close before next iteration of loop
outFile.Close()
if err != nil {
return
}
}
}
if err == nil {
log.Debugf("unzipped %s to %s", src, dest)
}
return
}
func zipFile(fname string, compress bool) (writtenFilename string, err error) {
log.Debugf("zipping %s with compression? %v", fname, compress)
pathtofile, filename := filepath.Split(fname)
curdir, err := os.Getwd()
if err != nil {
log.Error(err)
return
}
curdir, err = filepath.Abs(curdir)
if err != nil {
log.Error(err)
return
}
log.Debugf("current directory: %s", curdir)
newfile, err := ioutil.TempFile(curdir, "croc-zipped")
if err != nil {
log.Error(err)
return
}
writtenFilename = newfile.Name()
defer newfile.Close()
defer os.Chdir(curdir)
log.Debugf("changing dir to %s", pathtofile)
os.Chdir(pathtofile)
zipWriter := zip.NewWriter(newfile)
zipWriter.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
if compress {
return flate.NewWriter(out, flate.BestSpeed)
} else {
return flate.NewWriter(out, flate.NoCompression)
}
})
defer zipWriter.Close()
zipfile, err := os.Open(filename)
if err != nil {
log.Error(err)
return "", err
}
defer zipfile.Close()
// Get the file information
info, err := zipfile.Stat()
if err != nil {
log.Error(err)
return
}
// write header informaiton
header, err := zip.FileInfoHeader(info)
if err != nil {
log.Error(err)
return
}
var writer io.Writer
if info.IsDir() {
baseDir := filename
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
if baseDir != "" {
header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, baseDir))
}
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
header.Name = filepath.ToSlash(header.Name)
writer, err = zipWriter.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
})
} else {
writer, err = zipWriter.CreateHeader(header)
if err != nil {
log.Error(err)
return
}
_, err = io.Copy(writer, zipfile)
if err != nil {
log.Error(err)
return
}
}
log.Debugf("wrote zip file to %s", writtenFilename)
return
}

View File

@ -1,21 +0,0 @@
package croc
import (
"os"
"testing"
log "github.com/cihub/seelog"
"github.com/stretchr/testify/assert"
)
func TestZip(t *testing.T) {
defer log.Flush()
writtenFilename, err := zipFile("../README.md", false)
assert.Nil(t, err)
defer os.Remove(writtenFilename)
err = unzipFile(writtenFilename, ".")
assert.Nil(t, err)
assert.True(t, exists("README.md"))
os.RemoveAll("README.md")
}