From b9dad87526ef67ace9e6ebe890955d85481adb33 Mon Sep 17 00:00:00 2001 From: Zack Scholl Date: Fri, 21 Sep 2018 20:17:57 -0700 Subject: [PATCH] completely rewrite --- go.mod | 20 +- main.go | 242 +++++------ src/api.go | 186 -------- src/cleanup.go | 31 -- src/client.go | 620 --------------------------- src/compress/compress.go | 36 ++ src/croc/croc.go | 34 ++ src/crypt/crypt.go | 61 +++ src/crypto.go | 131 ------ src/crypto_test.go | 43 -- src/files.go | 247 ----------- src/{logging.go => logger/logger.go} | 2 +- src/models.go | 201 --------- src/models/filestats.go | 10 + src/prompts.go | 60 --- src/recipient/recipient.go | 170 ++++++++ src/relay.go | 218 ---------- src/relay/conn.go | 116 +++++ src/relay/hub.go | 96 +++++ src/relay/relay.go | 23 + src/sender/sender.go | 158 +++++++ src/server.go | 291 ------------- src/testing_data/README.md | 135 ------ src/testing_data/catFile1.txt | 1 - src/testing_data/catFile2.txt | 1 - src/testing_data/recipient.gif | Bin 47013 -> 0 bytes src/testing_data/sender.gif | Bin 50974 -> 0 bytes src/utils.go | 216 ---------- src/utils/hash.go | 23 + src/utils_test.go | 119 ----- src/zip.go | 183 -------- src/zip_test.go | 21 - 32 files changed, 850 insertions(+), 2845 deletions(-) delete mode 100644 src/api.go delete mode 100644 src/cleanup.go delete mode 100644 src/client.go create mode 100644 src/compress/compress.go create mode 100644 src/croc/croc.go create mode 100644 src/crypt/crypt.go delete mode 100644 src/crypto.go delete mode 100644 src/crypto_test.go delete mode 100644 src/files.go rename src/{logging.go => logger/logger.go} (98%) delete mode 100644 src/models.go create mode 100644 src/models/filestats.go delete mode 100644 src/prompts.go create mode 100644 src/recipient/recipient.go delete mode 100644 src/relay.go create mode 100644 src/relay/conn.go create mode 100644 src/relay/hub.go create mode 100644 src/relay/relay.go create mode 100644 src/sender/sender.go delete mode 100644 src/server.go delete mode 100644 src/testing_data/README.md delete mode 100644 src/testing_data/catFile1.txt delete mode 100644 src/testing_data/catFile2.txt delete mode 100644 src/testing_data/recipient.gif delete mode 100644 src/testing_data/sender.gif delete mode 100644 src/utils.go create mode 100644 src/utils/hash.go delete mode 100644 src/utils_test.go delete mode 100644 src/zip.go delete mode 100644 src/zip_test.go diff --git a/go.mod b/go.mod index 84bdc97..342ee8f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/main.go b/main.go index 106f5f8..a33ce79 100644 --- a/main.go +++ b/main.go @@ -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() +// } diff --git a/src/api.go b/src/api.go deleted file mode 100644 index a9b8b6d..0000000 --- a/src/api.go +++ /dev/null @@ -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) -} diff --git a/src/cleanup.go b/src/cleanup.go deleted file mode 100644 index 9c1fa58..0000000 --- a/src/cleanup.go +++ /dev/null @@ -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() -} diff --git a/src/client.go b/src/client.go deleted file mode 100644 index e19d68b..0000000 --- a/src/client.go +++ /dev/null @@ -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 -} diff --git a/src/compress/compress.go b/src/compress/compress.go new file mode 100644 index 0000000..bd1a1b7 --- /dev/null +++ b/src/compress/compress.go @@ -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() +} diff --git a/src/croc/croc.go b/src/croc/croc.go new file mode 100644 index 0000000..a1632d6 --- /dev/null +++ b/src/croc/croc.go @@ -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 +} diff --git a/src/crypt/crypt.go b/src/crypt/crypt.go new file mode 100644 index 0000000..6a6b0a8 --- /dev/null +++ b/src/crypt/crypt.go @@ -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 +} diff --git a/src/crypto.go b/src/crypto.go deleted file mode 100644 index 8288928..0000000 --- a/src/crypto.go +++ /dev/null @@ -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 -} diff --git a/src/crypto_test.go b/src/crypto_test.go deleted file mode 100644 index 24df5d0..0000000 --- a/src/crypto_test.go +++ /dev/null @@ -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") -// } diff --git a/src/files.go b/src/files.go deleted file mode 100644 index a055e5f..0000000 --- a/src/files.go +++ /dev/null @@ -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 -} diff --git a/src/logging.go b/src/logger/logger.go similarity index 98% rename from src/logging.go rename to src/logger/logger.go index 1e65cff..f58743b 100644 --- a/src/logging.go +++ b/src/logger/logger.go @@ -1,4 +1,4 @@ -package croc +package logger import ( log "github.com/cihub/seelog" diff --git a/src/models.go b/src/models.go deleted file mode 100644 index b32c7d6..0000000 --- a/src/models.go +++ /dev/null @@ -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) -} diff --git a/src/models/filestats.go b/src/models/filestats.go new file mode 100644 index 0000000..7fd19aa --- /dev/null +++ b/src/models/filestats.go @@ -0,0 +1,10 @@ +package models + +import "time" + +type FileStats struct { + Name string + Size int64 + ModTime time.Time + IsDir bool +} diff --git a/src/prompts.go b/src/prompts.go deleted file mode 100644 index 426dab4..0000000 --- a/src/prompts.go +++ /dev/null @@ -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) -} diff --git a/src/recipient/recipient.go b/src/recipient/recipient.go new file mode 100644 index 0000000..4f338cf --- /dev/null +++ b/src/recipient/recipient.go @@ -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++ + } +} diff --git a/src/relay.go b/src/relay.go deleted file mode 100644 index 3287192..0000000 --- a/src/relay.go +++ /dev/null @@ -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) - } - } -} diff --git a/src/relay/conn.go b/src/relay/conn.go new file mode 100644 index 0000000..4bc139f --- /dev/null +++ b/src/relay/conn.go @@ -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() +} diff --git a/src/relay/hub.go b/src/relay/hub.go new file mode 100644 index 0000000..c846b50 --- /dev/null +++ b/src/relay/hub.go @@ -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() + } + } +} diff --git a/src/relay/relay.go b/src/relay/relay.go new file mode 100644 index 0000000..6a9368b --- /dev/null +++ b/src/relay/relay.go @@ -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 +} diff --git a/src/sender/sender.go b/src/sender/sender.go new file mode 100644 index 0000000..b06f738 --- /dev/null +++ b/src/sender/sender.go @@ -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++ + } +} diff --git a/src/server.go b/src/server.go deleted file mode 100644 index d21dcf3..0000000 --- a/src/server.go +++ /dev/null @@ -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) - } -} diff --git a/src/testing_data/README.md b/src/testing_data/README.md deleted file mode 100644 index 54015dc..0000000 --- a/src/testing_data/README.md +++ /dev/null @@ -1,135 +0,0 @@ -

-croc -
-Version -Go Report Card -

- - -

Easily and securely transfer stuff from one computer to another.

- -*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)! diff --git a/src/testing_data/catFile1.txt b/src/testing_data/catFile1.txt deleted file mode 100644 index 0651332..0000000 --- a/src/testing_data/catFile1.txt +++ /dev/null @@ -1 +0,0 @@ -Some simple text to see if it works \ No newline at end of file diff --git a/src/testing_data/catFile2.txt b/src/testing_data/catFile2.txt deleted file mode 100644 index e9d648e..0000000 --- a/src/testing_data/catFile2.txt +++ /dev/null @@ -1 +0,0 @@ -More data to see if it 100% works \ No newline at end of file diff --git a/src/testing_data/recipient.gif b/src/testing_data/recipient.gif deleted file mode 100644 index c59f4c81714195ec5678c4114ce21c35c349f2e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47013 zcmd42WmJ@J`}cdzFbqR8LwARifQXbxGjvFUNOyxYI1DxP(B0h~0@6r_s7OhP2v~@q z2r3)>#j~Hi{`Y$Bz1DuQ-_7F1a9-ze9r67f-%(Lj7MHNR4wC2nM7TwY%8pW zzx_z7pH0ZqLT{~ICLgWXh`kl#r9N89YoVjuo-P{dq`TB2nHqG>fL8lyy;?h~)@+T& zWF@;ija)^nWHh6Q8@*x)3$l?_CY1>Q0M<)FEhYWC3UWG1B7#CVpx=M`4uL>{OMw6C zGw}N{0q|vHa?SJy(z5*a)%doJz6;Xm9c1QSY~|rp=SRZvV~v+z6W^ZCefvd9L=ZDt z`Wd6an>ADU1wT=(>Ur&H+^logdgC`jlKAo6*6#^gbuZs{UJxfwzdh;snKb|I+mHTB zQW_p{q_JZFCcC9uzkF61oI8XEjSLVkVU%zA~YW+IX$LE&3OAY3|TrUo9)-Sh+45i+V7jsi?b6)ES<--cB zKk|K7n&xt=Y2)$i(eY10aZiB(G$Fl=Mx4;jSOUGa-h1)Z-AOdd-Tnj#ABO2%so-Bk z(Kq&=maE@^yGeE&zNk0r4I`H7e81A}veus{)%EdJU+~2*V(IRq?a_GpJ4w=yj^E7W zNj)UF-E(sAyv}SO>2~ke59__bmn1TMXP)Di_IL0X1GnJ|`U0>KH(2of2XRU|?SX%$6ffw79FbDyw^VG4s;W7v|A z*0EfL80$FR#tG|q!G4%cg2)`wCQ)nyW0NHLal$59<`>KsD^H?oo1)AVYn!SjIBA=v zC4+C5uBWAHmtklTYnN&2K53U_5r%J{ZJnfQpJQJbYoF`fIBB2f-jDB)?>(pLP~f)_ z>rfc=(XcF`7ipu_T%)&apI3aLTbPNru3wJXK50sUp)N&Z#ojeafk-FpR*t zx-?16xu&u(&bhX>amu-_v7f-DzI9H`rJ-{p&ZV*ETVrN7V&PKtM1coT^nJ9?%g{{>h6#B3gg{-4jZT4dq4IQdh{L7 zse3&Bx)JZufBtdWW8l{>LeD`ksfOnelsUn3nBe-1=LpFiBCk<|wuaXj)%^soCv+Y& zUgJy;iM%J+k~O?1xr!3Jr+AxYyr%^Rh-$XW4)KHMdfJ)~UKrj_e6VEdG5g@9#Y1AhW$R>3zZLtUM88$%rdhu= z_W@%6b?O#kkioLqyJ=5e)B(@$SHe$>KaCIrvNPwK2mKwVqM@t|f%`VIwqCp3$Q9 zCBz<>5r#w080q&Vqz_@EECgN{MgCHFGG>&6*DF@TwUoRFHpZjo6{lZcO4)=NN<9F3A{6J9V9Q@dJHKBI{!=S)*8p6Ap9mlVwg46YfL_X?Jdyl#$rI(?ldh6Q z3)Q}A#Kuj={12u-1PyEn-BRz5o8NB_Vie3;IT8(5xLlNo=q}vn`gD2kIu{r3{Rl2W zR8Xi%T#_z2GCG1M*)_p5GgmDo$|5E!!z4nLrU^SUT{GgrYSOiZC7 zPt1w{mq5nnkLgGV|IWJ5F&PNg-MoR7{Nx%1&mL7HXHR9p;LwxY+cf83f}!doTm?T- zirj3J4r?FlF^}C$*CH&|ix6)SeLy5DpAth->?IC~nZKl!jhL%uZHd;pN#(?}n0=g^ z=&wj3O{tptDvp}ZvTz;avmohdBuD$`Ae~F0LaolbJNJ}vE!i@Fj!(^qZCyz9?%?2v zRXP@%+D%eRz*0j1mPhunXg~~8DsLTiqns68Qy5;7JzM)`^xk$Z(WQ2KrL6Jbo;0Lf zHLhu5T24hOZvh4fb#f({Od-%K8}Mlne?kjkMHjE9t)z{tq35DXqN<;${OU^2^|e39 zUY|@9aqwTx=s;I zn=Iq_T}*_^u4U`NqeR6xy2rW^&u=2DJ-5P|@ZVk*1*3Ch6=QI?j9E~4xz!sFNk}5_ z$$eHjqLsQ{QE$Ol0%?daJV_@7nC%1P4nnA7$r`RAh)1lbq#8pVtrD_Vh7gvWg!r@4 z1(vyTlK>0|rN#-HRI>i+Q5_LOTRW^ky#hdxLTr_7A1BPUXLx(nQtfU4!4ap&edlL% zo`hoE_H@z(*Pi+~pGPa+wIddb)E9?G8yG)-Lsp91KtvVf>LvLI&VUvjFEFq@v>$#pse|+;) zQ@`1--n0R*ORL9_oe279m`FX`V}wjadUOO~)Yq&a_g6j<0}z0J<0_vR|KwBJ?|kBb zCQX#LPB8EmP1=*IWr)=1xWF%-(>>B{B4eOROWFA>frZwuC}KdmvigdLH=d%)ndRJF z6rd_(IAAa<4QY4~K>twBLmnRlHB19&K*ZVT-2B2MU&c~f`MiSEq714^OL?}GyG1}G zZgdMAl~Wyx?CX&C4ujGLRyjSkm^PhlGIyKT#Cz7BWbY#Pdq%>A0(b!b`+v`g`IGlo z=d^8|;NUHqsH7ms5UbJVBd_~?&QDk8#FL1;QQyDYaNwW_G*eqC{CWVvr;Yjt~V zXMJ~LkA{ykjze}9Pz#FaXOqR_ch&&GtUS3v+i z2*xT;>r3M7066DAAEts1S@B;D>8+ z!wkm&;bj^Nw(;;L$G`tDh)3TxJCR+l9Dd(NT&qOBS*A6?SFJrDt))j2PlNSO=!yR+ z`E(#z0KmC4?pgmOG^JYiSp3~=?|>{%lC4vrV{N1ByM9mpd0)59fW}WDpDtyW9sX(z zsmrE+8pFAhUej{DUtNp$z*e*I0b}0$zHeE*d++95u@J2$|J|>BVe^AuE>Z4b;gM06 z5z(=6dYJg6WaUI`S~_iNMs^NqR&GH~eqjl=xU?d+ys`#WUE2^?-`L{W+}h#P-ucM7 zyXUc4U;mK);P9CC=#xq0#MG?9%-mD@g~bs_8#Z`a-&zI*@S zBiH`^r!Oa`U&TJZ{dV!==a28Ne&918br@tlaLIYik*_T!qsiFSIJR}xM&oJ74T-QV z-eu##^mpPI9tmboxI?y z-fQo7Ua7;mHV1AX{;Cv$0O#+{%MV-!{fExmVA?c`@|%8ReRaVB8iINAqm4oFR~LNL zo!@6M|LLqR?0RF}cT_k!A~5V;BqoRq1dnxufe3=4Edcf~s6GTimsF?_pIGFZl9p~N zpY>MPoY z^?0LVNiXGr)3yM-Hkl^GQ#RGV-_F4;TEvr!Pgt|v+OC~-Iwv@-N1qu@hE7+vtP)2&OT z`>#xJS&eupKWiX*uDxIeOphYRpiFddYIjF37{F0LL1C$VK&fkHA$Y7;N_MUD?E-mx zc9tUe_7V*$gpH-sN?j?}kAnwFgB|h{{Q#p+Ttjl$1|l`c(skqwRCiHE>wb$l3Ka)g zG9MXB$ia^2H=HPIaHMgDy+~>muCtieCc>9E$Lyw6Ek?cAtJGwln#G=Gxo}@Xw$d%# zxRWQ^5X-`u934XC7>Qhn!gmWsSiMXSqif})uxFjxE1m7I4=>X?7j|J@H>qXkLqI*L z%*!OdD;5|GR+qzjM7(Mdk$d~2$+AhY@AHlK%dd5GHRia~DM|9FRAd2Sm#**}chkZ! zbs_b!+XQ~dZ3I!{nNFI9B_Dfm6EESDk}Y0dL8ouIU#t5Zil?u)XUj0~#BJ-pU#plS6XEOrnQlm0}l)UW+|Nv=T32+135a;I7+b4N+;3o!U&n7fyB)6q5O zXF!H&ri{&xdZjr0@VOSg0%e0?pna(YATQpn2~vNFksbVDh1!-6MgRB)!8hlYm4x$f z(@eR=0v5h*WlsVu61`MaF@9LT8)5+-NkckxO;69BWPut^=mPFw>kJGphPf&8=wJq^ zI(&5kQU#4|oNVS@UE)UfeqO_d=oG)iiyC=AvCNaaDL<|M{JeM+QAkvLen5SqroQ+s z1NRbfj-O*Zq)Wr%YpEGF82@DzS)ZH?+P-!1RFW@V^Nt2vW_q@((m77i&yU=X?QmV_ z$P$VnV_ygHO^?tt^%T%s6LL>0Zdtt2@d&a`_?khbL5AUKbf|)4TQ;7)#w^RyIi;0a_Obk`Ea8}SHjIx%8Qc*Fh z6%~;ymN~d5Bql=*U29V76p669O?ejejX|{`J)M0m{cRmOU5}$j8wQ)RTNT==dUGf8 zr=E423_KZ{8(A3B9v`0EnpS470>R4g?`_IIMLL+!e0r0DO#7~L@#B2{m%%irP+le) zr-PNl!EO7t@V^g82=J?2J>XxX+<%@z+C{)nR{v|Ks4<)JZ$!wtY1eOFZgRab^y(#+ zpqzJZ-tvq{m<80#Xo4&{+RqMkPc=kqUiHoej95 zDGgoKUf5Dn)@+`coh#2!5bhuEiXBal&?b3o98exYdcVWpR13ZQsDEv&Xk&VNb$R^d z+RncBKjRY_z;FET!At-9*=|$?tSd_+rqzuF*(IE^snFpCtFX8PF@uPe2tm{sTY%IF z>;;0QSOOuqR7J(`3|BBB&m*-GYEd3SQ}4;50x>NI$6+-Xa?RywRj7N@h-e7=j1>yB zpHx@Llau&9pk~Aw*LUxAkb)cR$^o{7}B=hmO(m z-jM;ZKs?4eBE>w-DhZpF8k3vh%Ti#hLcby&~LXQ3(OtiBe*nVWC2U!t=1u{kxxHB<73 z=TKpxXP@uA``&l)_@%aE4n=HhCh@*?Zh+?QvukLr3WM75+%fkKy2fZ0l89SvkiVyK ze1JdVpYMlO{vRVo_}4U!vn;Q+CEv|L0LPFs7ClTSvHWaP|QDMlj3}rWqD={ytAgD5~Q7i-%dq!p}WZ{DA^<$G2*i(B9+N0x%RpQ&c zq5inY@U+}^U%!AL^wTNR$-%qn)T`c+J?GEzX#ZousBhLz_hlMu{9fn--#w`)<88{MqtTezy$c7wE!g4WYAoZ$)^lKo-pJz zWgoP%zJ`9aWr4S9@utW2pLh96qdfj6V7O0zYZRwK+2Th2`2ObQITzPF(a=xc8^=ki07$6GbEwY47K$RR2v4EalYYdvzD_~P7_VQ!_(`eUh`J4;_ExXUGlZdDYA7kfl zZ75su!pT+Pj@-l;Gf!(=Ja%$k1}=+)a4$U<5Pe2&mOL84Nt0h_nwMx(4q~jjpY2`W z$Qf77VPKf^kfWlVBeSNZ?r}p?u}o>(_;5+@G$$!9?S#bC*mJHJM;bUCYOZyehcLM4 zk&)2;0nfYlFC;%5-8lZDaLWGi?3($-mdsxwhy1t5S^YOS7XK8J|FaH1@Hb<+KmXzM z%e_0JJU6rgvhb0Lkx-Eq%BJ@UoO zOe@U|L!~TRvkC-B8kDq(^9o+?J~-mjXiyBSJ!ia@p*j)nj1r35WRH=@HnVdR`s@f-&L%kPuk3M~q6CA`OViJ&5>9?bCs%(^0Wh#U;#H1-1Z{ zIlRKMD%CWBGOsBkq74O6Nz;JTnTKC>J$Z6O>iTWtRv`~Rn+ANre13IBV@$g8a9xFQp^neVInoz{liXBM09 z?KHU`x1^xKIUrpQoIpsKP$ z60f$TUh>{wiR%0pjo=65!GCB3?Eg+9E-n89jTkKc2UFMZivPjXS7P~!M*P9le~INQ zOg-x^b^T-a6^&T?Etco!M=pMg<;bY$7)=2wDb7P{KC`4qDRk` zUM^37U#z^^*lb#(+}eG!SAetr_KHSCLk>S4e>w4)J3ae;Zuafs*X6Hs5CgKgU=R%> z_miq_E>W;2VphxF=VKd-r+GLwJ<)Q)E)dV85H|zyE}u+GR4X-~6j_J@glwkLC(EyB zgt*V4xk#JCOo40!0i#-bHMB@3k@wzId(ATzWUx$uT1V}RI>yqu(W#EQmrd#&ixBnB z`js~M(O-(woegVU*PahYs&_TM>V@w@2dBH5?zaV95-@3WJ8cbP$oU!r16p2>V*^E# zG#<6Sp-j8(vN7|$dVQ`~A>I!g*naRVMZL^oHmLbxo~KS|@`Mu$B5@>Viep-77{`s8rAEw8)raj!qg`anGMbyn%`U-oCq?qA-! zeerGi)kx8$H0V$Ev;M_HG=P!cFT(${iTIyG{F{gT8{$tFm;Z)1HZK035U1aRfmlr# zKp-4{S~zY#6q&`Q!kk~`LI9%9P$dUZndcQWM;Dj2Rb(qeYV#2pcqCc*EV}qZUD~De z_i1Nv;S_ieDs%I}evA3cu%_yMWojyT4cw)KT!nxbIzDMYi=gV&Z&*|81d+-=8@7|x z3(xPgHg$+B@K25AL)R2^oNfGcJA;T&3*8($4-wpF~1qTTa3@pB?&cu ztl^zxhal}^*sI5+ZnR&9)Y6bWEk1lLYJk7~VN`yQGZRfmUl^O?JE_J=Euy#h@?#u- zq-Ja2XXQb1yi$Ow#mK3X_*_UnXPGbTrRC5B&pFvBC7Lzk4yu1fa9TZE zn4#Krw!0(M0Ee?1uS{b@J`-l)n6f6BiKd<9g1BTPEnQH@$>7b3smexoE5MTs;zhCe zcU|^{=UWqw1Y(C4c{@d$w3g3N&2fa{dS5(mSggW*zbjEwg5;e;Yk3fphs7VQSShkA zsK6`BzW(2;!+D60_iI_{vt#ebGvnhVFcbNVxjq_`r>6=48OVmogPQ4Kc;41)0ps{7 z`IL?|txUkYwB=<1Zn24DQKYgC3nP>`Km-c;j9)2dvcPCF6sa&eS{lOQNddwW_I~?{ zqIX4wAne>qDgPlrfW!r@;Vav~+iwPf5;#^I$ipbj5px;R;V;<1nYdMEsk|Fhz=KOU z2pdoU|ICo`O#etd#EEijUN9g@XB0vmdthS9DL7V&v}nyrv67?oN)@6Mc`8@*vY-vP zgS8*OUq+=yO$2?ZKlC(NZF#st8<&R%Wf@sQWo412A*qFtl{c--ixEjf6j4_^c$%x| z*b^^_bVTOxjx*p7;*3+6StQO-e0^8orS+MogvNZ=xiZq!G^r&Ws#xI?!1e$l(l6*A z34-ecU+NmkZy9jM?p+IiDrk~vo6@DcJ|b&4q_Jw>d&Cqxnf}3Ik);h6MF)m{fxRb4 z2HRR5DefiKeu58D5XfpAGseT)DF-o6vHE8@qxL;kys|uSRA$+fCIe0F%uI%4lngMa zjN(sgR4a=Sy<1&QNjon&DoA49HZ=IwJN`)4HLkTDKFk|%WTIV|YiK05h|B=)Kh^WC zB&B{Ys#@0Xyt`oEIWs%Wp^!rI62yF4eVF`xNO-3<4fU;wmlWjfBdTW|EQ9$%qxEZ+ zZG8d;1n)J5gxnB7c&6R(*DlfOp&Y-xaj$+W-V5Pi0bs7+WyRBIwiDEh;sZgp8I5As zOUQxR$+xQaZ3_HKVRZg0p0kxANDB43ClVo35A!4&i&5@*Y)>92EHGG$-7V>bGqywN zp)y(4srx z6wkJFq!CTCDPSW+5s`X$ZY0_E6W{o=b8bG5V-MRX1g&RTyy}jw$!>PDaad$Jx3BEy z;@G#pC|0(j!#uV(@Ps|tE;2H;>571B1^wDC;c0!sgtH&3uP(&gE@{WEpjpjQ4By^b zFzkJ!AI-AfLqoyT&CPlYeVDB_cH(JVLg>|WlS~9=JEK_eYlu{9M!aBTv7_!R^3pK` z8b`q>46b8s)()7VdwrPur1K6*N}wUx_J_>2$4~U@lE`roAMC$LEFLZ3i`Nx-gos79 zX$U)eEhFHk+k=;W+1A@MoBsJ~z$!1IF(S z$!6AZ_>Rsd1L2r2JRxN}h#;rx>X~OUr&p6?;)*=?`^vyG^BtDEsyAOG?rUC#D-I8c zP_b=Aapq^~%U@T*o04vI&v;j#C|xzE>z7IvQT~4TBA2B$L{b|d>|VNa+rLIf%__Z} zu2AlFKxgDRf+(}j=+zOjj=M0Xbfm>mtLbRY+xlsl>MYnKm`5%~L?xq|kWNtQ#bo}R zaz@zsuG~B*O9#ae1{J+CU2H_z66u8D-Wfp=hT%1>_nb5ef4ot z?LaP@R0v9C++4Ifkeqp;Z(mpYy~dXQ^-Y^lp_IPo{Cd~)izAM1CQskJERh-Nuqb0n ztAzbzdx}(F{Ng8y@kkJ)U4-=ve14hPj@um8p`-t?kvRTM1KKG0jZ&{6>%&;L4{9wE ze4#ufQTQJ}IK^WZKDEsG>$X{!$-ZB@Q3|s+POLGrxkYY-?n>o!cQ?4ZI7uu~GI)&> z183wkrGicu1>)jAR+pb(jO6)^4yr}%LuxT7>y1Q#bhH@P+BK8gk={cpT3+KyjEw@TE%#@R|a6uF3SF z`U>4k2aDv8^S3^s2~fS8cIIS1EhKK_wnz{{ZZe)jzaz`33*3F45vlw#Qz>qr%>VnM zybfwkzWXgGn@&GnnagWHUTCPmG~&)p21m50oMVK*Gjq4Hgh!Iu6^vsGzPw9E6gvFU zR?XM2;7U?6Y8NrwyUDDp9~H7R_-m9_yV9q=DSb^8$FKlvLBEuRL_f%9WW*bnYJqRB zT)Z|Mkm@STSZY+38z+0(ql1Su?BK_B3U=vnP93Fg-{9P|#yi_1A)ILCJN5{@)xge@ zBt9E4J}i!)nB{HT5N*Ew1vBAg0quG@7U=l))y)-pBi_?ki=4|t`h&Q9s@9m1M;{%r zh@}Nqrm8slR|4P5bE=aL#&zWnSu|_tV+3lWBWU^dYMK_@>$G02ON5>5Jc-?QNq)5I zw7;*fy#*e#?&;9BrA_>H*2YA}2c^QAajD*L5$h6==#cS)meaM}V3wADGyAwt-SU&h zQC~7y;|HA=F6wG9(Z&99I6UZ;pkKqe$Ea-a_Dk)S68P9b;{1<2smt%0+3}yltzJaa z#C%+s@Bi9GN=RzC<|)=0MiBD}{xR?J$G(5$_Mq3Dy-m9_WA%X_Z?!M)AcNm^H4gmx zHh=l+%9RDspjxQ}5x4I7$f6)LVFrW3ux>N(8H$W193dWlNnspL6%bC79Zq+qM_v`q zbQaD+gVr4k7ve^91)zB_;T!>h*Ot+OXXyPdv?5-FsBwhY8NEnAgk*Qb9JjeG8Jxh8 z4iC?QV7;iaGWfiUD}jkLUye%Nil9Cvw*Crv zP93Q`Xs&ik&d?D;hW5MH9A!Zh<82HhalB24C2IB8Og%Bx&hXm-gj|$m23ycR#+c}R zJWCpN6W!2TOCBTMn)=n+E;Kw3VNlO2PhfVe-PQvT+L#L`?9mthdao#kC7#FW!Ys=6 zVO~OM-HxV6OfF4)>oRUUT+oN>Ml?=vv|g;<7WF0V*ZYbK+PU_|hPR?h9WA49?y|$; z+m;jN998>=xF2$JwS@yc)vDi(?qP0uR#eA0bV}WN8D~BgLsFWc;~6`jopdNp^3s-Y zmx7@s&7xw$gppeD^{JMJgWM~2k{w4AH5ApvTgZNQ5+o<-NXf#4(^j2~!vuy~4Kt~2 z({HD?#%$yNL@I~x(<(#!42HvTzo#7XBq)qA=5Hn5lUE#IH|0tX*y6FNGfpb!vC5oC zU@=K+;YkrNQ5fdbF($L4kG?1Oz@ZA1cE$Vq5V^a1qO!!lhV@}h) z9g%3qk_&&2Q_4=aFu{m%rfu2MU(2%nm5qBjj7$FpPLS$<|Bh1(H9kgJHSNAh7BT>P zf6N*43|p9)_&ho_G3#z8i@mx8p!F@C@N2ZMQ&z&Gi0%^G&1%(Lv1q+Pr+5m2K56S@RXHthh%Zrr5%FCPfL?LX~K(%soSGWsAOkFH+56 z&XJJ97&&_?StfMj{=zRNB1k3D3B(hPysjY!V)%|rxLQmTm%b<`biX^B_Pm7U9FLl= zl*6>tU8s~Rx0H9ah^ePkP%`B2m4zTc1@PAau79m8aLfOb$`>xW@*f~{hqr~X5dNHO zAmUUUUWif#Mp7X}bDUdr+3949N4mDiWHG~=y3bb#lw(l}j|_Ru?LvImNP>L*(6Sz0 zl9GzdQ-;#_NB168@$DU4(zk zYC7d2YQH7ou;_0JU`lwkGFKnlYY!?=Y}@qwD5ljU_4aF@1!nW&QaltD5r~eA@r=gA zJH;g=TP9)COj6Uc^fR;bG;{NdlnaZ?F)tPr@HS zM<)Wur)Sr`egDCB@e`2xL5L(9xuO8v3>p&)L&MQzAnAk&Pd24Us_TfEYB(Ruc+w9U z^eDGC$KzD4%@U0ocg|^V!Tad=Yd-X|xl8Ul36(x?TMK2aV{F{#y78U=+N2;o8{_v5e=9WKvq^tYUZ$9$-X0G_i*pua zA6dC_Yh3Y>KW>dTdvEs-{}yE)Qvs5p>?>wfRZa6}ch_zNC|j&$*R}L-^S@65LG-xs-ok z*nEPZpf?u+#h$Brtx|q07*q7d&!)H^5N*Y3n)v1EsRg9~FKe!%3cv4ty7_};^I(BV zgA!bxM?8vQj3z2s`d@3(t)1V8Q-U&XQiYqLa7&^q%*phIijtM&P9qJ4mF3E8bWbJ} zGc4~(L_R#cw~`CrwM(%<@C!#NdZTM~Z3`OVC`LW7Z8IC3F>YmF9~m@SUDo;ek&UGk zG$<`V*_UQ0mSLvc1{WVLlm-%7$$`M$twhLrXbVjOcujtC)Z)~e*jLmx_jT&QOu zMcVnMp>HF2!NLEYSIj8HVdE4tBz6jmppiM5_qAv z6|Uq-E7{ErT+AbQ*FvNDXh|TwHdp>BMPdGG7SkqX?}m7FDW$@h97>rRT#g zN_z`b8y&QT%rzKlb78*)fAR?Ju`YPWd+Al9q$Qo>;3J zX32YwT;mrHT#d`UaxMt2Y+n8E{LFl>ys;AELXz7N_Xy+8uJqmxmJF)_#DUgN+f#lf zI)&<*?m_K9`8nk_Mv-h#EQ^|+b)|e1tq!Zrig3ljEYP^lE)?-i{oM~Qi)Rw}6{R>D zlztystI3)1L~Q%*dG+w6hF-Csb`EcG3a~V@=2O~!i^O-dOVgZK_dPqiAw|;Sm!rGk z%8=H>(fkG35N-qKq2Mj#u@vD%I0Orlfk_-)+>o;S`}UmR2wqu}Y6@HOqv%-~Kb^db z=kv_0TmraQ4pon5=_b2px>#i`H>vPE5>9_E+R6`Zmre?0QEWNA!cQXuD17D3=+2^W z_BqQ5+N-08{B9@dczafcU{{g`wJZE=gLRzAicJqGa_F2AW^rWiI>lDS{jw{IYT?yt zbUr%f4Sfn*wn7w>~1e2bXUPp&RP9aPM{vkz@)+ve&QU-wo)b>i4`OQg7w& z**)_9ObGLWbq8KnHp(T27k5`9iL%X@K`=~M+?wCUW1;)Wdbe;=N6BkOo3e44PGz$H zts&058eZ^ELryGmuTh`} zQ8oN&$kuGlCbVws9Yk(~N_8GSjMOk(Tp9$PG8sN0z4@b-uHOxLxoOF$Z<>k%Ne16$ zw52D8PJ&oeDQG}2N>W2wDyqrhlu;L-JuqwYtx?%#fmfZ^%qN<+DlTa=81dw@WJL3JD-Xeh+yUL>;>5sMOFc7hAqpXf=p);pTqUZFC=r9m@lcm#${H6 z0I`cmq(rj@IPxZaygq+%`XFwaA#V~yM+7BYrdS9)tJX$g6U^# zjJUl;p_WSJBaO{Im{MBg`s%5W6~Iba{#ACAmJ*X5LCD-c+qzn;GMf7VkCi{q&o4!X zrDwN21-lX1N3az*p`Z0U#ZlnB_WXUgLk6u-%=h=;+5uMbBJF`+Jg+|=P^F+kz<8|8 z>(F8po)9)0{tS0P_CYA6Rgw*uXKN-fAGBxTvxARJwF>O3y4-|7DY2*&fXBS#J24BASrbsvsira}4T+D)DxBi295&lW-u#HHT2N*M|LF|AhOfU1Af@a;=SUmVKrh{O?e9!pj;pV z=*72W4L;jZ@7Fay$+a#LX7guEy%o2j4l_nh?N@;0D4%G;}4iZ+U1bcS^l0me z`_TX6T*5JIx4M%xE-Y&KfMRclYts+!A(wVZ^sxF{2CnnHnz0j|wNFnNde1)9lQ~Fo zji~dCxID6HtSrmgU;?2B132lgGu*wI@ZLL4IMZWX`kTi3ct>>KUc0=niAb(h!#7<7 z{@t?=Ieg>%!fD=$gu@!fW|ZZo!tIVFf`f znxa;&?DXu7oTK2RBK)2HC#^>xmmQiWUq9}0jZgJ4EF$=}n&t6}eOKyNu6W1R>(%Fx z)590vPhNT;&oxnP(bwC75$KPiY}{Y(RR+PUR$*UsGf#c(2ceQ=q%e*o@VNhDyeoI# zeax087iNeg=qMt0iv^~F97fQU^8IUbyYeB612X{ae_N@l!mn1VGHHZI-3&=NYu7(s ztycftyP3IST%rwPU6~V|q48<|Rw_L#ra-mGDY+=Ms-_ z{wp2EI?5;q|40WLHV~BwtxG^JtW9CqNe@y4n|t?uW}$NV^VMqggnsRn6V;y@<}2AP z=g=4z9*R4eA>~k8FcS00{Y4(KrF2aHTesEcuX3?CiiC6KG=5QA0NWd|hQweir@9vz z)08=N3%oJ@YO}hY!)?=lne=PXQM`s#Gg^d}+)y~ydwU*qqfD?a2T3o|S4Z1w#o!7?Ut7tG~Jc3fRF+?{nB zc|@FldwWM|Dv3JPs@}`z2~=nR>=}BnuUqm}(+@)BxW&0<0$PIOZ;^vN0STcYX_L;}lCu!Yl6tu!-Be;b8+8WhO^3wA;WR zus~&F6r^`Rm}v>SG4NS-z>e?JcAhus`re?m`?8amc`up-2U%c0rHdHnj+SovLj}{X zx*!T$jG_ql3$D`C1dYAjv;#$xH-#ZzAC1QAkJ8-tSJ*Pn7Eke|it_ZTR5gR+nOBqI zctbPG%?Nh<9Z)*MuisV`ALH*+pb?+%Wh$*YLLm6d=sj|FMhQqtlmJVO(KmzTO#UBk zZ8iElU#Sk8M{C7-jkX$`b99FcENRJ8#Y(IFXH0AP9DPM>-!|RARd;OJitb&_SkJK& zuu`Dmx-92OQ-Zs(J#(f_?MR%9y+)~Yd)%$7K#k+$0O0b#m7*dD#s#+%7N(1k5tHK! zZbHC!FnZE#tniE0*A31f|MygC}E4P+dOTSFn)w+RP5Xe3=}!NeLYRkp%r zc}0)=psO2Xw9lSU9FZ={ApWf}S9s!gjluhGYfRbi8bg)3@y|3Q zy{@-^aT&_u@Rxn#?`g=?HPyvE=r^A5hNrorvHuHC7#E~vN^S51z0=fGDo&4f|7a7r4?3XCV@BW z!SpVe;L5(i0{HAVSMym8$;-2oIV$Z7VNgZ940K^IAQ{sqsz`*CAKa!JNb~ujd2HWY zF`Xr($*M7~d!GR>l`8*{&?kb(%Uq^ABv#nyApl?zdagJrnxv-6Z=?3xzLA|n5YBX< zb5b2GN;~Dc1zX&$E27!Kr$n>Jg4q}b#R@K-r@iP6_;UCF{i4c{UF~}05Be)FE2*7b zi2$`ncPPUWm{04J8=}Z|9$O5$nE?e!{vh&^G^sc?fTnxOg}9MF92h8gpQmALg`dtx3*;l2c7%%!vjuc7LTS zwjVZ1FRXDojn0!l8jTL{w;w4lDvsuLhTm1z<@BQh_6#dlB!q@2s0vhUf<3)nFX+;e zcpYFp=q&efNik#qxF9I^ZS@2fyseALz&HurjV}q{@##~Bh9&Sz@Tl^ z2ucw@TQyZ#cPSV)Q)}`%57A0#hVbd{RSfO>*~O?BX%z>_-D8UQ*aM;NGwRh6U$b(u z!LNeAK8uAnC88FeU2po4O4}@qL;wEsxG?K_B#j*tp>AKtQD@T)@v?S7n1$@RRXn>rhewN zU>QSvZ)|ZT&q=G?{@ZvR(GJF9)Y;?b*B8K|J8%}VOfE%o2V+06NJ*Y)p5nw zy7BbTGmg(E)9oaz@?8NpY8A1@O&zyFW$-?Je~V6gRkSi^Y0+)Suk-g^5A)9qz7ziXaq==b?#I`+O_#qgSLo|+C*f7S z;k&9gi2t_UJo!B4eyocHrg`F0{gt#5Pm;s^p6!aYn15WW|X|IG1WygHLnTU0ZDghdOv4 zRSb^s#l`ZE907wknZ|Cy(*|u=7}!chY!b>gqw z7UHl#5t-mT6Gq0mERS5-#h65>;^#P~H;8G~{E48DfY*L#OJB?@wwz?5sG95HThBaU zZQ+l)JKnwAqoZyxA8kUbyW@}n4WG0j#JzQ*XVWH5ZVwUHn{QtD**@+KO#JaoXLlR# zdG);=^z4;g%(1mWdWpVcf;lnm#Sirn*+YvkTs9bwdt^khC@j@8^L`e`$?@J#JB0R$ z6?eAt$B?QmG*KzSg7P)3RRJ0K5w0v!{QC>0D1_|Rr3JvkAq>WRQBoPjz>URap{WOB zMGn8hGoegj6Ipq(Q2QuL9^$Y2uahy-NTo<64i-I_%RL$t&NtsDiXN<$wiKbD=)p9m z3*58(1mNpw;cbHV8D&XkAg8??M|aj;3dKU6MFp~qCEGBqT^m#!2&%#fXAL}3%v1?g zR&?xmoRtJIjFsS0mr_gLE5j=HjOOaK=M?ggvmAl$L&Xow@P_bXmh`wPjR0KT9S{!# zdd}p2s^WS#Pnn&X^Sg#*J-Vn0c^ap1co^hGc@T*{C$5Sw!nl?UFXRio_RM1*#X2ql zsRmTU-pzMviITDJy0&<$`6&9pLRN5^r6-rNxbG-g&=d;aSDqCrVVVa829)bK#V#3z z?>+Evn|5p1rgCj|dHrjJA#hb)T_2~z1mBH`L35#G^ftq_3rixBr@^VITyewC5 zV|*#y#!wCt`r7m-w}o2}1#E{q{WB*QMR8#uD(_qD%)ZA9isR|t|pS{FW;9ShMK2eWM8!!o~*iR`-qaO7QGn!zZiQ9zb5~` zZFt2P={7nY-KB_#s6z&fMnxo4LTYp>C5}#&Ze(=Wedu4g8sh0 z`*q#d{k*<^#Ce>@`=}3y&?O!Uha2o;do{3{r^eY7uwUO=sHWDEs%6)NxDak`3L~&4 zsUJ&S?H#yRI#4Rvz#z(+?O>{z#6!+*+ZPV8)PjMt7Z8$I@?zW*+>ed=EvxJnbec?9 ze1sY|1>DxzB5gI_Y}VG*QL%Dke_pQpx6e$2hD?W8(}$*QMPhez;U_!Q3jn7%dHI~@ z&L!9}{9NF(U&C_|0H?8u)xXJ^P&piB{=nhI)}^m9Gw1g~^rQ^+rsiftJ(8u*2)?vTCTX4$!S?jvj{nNk1MLIEM@g z`fm{{@er{T{>zZs1NFNP5i2-^2oDXm`vD=$NiJ$P(1V7GS$}oKE z1<%6|)rxkndG$L33f>rb)G@zMX4vKP_m#t(-1~&4n@>ty1}2Ex9&hLyJj0n1+8O2*aq++zpnXuR?NQBhHc};#Gtn znS;1tA~NOT)Gmq(QC1(D;zZe6&Bx(bf55^G?d?P5r3OOCK`u1c7SUg7lNT=wNK>2g zjMIE)e_-KWbL%0)`7{HcC+wxm8IJsC!V=V!CqbZyTm&}hqSsm|49<>O_H(E=#@XIe z$X!8BT2d#bG5IKfLDnfh1dPoe6V?isTEx`MTA!v*7{ZOeP{UNMZL!qUE<42hM}ZWT z+`G?F6#L{n7IBnf7^hfBY|u4{A~M`DrRWk_6PBn>Nnbfnqg)fF%-Y_Sb?<2ysW(@F zacYz*K9%e)gc(GMm3-Ba;3)wz9#rAl~W`&A9xci$0&&G-W`hamE1>1rRfS!|gV70a0M+w22dvAm>t=AoIcF%m(y zN3yB?MUTI_PN)YJInpRli%61Xp>FpNeYR*p_qZ|WDoNB$xH|lV%zEU??wA1+)RQTT ze`nrSn?6>^;cmCm0?N2ci+W_=Z|(Caoln2jD=qyD_3l#|Qr~7Pb?)X2mCQePpY6E1 z%?dj9J(rV4-7pG>t$OtbA^hKo<2;e=)RQj;>7O^EljA))u>FvMb-q1(zQq4UprdE8`Y{`#(3#)KM-znXWM-t*YV|9cO&b!!=z*R=lq>(gcp0EIEm8p}7mTs{KX_S!x>K^F z%e|u*|D!u8PT`6F@G_I!l(d{fRXnrWA;0pkP2JJ#3~B^y>i%AE>wkFJkVEYNU{m`} zzI=T+DnR}VZPRaO4o|^Jf+`alg#hGnehsAhh6GWiJLnFAY16WZlm(!ifRa)wV+=Hq zRni)pAQSgQ4*bP();LV{*|GiA*G!N3#)J7$l6+~Tnj&4rkefk?{dL8|2RWBS%jhWN zx0zse@%ViY%s3VJ8U~i8QE(HKa^;*3rl#v+#%)zujB!nnBUAnVtj0+uXPPN$_YHM=6no}97z7W)iq^+1oAVgPXz*0}E zSuYt_0%E?ZQjK2w2~98I63>|GhP@^r#t#7eiG6%<>DZX^agWm?^8#|2XMDH5*K)!p ze5uW^zU6aAS>fZY-s*BDWuml;g1+s~|fk3n_ZU@8H9=l_}Fm>O;cj)g0 zxnN-ksgW;&7jH1-2z!=dLC~&PG!9XoyGVbV2`;u2r(U6<=C4i?Ts(ttLgXQzT5g9% zVIcvQvk$Y`FfrGV3USJ*S+26l8alZ>M0GB5XP_GbCAT7V3F#O*=W*NNog z`NcMB_PL(+B88+Vt*_w_Pvrb$ScQ6-1m=S2`iJUFOHSr052Z46sX>+81k5b!mWm{X zUOoX`)%`3M#gWv4(-}tBerQrGrP*j+QWBJ8&IngKkc^m_86aX?>_^9=9tsf?q9M^7 z5=KeRXGu+zz_WxF7@OqF7X1Q=UjH2Q=w(oe-sBXkqOfsLK<}>@`yf!kg>O%ytLalXlW%Q$Jht;728bL;zXANr`Ih}9aDGOlhRH>2&oEaT_qiTznZC<(cb@ac|k#{a^ z^y``>Ye)*c-sU#sbno(pxgoosa$YG;M*6ZLIk3ui?xg6Mo1TEY5&kCrZ8=Ng;^`3+ ztA6-nvtquFkMg!YKN3PAKGv=|#C4vWQqbdVq^;vR8|GU8y|cNuWf@Qv`nE53d{__G z?zr!*3A>5gea%o1Pnu?s#zT%7aJAfTyE7ayN{*2EUBg0qC-+;p%RT3+)usdWt;If> zdxP)mB1S*Yr&)uw48sOH_CQO?Z?$jZil6w$#wmDwVQKhJKQOckMRzGa=XHp z{->n12lAo>eL5x+px4U>au-M|v4E`sO}K+pEw zNM*0VAP0xHN-M&dS?K}MJleOp3}P-u1>$8-VDWO)k_J=TL{Mbjr`?b4GwAz<{Ll-S zc`cvv$-toj@tZ^6Ur$F7(yzOHuzyS`(od)LJ>%^*Ka4n*sWsaSG?HR?&1z2@p0E)_ zO43J@f{3igz%imFM2c8A6((W(K5f;>34T)Ihg-(?<=0n6@>Q!4EngCzyWXcr^@21N zMQBbyd9!UpZ5IP$9?H~{ZyUYeE{xKHPNaMWBrSnUH^RJ`HZ*kN;O8&8VN|H+-YfKS zi=MO4zbUN-CxdzMal>w?GQb^GJhy#tGXS&MJ$v!xn%zRw@Qy~=ipMOA%tSborY4 zzzyl%4-9$CXv;b)Fs&3M`JJ?5_C~AO=oAT3a#?qcNoKANhHhx5KnU3+=0W2Cb*8qF z_=k$8*J;&?Ost-xpzWooU{-3cAN-mQ2H&j8%Jm3ATYa08|M)utp|OLFF5qt%P7v|PXa%u%y}qG`WU^k&&!t;SAzG!s#hQNEtr z&CtKuMO5CaYNtOvgy_({Ue430DN2&TT!Tk`Y}O3ml6Akpk14ou>}QB<_ol+q_5e_b zmt#V+8MEBJ#xP1O?K?N}G1zSg-%IsG%wiJGGNlM>;%(L-TXzR!Sw`(ISigBrUEHoW zXl!>srS{kKAVHV{A$8fi>BL!#DMvJoJkk(Qu;`MDF7-&qCThKLH>^9$uQ%fG@!PvS z72_LSg_!T~unx{wH~W6Fu67}gf3|&<0*wuQ(4V6u_3d@!^2Z-sMjv3`R|~jycG2E} zyc)UC{PNcI+=#0`Hwtp9ZuP2hWUt@XVg=Feb|3H5^WOM6>e4>Z=vW31%04bh5_>W% z)xOf(=DYtzQsmf;$6fkc?Ffn;Ch+@Gs_Xf$t7j{IdCvMD%I1AC2m5&y4>`ZB9ZFis z3&TKCn%=^z`5y|!{6|Xv_HS_qd_E~VLkYDkaLfQSLPy;@P9O_TfFaCu?%^;BU%YHF@ByCxvz+8vt;mlkk zy(3-C6hVy#4p1mou~rJ9;)JInm-s{?{@jPRU*6Qp zZWXhA0oZkqTc@{a+p^DH5F}b)KqImoLw;#kiymD&%#RY~=x1|rbha_1a%@Aj`8lHo zpb6=N>dCT5rJSR)g&+i2<$PY73|GZvM&)Gu8f@{*B8REQ@K(OBQ611>6&bat%|9!} zxyb_1BXcl49q%{XIl6RCB5})d9`#!7BqUC+Ik4xBu_H zoA@ZUNk3Mk0_Bas^lNBcCLa)PR4|TQazo%HwvLbAblmDNv{!oC=oB8++eG#j6+|CX zqi$uE2)@D-(;Mo!GmJeM@6OD1!BLZ1H8k3uyq}wuEwR^7_7;N|nJswy zP@T*JJA3D@_OMEXl!BN4W;8Aam~#m_&&HF0i+s3%{sJmy^yq@dObnGx2I$j&og&7Z zlV^V8*HPtqSLEeMX+Snt>}*{}#MdoZBfId=(M+ie(O34rub46ZhV{H~(Gv+-5^6rZ zQqePiX0>LL{YOe<)11stINwrL8>0PFr|kyWT--0L;f>4wPQOUCvR$1c$Ia~#4VhmZ z!}_kjzD&JaHADWD#|1!=hiBNo7MoI!xXXk8fg!{|IMq=&DGX!xPt`X0)**KZ`omrF z?uDcm6~+9+T`Y>zGV_np!r6uOPr#**|8kd0WQWE>?(!<9bExDmceybEemMqkmnBw8 zarY5-(Lr?`g2}>i9snk46g4Eb1@RU}wOyWT_b{Af+Is2(MKcR8$JNU48G@;?=(32X z;gt@+j}eDpG`onaWW^G66a!8FU9raOL575d3NT-1AcLL+58 z!UT|nZl$(Q_{lu%vhHBI=ZD6SG|}D>atNKi$7Ire??mGdDwsByknb< zOF-?}W5_m*4$I;CZbDmGwrFC_ch({IP3dzatr9D21S^D*||~EM38c0 zfuVZ+@~HFKucEdF7dZ16<_&&xQ1j)PsaJ(TS{P~JwwUTA3CENo!ZIjS&) z{}c%X<`#7l=T>XK^y!5KUBaiuTCDsENLwK5}GSkHKY*p`Q)V*x<#KYk>3PV zPmr~stDmtBAN9^TSf}GaO(ux2kIlBXRQ7#e?fYW_xnxN`X3*84wS?CZ#d|cz{lm-z7X>gUtYz(wyYs9(6{Me zqUtn;wO!9}#q52FGwo~(w>+&(>A`8soR@B}do1^L^+H)2^B1Ral(Gz!>-SHfSl0{B z;>~h?|7NK=9yTN%{HXqGZupNNed0H53(PIBFR4;!o{CZ#Zjo;;{DM*_*MM z@oW(oshkhtGMOSUt!xy~k4uVPW@JcUgd<^C3owLu4;ZQ3fa>e2otoVs%=Wt*fu8}>6ag~>2%5Jo6uGe_%W&uFfL^~dLIWhM7h>GQe zEFJCjek>6R3yZA^5HcR=4+Ej1BdxVtPSg}=Lai$XTk7ltj_2QQPQIptNW>{3A$uba zLqHYa$YlDqetl?)=Lgm>j?I;Kmt~cR2 zX5E`x6fxURJniki_j@XtU4!@gul8odr#bFUb>A)sV;A)0@xQ(P7{ZNYhd@K^C_%xo zO)q@u6%cxUx04%j{-aW`1Q4wtb`152C*lC`{Xhs1HM7_28LoXfqn9XN`#ip2*=JL4 zf*B|57v*y2ZDvE!=pADxEw2(BT#I3p-%GC`?QOIquLm$DpiZE~Hap#3Og6p=;bZyW z`#tp`7^cR;1Wz)#!cA=;3WL$zH%aD+k@_*@&B-X+bqk?`dH<;OQj6Ag6S#N@#~Jiw z{>_N0sBkEoLp&lu6^V@m^B5P0CRh?QdX3;C;aoYS$M5arLQ)(Y*v<4tg&vv*r$@u6 zErRSSZk6&~xv+OvH1uBjW{JIiE?hgwEYsTdeQCe&piO|%<5c?sv|uPa=w?GY3@p|q zaA00ITGZq^eU=xm2HOwnWSW-v4XgWMGa7V1 zE<$#Z)1F0Cl#P*bi!nb|&zI+FWSYcZ((IhoX!=E0yA?P2dA6{VUTr9|%IdAx_UrZ~ z?-B!lBTm|mTE1+;VlWI^Y;Y&D4jRv&_@tRu@RzR;bm~(7222HPI9S zKZ7o=zC@K4?gN!*448;T-$6-jK(k)5xl9cLuc{HL`9;7Ir@U)5ucmOMgCm7(f8jop!{ zZZQ*}S8ry)nj8dHbtdKO-{Y2~0c8E6Qne*?1^C7Keycp3#sQ~@ggVC9pwtX%JL<3UP3o0C1T+@<&cjx>RdK^D9d9I zf><1g8t-+*%mbuDNP<8)lK2qSNAWt+tuf!@wN5SM-j8R1xCD!jo+#_zdweNc_>PTv^ zzrGj#xC|~fWr8bYZSrb@lPMd6@JS`Hl`J!?Nm$ankRpzn5I1g?ZDG5tPfaa9pYMT% zj0|4|U~eJNS$$w8GqqQ91z5bXB;DasXFDF@Xn|45y@pU1_l#052~E2&Arh)3bSh&x zKWA5Dt@4ai`dU@{W1E{va(d~#P&EGYock3?1Jq+RB$YvaJuZD3n`bN(`Jqv1 zZRSJMyBSTYN5yMVy{V@z<{qeU)-E81smCtSfg&LmXCF1rE@#FyQNG5k6@|uqC}sKz zIvqy6aS;sh+kK6%u2i#aE9zu^*i-$4{jAs~Go(DKm-_?fP;QC_DKmwMRyH8C{aDCP zeQtKvf!AR9YfheuY<3l&qPet3-EHN8=O)qvY2)+Zs4^Yg1LQdUCnTp~TvyI9@<~U3 zMzfgN*YYVIe zc)n6vV{d$&fAFP(P$SXjF$=rI#HLc|F?K)dExU_#WXX`#+tZPu5rJv*k*Rtq3kG;G zvGT}P@t+^w!tH-E*Q@nHb`;8(@a!-FGe zV#Clv{u_Hbq(kffk`CD&(ji^J|BDU<1-q~uVop?a1m?eV$jC9B;2d(KNdqfccUUt@ zO7EuvD_Mth$g0#atME_E(hJ_uMJTWP6Xmw(wr}b@(uQ2gFB)hMwV}CpwNt~df!NbZ zz~od(FJ*IU+x7^09Jm)(iS!P1Y73Fp*aizglSX)!nx&TBXa_WDqcc~J4W6TCl|;&( z2NjKd=Ocr&3%*;JC;7xv6ZPA0?{gGHQU5CCuLpQ=l^Nfn;rV>LPh8#xE zt=OxU|BCi?nyM}Bb5mT!d=G*x;eAYF?V>C5WTOrLouTmiI7M~{Jw-pb>_j!KvI%?b z+SoX=%ap`>jwpB{g5HOnL}d&UK>96D5P$TWmv`4BEK9)}KCQGFCffXJ zZT+I@V51K_ii8+TjXjemSA%{T24amiP=f|4BrN78KGdrxf&fkW!k6pH8?6Tian=z@ z{7miji3i3*zmU;yv&yOy1%OYl6&@rt!?qRNRWN zt87Ut&wy|MDJX@SjKET#RREhu?2fXd6P$QU1y!-<0L;-rhzbSCaQeW~Z}Yr1OJ{p9 zsp77cCD!{;ZTj_c9)D*GW1^S3QgJrZ{94K*uA-nIfc3cZ!m}Y7=H^@OJ|z)hd3~#8 zBOmSf3lg4{StTX6`jjT8Ymyitp<0*Nsx^_6Oa6R+JScyx^_*~um+pO=e39NrQVu&ZGtPr5AN&hN)jzo?{J z4bbmmMG}NOH-D^O3;DgXTGO}a&CF&KYU8WS(_GMEQSr^?!Mn0vX=UX)Ro?~Vk1vVg zv(Ndr_CAe)hF@-vt12i`+EuGNWVoykF(=}9_xFW+=7Dpsi*t0=j@do=b*M?-JM&aO zrt0VSLruDW_DGW^fe1lm|FE6EhuHGJL>}`)5r4xS^;3m=ZYZpJ#Q}#r}ye-5}tse!;HL2tS{LecYajL%a;>H|O5 zg~A=hXI&YYMvBV=GYQyArFn%X$gM!1mC9_myEJME+BxB!W0!x3l=AQC#vx zyYJ~@lglRw0RW|`?~QlDG7Y`l5KterFu!rz{eA0|AU2wZ?Hzk|xtGr5=)W6W6ASN` z&UJx#Y=b$jW5}pC;GW()>}u~bVMxK+Ok~H66b|jJcDwnog5FfPntX2q zmssMBoV1zQInao&iEo*XL6Cf)JMRl=!WEbCk>Rs3R-+hgLaQ)u|S(CHd^w^Tz#zzC>AJUNX9GN$69 zR^}`0cX75ZJV`G|Y?Tx=ZM~d(ftH3Uo-rkD@=ViHF|KSiA5tXr*i!l{-vL7hatfO= z2I3x?qE+G%!dK=H5#3#4m+yo|bwkFDiE1(I1o=mZ`*}e8Qqvw3TiP2awj5n9eAcww zu+#Kr1x44Gs%l1e$vT;Q-fW{$k+*!KsX(nDxvuq0S+DIigYttLkyE?AI4FyUM7951 zu;bRXp|T?LME&DfJ7ZfHcTWtEtD3-e{?`^re-VZ&E zO{59$h;cs-GeU;BcXJUTu&@25DRCWq1Sm`IS7O8aA4-eWj*_x*Pq7DAx1Qlnj>|>u zRh`y$i7uG;9E2Z>?~=UG!dKxFwcoT|es(XB6>{o;`}34B!;R0=v)q-08f|Ue50h{~ zsq#T}ox9P2n5)~T;NZ#^YchHXQqAVJ`Oe=@Ieg>v0fL*aw<&RVjK0q&F(vx=GMgkZ zQHk*6)!D>H%uOyOr~V#z|9+rO(Sn7ts7L`GOxsf_$aGO`6E8{RnBQpc6YSn1Qy=&j zHL*QR>sH&E+uI#Fy`?MySz=sz`Rv-EDlPEH`A>;tIQJim3uvSro=%_sC78qCDLPpcu0%M;?jWkooOad7zsOhu z1qac>wt+0<1}5hM6ecv#_)#$bTAa9)1vdBv$TB++59-1aOEii*>%|Rf5{o%-(ZK0c zd+Ut-y((|*mR+`HBSWDZ-74+{boaje_+jCL$MSn*iXtc^PY^oh_~4_Cle<070ad3e zxJAoBs4A0_k3#ec>c|X(K^UeY!-D{ZL-Gxkzd9yMCt{wrj&%do_r$oS-DcW+yE-Pb zru+WRwPCgE`7|6LZ~}vUhJfYV@_K;U4~*qoi=OK?v*vG5T|oao;PhMw(&(1i*_BcN zOdvIaJIM*b9-$;q7{mczorXGD~QSSaHluE8WiTdKz8e6!4d6H zaIpeuCWkA0;rR8UTxM?GowrvfrQGuZk5}Pg_H|S`Z8zj-iLCl{8O3Rv#67NN^A_;d5cptO%|*QI5M*YVi%`|$L$~>a<+Z@oV7-jY(2g#xE|>DC4R)FO>kFbn zoZI|Ew4Fa~l?9(PeW%;*>yONI==NdE3^hkrI2NMm)Z;?SKE0G9inD`?)1E0(Ki^Tn zeB-Vn2cfQ?U~awzD6WV){HrV;fbW{_?Pe(>G+#$dR$YRah2as*m`O_ zMxLnL>AL^9*48TUTWtb2|AFP^>D~y)97XH%N0twi>s(cSw<0r}zJKVG|6TDeYn;Ax zQIk)N%?5SFwo}W4NjbHx_SZQbQoTz>ReNRSHQK#Twf(<;JgN_a{VXHORR8Lng;4$a zW&`E9c9*kDdt56k#!Xpb+;ywd=fxeMEs~5s?#g+x?2Ork)0I!O_O|FKw-#2;{p1*- z|GLMP2kXb~IALLjFJq`;yJ+=fgMXYQ!DR%_Z%fq#0WV`9G^Qfxy|?{8&HPCT1xf;0 z$Xt3ro^Hg5|3{Pl+oz@cOZ3t>LNCex

K2y$h#`bcl-lhYIO9rV^aP{v-V42Subg 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 diff --git a/src/testing_data/sender.gif b/src/testing_data/sender.gif deleted file mode 100644 index ddd869b4f559832e3e5bdf25cd72241547748be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50974 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 SQ{&8;rOvIk+qWcv=>G@9*I7vb diff --git a/src/utils.go b/src/utils.go deleted file mode 100644 index d34949d..0000000 --- a/src/utils.go +++ /dev/null @@ -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 -} diff --git a/src/utils/hash.go b/src/utils/hash.go new file mode 100644 index 0000000..7adbc9c --- /dev/null +++ b/src/utils/hash.go @@ -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 +} diff --git a/src/utils_test.go b/src/utils_test.go deleted file mode 100644 index 7a26c7c..0000000 --- a/src/utils_test.go +++ /dev/null @@ -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") - }) -} diff --git a/src/zip.go b/src/zip.go deleted file mode 100644 index d8ebeb8..0000000 --- a/src/zip.go +++ /dev/null @@ -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 -} diff --git a/src/zip_test.go b/src/zip_test.go deleted file mode 100644 index 3a5012a..0000000 --- a/src/zip_test.go +++ /dev/null @@ -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") -}