Merge pull request #124 from schollz/v61

version 6.0.0
This commit is contained in:
Zack 2019-05-02 16:32:35 -07:00 committed by GitHub
commit f2e420cd2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1856 additions and 6479 deletions

BIN
.github/1.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
.github/2.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

BIN
.github/3.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

BIN
.github/4.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

View File

@ -1,17 +0,0 @@
## Expected Behavior
## Actual Behavior
## Steps to Reproduce the Problem
1.
1.
1.
## Specifications
- Version (`croc -version` or let me know latest commit in git log if using dev):
- Platform:
- Subsystem:

View File

@ -10,12 +10,10 @@ install: true
script:
- env GO111MODULE=on go build -v
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/compress
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/croc
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/crypt
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/models
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/relay
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/tcp
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/utils
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/src/zipper
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/compress
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/croc
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/crypt
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/tcp
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/utils
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v6/src/comm

118
README.md
View File

@ -4,8 +4,7 @@
src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg"
width="408px" border="0" alt="croc">
<br>
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-4.1.5-brightgreen.svg?style=flat-square" alt="Version"></a>
<img src="https://img.shields.io/badge/coverage-77%25-brightgreen.svg?style=flat-square" alt="Code coverage">
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-v6.0.0-brightgreen.svg?style=flat-square" alt="Version"></a>
<a href="https://travis-ci.org/schollz/croc"><img
src="https://img.shields.io/travis/schollz/croc.svg?style=flat-square" alt="Build
Status"></a>
@ -15,140 +14,94 @@ Status"></a>
<p align="center"><code>curl https://getcroc.schollz.com | bash</code></p>
*croc* is a tool that allows any two computers to simply and securely transfer files and folders. There are many tools that can do this but AFAIK *croc* is the only tool that is easily installed and used on any platform, *and* has secure peer-to-peer transferring, *and* has the capability to resume broken transfers. _Note:_ A new WebRTC version is in progress.
`croc` is a tool that allows any two computers to simply and securely transfer files and folders. AFAIK, *croc* is the only CLI file-transfer tool does **all** of the following:
## Overview
- allows **any two computers** to transfer data (using a relay)
- provides **end-to-end encryption** (using PAKE)
- enables easy **cross-platform** transfers (Windows, Linux, Mac)
- allows **multiple file** transfers
- allows **resuming transfers** that are interrupted
- does *not* require a server or port-forwarding
**Transmit encrypted data with a code phrase**
For more information about `croc`, see [my blog post](https://schollz.com/software/croc6).
*croc* securely transfers data using *code phrases* - a combination of three random words (mnemonicoded 4 bytes). The code phrase is shared between the sender and the recipient for password authenticated key exchange ([PAKE](https://github.com/schollz/pake)), a cryptographic method to use a shared weak key (the "code phrase") to generate a strong key for secure end-to-end encryption. By default, a code phrase can only be used once between two parties so an attacker would have a chance of less than 1 in *4 billion* to guess the code phrase correctly to steal the data. An attacker with the wrong code phrase will fail the PAKE and the sender will be notified without any data transfering. Only two people with the right code phrase will be able to computers transfer encrypted data through a relay.
**Fast data transfer through TCP**
The actual data transfer is accomplished using a relay, either using raw TCP sockets or websockets. If both computers are on the LAN network then *croc* will use a local relay, otherwise a public relay is used. All the data going through the relay is encrypted using the PAKE-generated session key, so the relay can't spy on information passing through it. The data is transferred in blocks, where each block is compressed and encrypted, and the recipient keeps track of blocks received so that it can resume the transfer if interrupted.
**Relay allows any two computers to connect**
*croc* differs from a utility like *scp* because it doesn't require any two computers to have enabled port-forwarding. Instead, *croc* will uses a relay - a temporary server setup locally (if both computers are on lan) or publicly (default is at croc4.schollz.com). Any two computers can connect to the relay, and after securing their channel with PAKE, they can transfer encrypted metadata and data through the relay. The relay works by first having the computers communicate the PAKE protocol via websockets, and then exchanging encrypted metadata, and then stapling the TCP connections directly so that they can transfer directly.
**Why another data transfer utility?**
My motivation to write *croc*, as stupid as it sounds, is because I wanted to create a program that made it easy to send a 3GB+ PBS documentary to my friend in a different country. My friend has a Windows computer and is not comfortable using a terminal. So I wanted to write a program that, while secure, is simple to receive a file. *croc* accomplishes this, and now I find myself using it almost everyday at work. To receive a file you can just download the executable and double click on it. The name is inspired by the [fable of the frog and the crocodile](https://web.archive.org/web/20180926035731/http://allaboutfrogs.org/stories/crocodile.html).
## Examples
The first example shows the basic transfer of some file or folder from computer 1 to computer 2. _These two gifs should run in sync if you force-reload (Ctl+F5)_
![send](.github/1.gif)
![receive](.github/2.gif)
The second example shows how you can restart a broken transfer. Here, computer 2 presses Ctl+C during a transfer to abruptly break the connection, and then resumes by having computer 1 re-send the file. _These two gifs should run in sync if you force-reload (Ctl+F5)_
![send](.github/3.gif)
![receive](.github/4.gif)
In version 4, there is now a GUI available for [Windows](https://github.com/schollz/croc/releases/latest) and [Linux](https://github.com/schollz/croc/releases/latest):
<div style="text-align:center">
<img src="https://user-images.githubusercontent.com/6550035/47256575-8a193e00-d437-11e8-96fa-42c9d072a8f1.PNG">
</div>
## 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`. Since *croc* uses [Go modules](https://golang.org/doc/go1.11#modules) it requires Go version 1.11+.
Or, you can quickly install a release from the command-line:
Download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:
```
$ curl https://getcroc.schollz.com | bash
```
Or, you can [install Go](https://golang.org/dl/) and build from source (requires Go 1.11+):
```
$ wget -qO- https://getcroc.schollz.com | bash
go get github.com/schollz/croc
```
## Usage
The basic usage is to just do
To send a file, simply do:
```
$ croc send FILE
Sending 'FILE' (X MB)
Code is: code-phrase
```
to send and then on the other computer you can just do
Them to receive the file on another computer, you can just do
```
$ croc [code phrase]
$ croc code-phrase
```
to receive (you'll be prompted to enter the code phrase). Note, by default, you don't need any arguments for receiving, instead you will be prompted to enter the code phrase. This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
The code phrase is used to establish password-authenticated key agreement ([PAKE](https://en.wikipedia.org/wiki/Password-authenticated_key_agreement)) which generates a secret key for the sender and recipient to use for end-to-end encryption.
### Custom code phrase
You can send with your own code phrase (must be more than 4 characters).
```
$ croc send --code [code phrase] [filename]
$ croc send --code [code-phrase] [filename]
```
### Use locally
*croc* automatically will attempt to start a local connection on your LAN to transfer the file much faster. It uses [peer discovery](https://github.com/schollz/peerdiscovery), basically broadcasting a message on the local subnet to see if another *croc* user wants to receive the file. *croc* will utilize the first incoming connection from either the local network or the public relay and follow through with PAKE.
### Use pipes - stdin and stdout
You can change this behavior by forcing *croc* to use only local connections (`--local`) or force to use the public relay only (`--no-local`):
```
$ croc --local/--no-local send [filename]
```
### Using pipes - stdin and stdout
You can easily use *croc* in pipes when you need to send data through stdin or get data from stdout. To send you can just use pipes:
You can pipe to `croc`:
```
$ cat [filename] | croc send
```
In this case *croc* will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789". To receive to stdout at you can always just use the `-yes` and `-stdout` flags which will automatically approve the transfer and pipe it out to stdout.
In this case `croc` will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789". To receive to `stdout` at you can always just use the `--yes` and `--stdout` flags which will automatically approve the transfer and pipe it out to `stdout`.
```
$ croc -yes -stdout [code phrase] > out
$ croc --yes --stdout [code-phrase] > out
```
All of the other text printed to the console is going to `stderr` so it will not interfere with the message going to stdout.
All of the other text printed to the console is going to `stderr` so it will not interfere with the message going to `stdout`.
### Self-host relay
The relay is needed 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, `croc4.schollz.com`. You can also run your own relay, it is very easy, just run:
The relay is needed to staple the parallel incoming and outgoing connections. By default, `croc` uses a public relay but you can also run your own relay:
```
$ croc relay
```
Make sure to open up TCP ports (see `croc relay --help` for which ports to open). Relays can also be customized to which elliptic curve they will use (default is siec).
Make sure to open up TCP ports (see `croc relay --help` for which ports to open).
You can send files using your relay by entering `-addr` to change the relay that you are using if you want to custom host your own.
You can send files using your relay by entering `--relay` to change the relay that you are using if you want to custom host your own.
```
$ croc -addr "myrelay.example.com" send [filename]
$ croc --relay "myrelay.example.com:9009" send [filename]
```
### Configuration file
You can also make some paramters static by using a configuration file. To get started with the config file just do
```
$ croc config
```
which will generate the file that you can edit.
Any changes you make to the configuration file will be applied *before* the command-line flags, if any.
## License
@ -156,11 +109,6 @@ MIT
## Acknowledgements
*croc* has been through many iterations, and I am awed by all the great contributions! 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)).
`croc` has been through many iterations, and I am awed by all the great contributions! 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)).
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), [@nicolashardy](https://github.com/nicolashardy)!
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/). Finally thanks 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), [@nicolashardy](https://github.com/nicolashardy)!

19
go.mod
View File

@ -1,27 +1,20 @@
module github.com/schollz/croc
module github.com/schollz/croc/v6
require (
github.com/BurntSushi/toml v0.3.1
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
github.com/dustin/go-humanize v1.0.0
github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/color v1.7.0 // indirect
github.com/gonutz/w32 v1.0.0
github.com/gonutz/wui v1.0.0
github.com/gorilla/websocket v1.4.0
github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.8.1
github.com/schollz/mnemonicode v1.0.1
github.com/schollz/pake v1.1.0
github.com/schollz/peerdiscovery v1.4.0
github.com/schollz/progressbar/v2 v2.10.0
github.com/schollz/progressbar/v2 v2.12.1
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9
github.com/skratchdot/open-golang v0.0.0-20190104022628-a2dfa6d0dab6
github.com/stretchr/testify v1.3.0
github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
golang.org/x/sys v0.0.0-20190312061237-fead79001313 // indirect
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 // indirect
golang.org/x/text v0.3.2 // indirect
)

59
go.sum Normal file
View File

@ -0,0 +1,59 @@
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs=
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mitchellh/colorstring v0.0.0-20150917214807-8631ce90f286/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/schollz/mnemonicode v1.0.1 h1:LiH5hwADZwjwnfXsaD4xgnMyTAtaKHN+e5AyjRU6WSU=
github.com/schollz/mnemonicode v1.0.1/go.mod h1:cl4UAOhUV0mkdjMj/QYaUZbZZdF8BnOqoz8rHMzwboY=
github.com/schollz/pake v1.1.0 h1:+tYqsPVkuirFpmeRePjYTUhIHHKLufdmd7QfuspaXCk=
github.com/schollz/pake v1.1.0/go.mod h1:pL7Z08gnQ4OQ3G27s5e5T6TEzp6cFc5GzCwLm0f75Io=
github.com/schollz/peerdiscovery v1.4.0 h1:wJWiJUBSMY2io9eIG1+gauXm8WD6sJVN5M+pLd4fYZQ=
github.com/schollz/peerdiscovery v1.4.0/go.mod h1:DXj/7VvxAkUuSZNabx3q8t524uWbrhMPxeX151kvvHs=
github.com/schollz/progressbar/v2 v2.9.1/go.mod h1:l6tn6yU6ZdQoF8lwX/VoAUQ3FjhCbrcZDnl9xeWZzYw=
github.com/schollz/progressbar/v2 v2.12.1 h1:0Ce7IBClG+s3lxXN1Noqwh7aToKGL5a3mnMfPJqDlv4=
github.com/schollz/progressbar/v2 v2.12.1/go.mod h1:fBI3onORwtNtwCWJHsrXtjE3QnJOtqIZrvr3rDaF7L0=
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9 h1:y08o5oQ/slxXE/F0uh5dd8mdVvb+w4NLcNSDSq4c2F0=
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9/go.mod h1:kCMoQsqzx4MzGJWaALr6tKyCnlrY0kILGLkA1FOiLF4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937 h1:lhssCpSe3TjKcbvUoPzFMuv9oUyZDgI3Cmgolfw2C90=
github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

12
main.go
View File

@ -1,12 +1,14 @@
package main
//go:generate go run src/install/updateversion.go
import (
"github.com/schollz/croc/src/cli"
"fmt"
"github.com/schollz/croc/v6/src/cli"
)
var Version string
func main() {
cli.Version = Version
cli.Run()
if err := cli.Run(); err != nil {
fmt.Println(err)
}
}

View File

@ -1,6 +1,7 @@
package cli
import (
"errors"
"fmt"
"io"
"io/ioutil"
@ -11,25 +12,23 @@ import (
"strings"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/pkg/errors"
"github.com/schollz/croc/src/croc"
"github.com/schollz/croc/src/utils"
"github.com/skratchdot/open-golang/open"
"github.com/schollz/croc/v6/src/croc"
"github.com/schollz/croc/v6/src/tcp"
"github.com/schollz/croc/v6/src/utils"
"github.com/urfave/cli"
)
var Version string
var cr *croc.Croc
func Run() {
func Run() (err error) {
// use all of the processors
runtime.GOMAXPROCS(runtime.NumCPU())
app := cli.NewApp()
app.Name = "croc"
if Version == "" {
Version = "dev"
Version = "v6.0.0"
}
app.Version = Version
app.Compiled = time.Now()
app.Usage = "easily and securely transfer stuff from one computer to another"
@ -42,9 +41,8 @@ func Run() {
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"},
cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the local relay (optional)"},
},
HelpName: "croc send",
Action: func(c *cli.Context) error {
@ -53,39 +51,21 @@ func Run() {
},
{
Name: "relay",
Usage: "start a croc relay",
Description: "the croc relay will handle websocket and TCP connections",
Flags: []cli.Flag{},
Description: "start relay",
HelpName: "croc relay",
Action: func(c *cli.Context) error {
return relay(c)
},
},
{
Name: "config",
Usage: "generates a config file",
Description: "the croc config can be used to set static parameters",
Flags: []cli.Flag{},
HelpName: "croc config",
Action: func(c *cli.Context) error {
return saveDefaultConfig(c)
Flags: []cli.Flag{
cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"},
},
},
}
app.Flags = []cli.Flag{
cli.StringFlag{Name: "addr", Value: "croc4.schollz.com", Usage: "address of the public relay"},
cli.StringFlag{Name: "addr-ws", Value: "8153", Usage: "port of the public relay websocket server to connect"},
cli.StringFlag{Name: "addr-tcp", Value: "8154,8155,8156,8157,8158,8159,8160,8161", Usage: "tcp ports of the public relay server to connect"},
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"},
cli.BoolFlag{Name: "force-tcp", Usage: "force TCP"},
cli.BoolFlag{Name: "force-web", Usage: "force websockets"},
cli.StringFlag{Name: "port", Value: "8153", Usage: "port that the websocket listens on"},
cli.StringFlag{Name: "tcp-port", Value: "8154,8155,8156,8157,8158,8159,8160,8161", Usage: "ports that the tcp server listens on"},
cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use for PAKE (p256, p384, p521, siec)"},
cli.StringFlag{Name: "relay", Value: "198.199.67.130:9009", Usage: "address of the relay"},
cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
}
app.EnableBashCompletion = true
@ -105,44 +85,18 @@ func Run() {
}
return receive(c)
}
app.Before = func(c *cli.Context) error {
cr = croc.Init(c.GlobalBool("debug"))
cr.Version = Version
cr.AllowLocalDiscovery = true
cr.Address = c.GlobalString("addr")
cr.AddressTCPPorts = strings.Split(c.GlobalString("addr-tcp"), ",")
cr.AddressWebsocketPort = c.GlobalString("addr-ws")
cr.NoRecipientPrompt = c.GlobalBool("yes")
cr.Stdout = c.GlobalBool("stdout")
cr.LocalOnly = c.GlobalBool("local")
cr.NoLocal = c.GlobalBool("no-local")
cr.ShowText = true
cr.RelayWebsocketPort = c.String("port")
cr.RelayTCPPorts = strings.Split(c.String("tcp-port"), ",")
cr.CurveType = c.String("curve")
if c.GlobalBool("force-tcp") {
cr.ForceSend = 2
}
if c.GlobalBool("force-web") {
cr.ForceSend = 1
}
return nil
}
err := app.Run(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "\r\n%s", err.Error())
}
fmt.Fprintf(os.Stderr, "\r\n")
return app.Run(os.Args)
}
func saveDefaultConfig(c *cli.Context) error {
return croc.SaveDefaultConfig()
}
// func saveDefaultConfig(c *cli.Context) error {
// return croc.SaveDefaultConfig()
// }
func send(c *cli.Context) error {
func send(c *cli.Context) (err error) {
var fnames []string
stat, _ := os.Stdin.Stat()
var fname string
if (stat.Mode() & os.ModeCharDevice) == 0 {
f, err := ioutil.TempFile(".", "croc-stdin-")
if err != nil {
@ -156,102 +110,135 @@ func send(c *cli.Context) error {
if err != nil {
return err
}
fname = f.Name()
fnames = []string{f.Name()}
defer func() {
err = os.Remove(fname)
err = os.Remove(fnames[0])
if err != nil {
log.Println(err)
}
}()
} else {
fname = c.Args().First()
fnames = append([]string{c.Args().First()}, c.Args().Tail()...)
}
if fname == "" {
if len(fnames) == 0 {
return errors.New("must specify file: croc send [filename]")
}
cr.UseCompression = !c.Bool("no-compress")
cr.UseEncryption = !c.Bool("no-encrypt")
var sharedSecret string
if c.String("code") != "" {
cr.Codephrase = c.String("code")
sharedSecret = c.String("code")
}
cr.LoadConfig()
if len(cr.Codephrase) == 0 {
// cr.LoadConfig()
if len(sharedSecret) == 0 {
// generate code phrase
cr.Codephrase = utils.GetRandomName()
sharedSecret = utils.GetRandomName()
}
// print the text
finfo, err := os.Stat(fname)
if err != nil {
return err
}
fname, _ = filepath.Abs(fname)
fname = filepath.Clean(fname)
_, filename := filepath.Split(fname)
fileOrFolder := "file"
fsize := finfo.Size()
if finfo.IsDir() {
fileOrFolder = "folder"
fsize, err = dirSize(fname)
haveFolder := false
paths := []string{}
for _, fname := range fnames {
stat, err := os.Stat(fname)
if err != nil {
return err
}
if stat.IsDir() {
haveFolder = true
err = filepath.Walk(fname,
func(pathName string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
paths = append(paths, filepath.ToSlash(pathName))
}
return nil
})
if err != nil {
return err
}
} else {
paths = append(paths, filepath.ToSlash(fname))
}
}
fmt.Fprintf(os.Stderr,
"Sending %s %s named '%s'\nCode is: %s\nOn the other computer, please run:\n\ncroc %s\n\n",
humanize.Bytes(uint64(fsize)),
fileOrFolder,
filename,
cr.Codephrase,
cr.Codephrase,
)
if cr.Debug {
croc.SetDebugLevel("debug")
cr, err := croc.New(croc.Options{
SharedSecret: sharedSecret,
IsSender: true,
Debug: c.GlobalBool("debug"),
NoPrompt: c.GlobalBool("yes"),
RelayAddress: c.GlobalString("relay"),
Stdout: c.GlobalBool("stdout"),
RelayPorts: strings.Split(c.String("ports"), ","),
})
if err != nil {
return
}
return cr.Send(fname, cr.Codephrase)
err = cr.Send(croc.TransferOptions{
PathToFiles: paths,
KeepPathInRemote: haveFolder,
})
return
}
func receive(c *cli.Context) error {
func receive(c *cli.Context) (err error) {
var sharedSecret string
if c.GlobalString("code") != "" {
cr.Codephrase = c.GlobalString("code")
sharedSecret = c.GlobalString("code")
}
if c.Args().First() != "" {
cr.Codephrase = c.Args().First()
sharedSecret = c.Args().First()
}
if sharedSecret == "" {
sharedSecret = utils.GetInput("Enter receive code: ")
}
if c.GlobalString("out") != "" {
os.Chdir(c.GlobalString("out"))
}
cr.LoadConfig()
openFolder := false
if len(os.Args) == 1 {
// open folder since they didn't give any arguments
openFolder = true
}
if cr.Codephrase == "" {
cr.Codephrase = utils.GetInput("Enter receive code: ")
}
if cr.Debug {
croc.SetDebugLevel("debug")
}
err := cr.Receive(cr.Codephrase)
if err == nil && openFolder {
cwd, _ := os.Getwd()
open.Run(cwd)
}
return err
}
func relay(c *cli.Context) error {
return cr.Relay()
}
func dirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if !info.IsDir() {
size += info.Size()
}
return err
cr, err := croc.New(croc.Options{
SharedSecret: sharedSecret,
IsSender: false,
Debug: c.GlobalBool("debug"),
NoPrompt: c.GlobalBool("yes"),
RelayAddress: c.GlobalString("relay"),
Stdout: c.GlobalBool("stdout"),
})
return size, err
if err != nil {
return
}
err = cr.Receive()
return
}
func relay(c *cli.Context) (err error) {
debugString := "info"
if c.GlobalBool("debug") {
debugString = "debug"
}
ports := strings.Split(c.String("ports"), ",")
tcpPorts := strings.Join(ports[1:], ",")
for i, port := range ports {
if i == 0 {
continue
}
go func(portStr string) {
err = tcp.Run(debugString, portStr)
if err != nil {
panic(err)
}
}(port)
}
return tcp.Run(debugString, ports[0], tcpPorts)
}
// func dirSize(path string) (int64, error) {
// var size int64
// err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
// if !info.IsDir() {
// size += info.Size()
// }
// return err
// })
// return size, err
// }

View File

@ -2,10 +2,9 @@ package comm
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
@ -16,29 +15,43 @@ type Comm struct {
connection net.Conn
}
// NewConnection gets a new comm to a tcp address
func NewConnection(address string) (c *Comm, err error) {
connection, err := net.DialTimeout("tcp", address, 30*time.Second)
if err != nil {
return
}
c = New(connection)
return
}
// New returns a new comm
func New(c net.Conn) Comm {
func New(c net.Conn) *Comm {
c.SetReadDeadline(time.Now().Add(3 * time.Hour))
c.SetDeadline(time.Now().Add(3 * time.Hour))
c.SetWriteDeadline(time.Now().Add(3 * time.Hour))
return Comm{c}
comm := new(Comm)
comm.connection = c
return comm
}
// Connection returns the net.Conn connection
func (c Comm) Connection() net.Conn {
func (c *Comm) Connection() net.Conn {
return c.connection
}
// Close closes the connection
func (c Comm) Close() {
func (c *Comm) Close() {
c.connection.Close()
}
func (c Comm) Write(b []byte) (int, error) {
tmpCopy := make([]byte, len(b)+5)
// Copy the buffer so it doesn't get changed while read by the recipient.
copy(tmpCopy[:5], []byte(fmt.Sprintf("%0.5d", len(b))))
copy(tmpCopy[5:], b)
func (c *Comm) Write(b []byte) (int, error) {
header := new(bytes.Buffer)
err := binary.Write(header, binary.LittleEndian, uint32(len(b)))
if err != nil {
fmt.Println("binary.Write failed:", err)
}
tmpCopy := append(header.Bytes(), b...)
n, err := c.connection.Write(tmpCopy)
if n != len(tmpCopy) {
if err != nil {
@ -51,70 +64,55 @@ func (c Comm) Write(b []byte) (int, error) {
return n, err
}
func (c Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
// read until we get 5 bytes
tmp := make([]byte, 5)
n, err := c.connection.Read(tmp)
if err != nil {
return
}
tmpCopy := make([]byte, n)
// Copy the buffer so it doesn't get changed while read by the recipient.
copy(tmpCopy, tmp[:n])
bs = tmpCopy
tmp = make([]byte, 1)
func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
// read until we get 4 bytes for the header
var header []byte
numBytes = 4
for {
// see if we have enough bytes
bs = bytes.Trim(bs, "\x00")
if len(bs) == 5 {
break
tmp := make([]byte, numBytes-len(header))
n, errRead := c.connection.Read(tmp)
if errRead != nil {
err = errRead
return
}
n, err := c.connection.Read(tmp)
if err != nil {
return nil, 0, nil, err
}
tmpCopy = make([]byte, n)
// Copy the buffer so it doesn't get changed while read by the recipient.
copy(tmpCopy, tmp[:n])
bs = append(bs, tmpCopy...)
}
numBytes, err = strconv.Atoi(strings.TrimLeft(string(bs), "0"))
if err != nil {
return nil, 0, nil, err
}
buf = []byte{}
tmp = make([]byte, numBytes)
for {
n, err := c.connection.Read(tmp)
if err != nil {
return nil, 0, nil, err
}
tmpCopy = make([]byte, n)
// Copy the buffer so it doesn't get changed while read by the recipient.
copy(tmpCopy, tmp[:n])
buf = append(buf, bytes.TrimRight(tmpCopy, "\x00")...)
if len(buf) < numBytes {
// shrink the amount we need to read
tmp = tmp[:numBytes-len(buf)]
} else {
header = append(header, tmp[:n]...)
if numBytes == len(header) {
break
}
}
var numBytesUint32 uint32
rbuf := bytes.NewReader(header)
err = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32)
if err != nil {
fmt.Println("binary.Read failed:", err)
}
numBytes = int(numBytesUint32)
buf = make([]byte, 0)
for {
// log.Debugf("bytes: %d/%d",len(buf),numBytes)
tmp := make([]byte, numBytes-len(buf))
n, errRead := c.connection.Read(tmp)
if errRead != nil {
err = errRead
return
}
buf = append(buf, tmp[:n]...)
if numBytes == len(buf) {
break
}
}
// log.Printf("wanted %d and got %d", numBytes, len(buf))
return
}
// Send a message
func (c Comm) Send(message string) (err error) {
_, err = c.Write([]byte(message))
func (c *Comm) Send(message []byte) (err error) {
_, err = c.Write(message)
return
}
// Receive a message
func (c Comm) Receive() (s string, err error) {
b, _, _, err := c.Read()
s = string(b)
func (c *Comm) Receive() (b []byte, err error) {
b, _, _, err = c.Read()
return
}

61
src/comm/comm_test.go Normal file
View File

@ -0,0 +1,61 @@
package comm
import (
"crypto/rand"
"net"
"testing"
"time"
log "github.com/cihub/seelog"
"github.com/stretchr/testify/assert"
)
func TestComm(t *testing.T) {
defer log.Flush()
token := make([]byte, 40000000)
rand.Read(token)
port := "8001"
go func() {
log.Debugf("starting TCP server on " + port)
server, err := net.Listen("tcp", "0.0.0.0:"+port)
if err != nil {
log.Error(err)
}
defer server.Close()
// spawn a new goroutine whenever a client connects
for {
connection, err := server.Accept()
if err != nil {
log.Error(err)
}
log.Debugf("client %s connected", connection.RemoteAddr().String())
go func(port string, connection net.Conn) {
c := New(connection)
err = c.Send([]byte("hello, world"))
assert.Nil(t, err)
data, err := c.Receive()
assert.Nil(t, err)
assert.Equal(t, []byte("hello, computer"), data)
data, err = c.Receive()
assert.Nil(t, err)
assert.Equal(t, []byte{'\x00'}, data)
data, err = c.Receive()
assert.Nil(t, err)
assert.Equal(t, token, data)
}(port, connection)
}
}()
time.Sleep(300 * time.Millisecond)
a, err := NewConnection("localhost:" + port)
assert.Nil(t, err)
data, err := a.Receive()
assert.Equal(t, []byte("hello, world"), data)
assert.Nil(t, err)
assert.Nil(t, a.Send([]byte("hello, computer")))
assert.Nil(t, a.Send([]byte{'\x00'}))
assert.Nil(t, a.Send(token))
}

View File

@ -75,4 +75,16 @@ func TestCompress(t *testing.T) {
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
assert.True(t, len(compressedB) < len(fable))
data := make([]byte, 4096)
rand.Read(data)
compressedB = CompressWithOption(data, -2)
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
fmt.Printf("random, Level -2: %2.0f%% percent space savings\n", dataRateSavings)
rand.Read(data)
compressedB = CompressWithOption(data, 9)
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
fmt.Printf("random, Level 9: %2.0f%% percent space savings\n", dataRateSavings)
}

View File

@ -1,187 +0,0 @@
package croc
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"time"
"github.com/BurntSushi/toml"
homedir "github.com/mitchellh/go-homedir"
"github.com/schollz/croc/src/utils"
)
type Config struct {
// Relay parameters
RelayWebsocketPort string
RelayTCPPorts []string
// Sender parameters
CurveType string
// Options for connecting to server
PublicServerIP string
AddressTCPPorts []string
AddressWebsocketPort string
Timeout time.Duration
LocalOnly bool
NoLocal bool
// Options for file transfering
UseEncryption bool
UseCompression bool
AllowLocalDiscovery bool
NoRecipientPrompt bool
ForceTCP bool
ForceWebsockets bool
Codephrase string
}
func defaultConfig() Config {
c := Config{}
cr := Init(false)
c.RelayWebsocketPort = cr.RelayWebsocketPort
c.RelayTCPPorts = cr.RelayTCPPorts
c.CurveType = cr.CurveType
c.PublicServerIP = cr.Address
c.AddressTCPPorts = cr.AddressTCPPorts
c.AddressWebsocketPort = cr.AddressWebsocketPort
c.Timeout = cr.Timeout
c.LocalOnly = cr.LocalOnly
c.NoLocal = cr.NoLocal
c.UseEncryption = cr.UseEncryption
c.UseCompression = cr.UseCompression
c.AllowLocalDiscovery = cr.AllowLocalDiscovery
c.NoRecipientPrompt = cr.NoRecipientPrompt
c.ForceTCP = false
c.ForceWebsockets = false
c.Codephrase = ""
return c
}
func SaveDefaultConfig() error {
homedir, err := homedir.Dir()
if err != nil {
return err
}
os.MkdirAll(path.Join(homedir, ".config", "croc"), 0755)
c := defaultConfig()
buf := new(bytes.Buffer)
toml.NewEncoder(buf).Encode(c)
confTOML := buf.String()
err = ioutil.WriteFile(path.Join(homedir, ".config", "croc", "config.toml"), []byte(confTOML), 0644)
if err == nil {
fmt.Printf("Default config file written at '%s'\r\n", filepath.Clean(path.Join(homedir, ".config", "croc", "config.toml")))
}
return err
}
// LoadConfig will override parameters
func (cr *Croc) LoadConfig() (err error) {
homedir, err := homedir.Dir()
if err != nil {
return err
}
pathToConfig := path.Join(homedir, ".config", "croc", "config.toml")
if !utils.Exists(pathToConfig) {
// ignore if doesn't exist
return nil
}
var c Config
_, err = toml.DecodeFile(pathToConfig, &c)
if err != nil {
return
}
cDefault := defaultConfig()
// only load if things are different than defaults
// just in case the CLI parameters are used
if c.RelayWebsocketPort != cDefault.RelayWebsocketPort && cr.RelayWebsocketPort == cDefault.RelayWebsocketPort {
cr.RelayWebsocketPort = c.RelayWebsocketPort
fmt.Printf("loaded RelayWebsocketPort from config\n")
}
if !slicesEqual(c.RelayTCPPorts, cDefault.RelayTCPPorts) && slicesEqual(cr.RelayTCPPorts, cDefault.RelayTCPPorts) {
cr.RelayTCPPorts = c.RelayTCPPorts
fmt.Printf("loaded RelayTCPPorts from config\n")
}
if c.CurveType != cDefault.CurveType && cr.CurveType == cDefault.CurveType {
cr.CurveType = c.CurveType
fmt.Printf("loaded CurveType from config\n")
}
if c.PublicServerIP != cDefault.PublicServerIP && cr.Address == cDefault.PublicServerIP {
cr.Address = c.PublicServerIP
fmt.Printf("loaded Address from config\n")
}
if !slicesEqual(c.AddressTCPPorts, cDefault.AddressTCPPorts) {
cr.AddressTCPPorts = c.AddressTCPPorts
fmt.Printf("loaded AddressTCPPorts from config\n")
}
if c.AddressWebsocketPort != cDefault.AddressWebsocketPort && cr.AddressWebsocketPort == cDefault.AddressWebsocketPort {
cr.AddressWebsocketPort = c.AddressWebsocketPort
fmt.Printf("loaded AddressWebsocketPort from config\n")
}
if c.Timeout != cDefault.Timeout && cr.Timeout == cDefault.Timeout {
cr.Timeout = c.Timeout
fmt.Printf("loaded Timeout from config\n")
}
if c.LocalOnly != cDefault.LocalOnly && cr.LocalOnly == cDefault.LocalOnly {
cr.LocalOnly = c.LocalOnly
fmt.Printf("loaded LocalOnly from config\n")
}
if c.NoLocal != cDefault.NoLocal && cr.NoLocal == cDefault.NoLocal {
cr.NoLocal = c.NoLocal
fmt.Printf("loaded NoLocal from config\n")
}
if c.UseEncryption != cDefault.UseEncryption && cr.UseEncryption == cDefault.UseEncryption {
cr.UseEncryption = c.UseEncryption
fmt.Printf("loaded UseEncryption from config\n")
}
if c.UseCompression != cDefault.UseCompression && cr.UseCompression == cDefault.UseCompression {
cr.UseCompression = c.UseCompression
fmt.Printf("loaded UseCompression from config\n")
}
if c.AllowLocalDiscovery != cDefault.AllowLocalDiscovery && cr.AllowLocalDiscovery == cDefault.AllowLocalDiscovery {
cr.AllowLocalDiscovery = c.AllowLocalDiscovery
fmt.Printf("loaded AllowLocalDiscovery from config\n")
}
if c.NoRecipientPrompt != cDefault.NoRecipientPrompt && cr.NoRecipientPrompt == cDefault.NoRecipientPrompt {
cr.NoRecipientPrompt = c.NoRecipientPrompt
fmt.Printf("loaded NoRecipientPrompt from config\n")
}
if c.ForceWebsockets {
cr.ForceSend = 1
}
if c.ForceTCP {
cr.ForceSend = 2
}
if c.Codephrase != cDefault.Codephrase && cr.Codephrase == cDefault.Codephrase {
cr.Codephrase = c.Codephrase
fmt.Printf("loaded Codephrase from config\n")
}
return
}
// slicesEqual checcks if two slices are equal
// from https://stackoverflow.com/a/15312097
func slicesEqual(a, b []string) bool {
// If one is nil, the other must also be nil.
if (a == nil) != (b == nil) {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@ -1,96 +1,842 @@
package croc
import (
"runtime"
"bytes"
"crypto/elliptic"
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/models"
"github.com/schollz/croc/src/relay"
"github.com/schollz/croc/src/zipper"
log "github.com/cihub/seelog"
"github.com/denisbrodbeck/machineid"
"github.com/pkg/errors"
"github.com/schollz/croc/v6/src/comm"
"github.com/schollz/croc/v6/src/compress"
"github.com/schollz/croc/v6/src/crypt"
"github.com/schollz/croc/v6/src/logger"
"github.com/schollz/croc/v6/src/message"
"github.com/schollz/croc/v6/src/models"
"github.com/schollz/croc/v6/src/tcp"
"github.com/schollz/croc/v6/src/utils"
"github.com/schollz/pake"
"github.com/schollz/peerdiscovery"
"github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
)
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
logger.SetLogLevel("debug")
}
// Croc options
type Croc struct {
// Version is the version of croc
Version string
// Options for all
Debug bool
// ShowText will display text on the stderr
ShowText bool
// Options for relay
RelayWebsocketPort string
RelayTCPPorts []string
CurveType string
// Options for connecting to server
Address string
AddressTCPPorts []string
AddressWebsocketPort string
Timeout time.Duration
LocalOnly bool
NoLocal bool
// Options for file transfering
UseEncryption bool
UseCompression bool
AllowLocalDiscovery bool
NoRecipientPrompt bool
Stdout bool
ForceSend int // 0: ignore, 1: websockets, 2: TCP
// Parameters for file transfer
Filename string
Codephrase string
// localIP address
localIP string
// is using local relay
isLocal bool
normalFinish bool
// state variables
StateString string
Bar *progressbar.ProgressBar
FileInfo models.FileStats
OtherIP string
// special for window
WindowRecipientPrompt bool
WindowRecipientAccept bool
WindowReceivingString string
}
// Init will initiate with the default parameters
func Init(debug bool) (c *Croc) {
c = new(Croc)
c.UseCompression = true
c.UseEncryption = true
c.AllowLocalDiscovery = true
c.RelayWebsocketPort = "8153"
c.RelayTCPPorts = []string{"8154", "8155", "8156", "8157", "8158", "8159", "8160", "8161"}
c.CurveType = "siec"
c.Address = "croc4.schollz.com"
c.AddressWebsocketPort = "8153"
c.AddressTCPPorts = []string{"8154", "8155", "8156", "8157", "8158", "8159", "8160", "8161"}
c.NoRecipientPrompt = true
debugLevel := "info"
func Debug(debug bool) {
if debug {
debugLevel = "debug"
c.Debug = true
logger.SetLogLevel("debug")
} else {
logger.SetLogLevel("warn")
}
SetDebugLevel(debugLevel)
}
type Options struct {
IsSender bool
SharedSecret string
Debug bool
RelayAddress string
RelayPorts []string
Stdout bool
NoPrompt bool
DisableLocal bool
}
type Client struct {
Options Options
Pake *pake.Pake
Key crypt.Encryption
ExternalIP, ExternalIPConnected string
// steps involved in forming relationship
Step1ChannelSecured bool
Step2FileInfoTransfered bool
Step3RecipientRequestFile bool
Step4FileTransfer bool
Step5CloseChannels bool
// send / receive information of all files
FilesToTransfer []FileInfo
FilesToTransferCurrentNum int
// send / receive information of current file
CurrentFile *os.File
CurrentFileChunks []int64
TotalSent int64
TotalChunksTransfered int
chunkMap map[uint64]struct{}
// tcp connections
conn []*comm.Comm
bar *progressbar.ProgressBar
spinner *spinner.Spinner
machineID string
firstSend bool
mutex *sync.Mutex
quit chan bool
}
type Chunk struct {
Bytes []byte `json:"b,omitempty"`
Location int64 `json:"l,omitempty"`
}
type FileInfo struct {
Name string `json:"n,omitempty"`
FolderRemote string `json:"fr,omitempty"`
FolderSource string `json:"fs,omitempty"`
Hash []byte `json:"h,omitempty"`
Size int64 `json:"s,omitempty"`
ModTime time.Time `json:"m,omitempty"`
IsCompressed bool `json:"c,omitempty"`
IsEncrypted bool `json:"e,omitempty"`
}
type RemoteFileRequest struct {
CurrentFileChunks []int64
FilesToTransferCurrentNum int
}
type SenderInfo struct {
MachineID string
FilesToTransfer []FileInfo
}
// New establishes a new connection for transfering files between two instances.
func New(ops Options) (c *Client, err error) {
c = new(Client)
// setup basic info
c.Options = ops
Debug(c.Options.Debug)
log.Debugf("options: %+v", c.Options)
c.conn = make([]*comm.Comm, 16)
// use default key (no encryption, until PAKE succeeds)
c.Key, err = crypt.New(nil, nil)
if err != nil {
return
}
// initialize pake
if c.Options.IsSender {
c.Pake, err = pake.Init([]byte(c.Options.SharedSecret), 1, elliptic.P521(), 1*time.Microsecond)
} else {
c.Pake, err = pake.Init([]byte(c.Options.SharedSecret), 0, elliptic.P521(), 1*time.Microsecond)
}
if err != nil {
return
}
c.mutex = &sync.Mutex{}
return
}
func SetDebugLevel(debugLevel string) {
logger.SetLogLevel(debugLevel)
relay.DebugLevel = debugLevel
zipper.DebugLevel = debugLevel
// TransferOptions for sending
type TransferOptions struct {
PathToFiles []string
KeepPathInRemote bool
}
// Send will send the specified file
func (c *Client) Send(options TransferOptions) (err error) {
c.FilesToTransfer = make([]FileInfo, len(options.PathToFiles))
totalFilesSize := int64(0)
for i, pathToFile := range options.PathToFiles {
var fstats os.FileInfo
var fullPath string
fullPath, err = filepath.Abs(pathToFile)
if err != nil {
return
}
fullPath = filepath.Clean(fullPath)
var folderName string
folderName, _ = filepath.Split(fullPath)
fstats, err = os.Stat(fullPath)
if err != nil {
return
}
c.FilesToTransfer[i] = FileInfo{
Name: fstats.Name(),
FolderRemote: ".",
FolderSource: folderName,
Size: fstats.Size(),
ModTime: fstats.ModTime(),
}
c.FilesToTransfer[i].Hash, err = utils.HashFile(fullPath)
totalFilesSize += fstats.Size()
if err != nil {
return
}
if options.KeepPathInRemote {
var curFolder string
curFolder, err = os.Getwd()
if err != nil {
return
}
curFolder, err = filepath.Abs(curFolder)
if err != nil {
return
}
if !strings.HasPrefix(folderName, curFolder) {
err = fmt.Errorf("remote directory must be relative to current")
return
}
c.FilesToTransfer[i].FolderRemote = strings.TrimPrefix(folderName, curFolder)
c.FilesToTransfer[i].FolderRemote = filepath.ToSlash(c.FilesToTransfer[i].FolderRemote)
c.FilesToTransfer[i].FolderRemote = strings.TrimPrefix(c.FilesToTransfer[i].FolderRemote, "/")
if c.FilesToTransfer[i].FolderRemote == "" {
c.FilesToTransfer[i].FolderRemote = "."
}
}
log.Debugf("file %d info: %+v", i, c.FilesToTransfer[i])
}
fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
if len(c.FilesToTransfer) == 1 {
fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
}
machID, macIDerr := machineid.ID()
if macIDerr != nil {
log.Error(macIDerr)
return
}
if len(machID) > 6 {
machID = machID[:6]
}
c.machineID = machID
fmt.Fprintf(os.Stderr, "Sending %s (%s)\n", fname, utils.ByteCountDecimal(totalFilesSize))
fmt.Fprintf(os.Stderr, "Code is: %s\nOn the other computer run\n\ncroc %s\n", c.Options.SharedSecret, c.Options.SharedSecret)
// // c.spinner.Suffix = " waiting for recipient..."
// c.spinner.Start()
// create channel for quitting
// connect to the relay for messaging
errchan := make(chan error, 1)
if !c.Options.DisableLocal {
// setup the relay locally
for _, port := range c.Options.RelayPorts {
go func(portStr string) {
debugString := "warn"
if c.Options.Debug {
debugString = "debug"
}
err = tcp.Run(debugString, portStr, strings.Join(c.Options.RelayPorts[1:], ","))
if err != nil {
panic(err)
}
}(port)
}
// look for peers first
go func() {
discoveries, err := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: -1,
Payload: []byte(c.Options.RelayPorts[0]),
Delay: 10 * time.Millisecond,
TimeLimit: 30 * time.Second,
})
log.Debugf("discoveries: %+v", discoveries)
if err == nil && len(discoveries) > 0 {
log.Debug("using local server")
}
}()
go func() {
time.Sleep(500 * time.Millisecond)
log.Debug("establishing connection")
var banner string
conn, banner, ipaddr, err := tcp.ConnectToTCPServer("localhost:"+c.Options.RelayPorts[0], c.Options.SharedSecret)
log.Debugf("banner: %s", banner)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("could not connect to localhost:%s", c.Options.RelayPorts[0]))
return
}
log.Debugf("connection established: %+v", conn)
for {
data, _ := conn.Receive()
if bytes.Equal(data, []byte("handshake")) {
break
}
}
c.conn[0] = conn
log.Debug("exchanged header message")
c.Options.RelayAddress = "localhost"
c.Options.RelayPorts = strings.Split(banner, ",")
c.ExternalIP = ipaddr
errchan <- c.transfer(options)
}()
}
go func() {
log.Debug("establishing connection to %s", c.Options.RelayAddress)
var banner string
conn, banner, ipaddr, err := tcp.ConnectToTCPServer(c.Options.RelayAddress, c.Options.SharedSecret)
log.Debugf("banner: %s", banner)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("could not connect to %s", c.Options.RelayAddress))
log.Debug(err)
return
}
log.Debugf("connection established: %+v", conn)
for {
data, _ := conn.Receive()
if bytes.Equal(data, []byte("handshake")) {
break
}
}
c.conn[0] = conn
c.Options.RelayPorts = strings.Split(banner, ",")
c.ExternalIP = ipaddr
log.Debug("exchanged header message")
errchan <- c.transfer(options)
}()
return <-errchan
}
// Receive will receive a file
func (c *Client) Receive() (err error) {
fmt.Fprintf(os.Stderr, "connecting...")
// recipient will look for peers first
// and continue if it doesn't find any within 100 ms
if !c.Options.DisableLocal {
log.Debug("attempt to discover peers")
discoveries, err := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
Payload: []byte("ok"),
Delay: 10 * time.Millisecond,
TimeLimit: 100 * time.Millisecond,
})
if err == nil && len(discoveries) > 0 {
log.Debug("switching to local")
c.Options.RelayAddress = fmt.Sprintf("%s:%s", discoveries[0].Address, discoveries[0].Payload)
}
log.Debugf("discoveries: %+v", discoveries)
log.Debug("establishing connection")
}
var banner string
c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(c.Options.RelayAddress, c.Options.SharedSecret)
log.Debugf("banner: %s", banner)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("could not connect to %s", c.Options.RelayAddress))
return
}
log.Debugf("connection established: %+v", c.conn[0])
c.conn[0].Send([]byte("handshake"))
c.Options.RelayPorts = strings.Split(banner, ",")
log.Debug("exchanged header message")
fmt.Fprintf(os.Stderr, "\rsecuring channel...")
return c.transfer(TransferOptions{})
}
func (c *Client) transfer(options TransferOptions) (err error) {
// connect to the server
// quit with c.quit <- true
c.quit = make(chan bool)
// if recipient, initialize with sending pake information
log.Debug("ready")
if !c.Options.IsSender && !c.Step1ChannelSecured {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "pake",
Bytes: c.Pake.Bytes(),
})
if err != nil {
return
}
}
// listen for incoming messages and process them
for {
var data []byte
var done bool
data, err = c.conn[0].Receive()
if err != nil {
break
}
done, err = c.processMessage(data)
if err != nil {
break
}
if done {
break
}
}
if c.Options.Stdout && !c.Options.IsSender {
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
os.Remove(pathToFile)
}
return
}
func (c *Client) processMessage(payload []byte) (done bool, err error) {
m, err := message.Decode(c.Key, payload)
if err != nil {
return
}
switch m.Type {
case "finished":
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "finished",
})
done = true
return
case "pake":
log.Debug("received pake payload")
// if // c.spinner.Suffix != " performing PAKE..." {
// // c.spinner.Stop()
// // c.spinner.Suffix = " performing PAKE..."
// // c.spinner.Start()
// }
notVerified := !c.Pake.IsVerified()
err = c.Pake.Update(m.Bytes)
if err != nil {
return
}
if (notVerified && c.Pake.IsVerified() && !c.Options.IsSender) || !c.Pake.IsVerified() {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "pake",
Bytes: c.Pake.Bytes(),
})
}
if c.Pake.IsVerified() {
if c.Options.IsSender {
log.Debug("generating salt")
salt := make([]byte, 8)
rand.Read(salt)
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "salt",
Bytes: salt,
Message: c.ExternalIP,
})
if err != nil {
return
}
}
// connects to the other ports of the server for transfer
var wg sync.WaitGroup
wg.Add(len(c.Options.RelayPorts))
for i := 0; i < len(c.Options.RelayPorts); i++ {
log.Debugf("port: [%s]", c.Options.RelayPorts[i])
go func(j int) {
defer wg.Done()
server := fmt.Sprintf("%s:%s", strings.Split(c.Options.RelayAddress, ":")[0], c.Options.RelayPorts[j])
log.Debugf("connecting to %s", server)
c.conn[j+1], _, _, err = tcp.ConnectToTCPServer(
server,
fmt.Sprintf("%s-%d", utils.SHA256(c.Options.SharedSecret)[:7], j),
)
if err != nil {
panic(err)
}
log.Debugf("connected to %s", server)
if !c.Options.IsSender {
go c.receiveData(j)
}
}(i)
}
wg.Wait()
}
case "salt":
log.Debug("received salt")
if !c.Options.IsSender {
log.Debug("sending salt back")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "salt",
Bytes: m.Bytes,
Message: c.ExternalIP,
})
}
log.Debugf("session key is verified, generating encryption with salt: %x", m.Bytes)
key, err := c.Pake.SessionKey()
if err != nil {
return true, err
}
c.Key, err = crypt.New(key, m.Bytes)
if err != nil {
return true, err
}
c.ExternalIPConnected = m.Message
log.Debugf("connected as %s -> %s", c.ExternalIP, c.ExternalIPConnected)
c.Step1ChannelSecured = true
case "error":
// c.spinner.Stop()
fmt.Print("\r")
err = fmt.Errorf("peer error: %s", m.Message)
return true, err
case "fileinfo":
var senderInfo SenderInfo
err = json.Unmarshal(m.Bytes, &senderInfo)
if err != nil {
log.Error(err)
return
}
c.FilesToTransfer = senderInfo.FilesToTransfer
fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
if len(c.FilesToTransfer) == 1 {
fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
}
totalSize := int64(0)
for _, fi := range c.FilesToTransfer {
totalSize += fi.Size
}
// c.spinner.Stop()
if !c.Options.NoPrompt {
fmt.Fprintf(os.Stderr, "\rAccept %s (%s)? (y/n) ", fname, utils.ByteCountDecimal(totalSize))
if strings.ToLower(strings.TrimSpace(utils.GetInput(""))) != "y" {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "error",
Message: "refusing files",
})
return true, fmt.Errorf("refused files")
}
} else {
fmt.Fprintf(os.Stderr, "\rReceiving %s (%s) \n", fname, utils.ByteCountDecimal(totalSize))
}
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)\n", c.ExternalIPConnected)
log.Debug(c.FilesToTransfer)
c.Step2FileInfoTransfered = true
case "recipientready":
var remoteFile RemoteFileRequest
err = json.Unmarshal(m.Bytes, &remoteFile)
if err != nil {
return
}
c.FilesToTransferCurrentNum = remoteFile.FilesToTransferCurrentNum
c.CurrentFileChunks = remoteFile.CurrentFileChunks
log.Debugf("current file chunks: %+v", c.CurrentFileChunks)
c.chunkMap = make(map[uint64]struct{})
for _, chunk := range c.CurrentFileChunks {
c.chunkMap[uint64(chunk)] = struct{}{}
}
c.Step3RecipientRequestFile = true
case "close-sender":
c.bar.Finish()
log.Debug("close-sender received...")
c.Step4FileTransfer = false
c.Step3RecipientRequestFile = false
log.Debug("sending close-recipient")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "close-recipient",
})
case "close-recipient":
c.Step4FileTransfer = false
c.Step3RecipientRequestFile = false
}
if err != nil {
return
}
err = c.updateState()
return
}
func (c *Client) updateState() (err error) {
if c.Options.IsSender && c.Step1ChannelSecured && !c.Step2FileInfoTransfered {
var b []byte
b, err = json.Marshal(SenderInfo{
MachineID: c.machineID,
FilesToTransfer: c.FilesToTransfer,
})
if err != nil {
log.Error(err)
return
}
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "fileinfo",
Bytes: b,
})
if err != nil {
return
}
c.Step2FileInfoTransfered = true
}
if !c.Options.IsSender && c.Step2FileInfoTransfered && !c.Step3RecipientRequestFile {
// find the next file to transfer and send that number
// if the files are the same size, then look for missing chunks
finished := true
for i, fileInfo := range c.FilesToTransfer {
if i < c.FilesToTransferCurrentNum {
continue
}
fileHash, errHash := utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name))
if errHash != nil || !bytes.Equal(fileHash, fileInfo.Hash) {
if !bytes.Equal(fileHash, fileInfo.Hash) {
log.Debugf("hashes are not equal %x != %x", fileHash, fileInfo.Hash)
}
finished = false
c.FilesToTransferCurrentNum = i
break
}
// TODO: print out something about this file already existing
}
if finished {
// TODO: do the last finishing stuff
log.Debug("finished")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "finished",
})
if err != nil {
panic(err)
}
}
// start initiating the process to receive a new file
log.Debugf("working on file %d", c.FilesToTransferCurrentNum)
// recipient sets the file
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
folderForFile, _ := filepath.Split(pathToFile)
os.MkdirAll(folderForFile, os.ModePerm)
var errOpen error
c.CurrentFile, errOpen = os.OpenFile(
pathToFile,
os.O_WRONLY, 0666)
truncate := false
c.CurrentFileChunks = []int64{}
if errOpen == nil {
stat, _ := c.CurrentFile.Stat()
truncate = stat.Size() != c.FilesToTransfer[c.FilesToTransferCurrentNum].Size
if truncate == false {
// recipient requests the file and chunks (if empty, then should receive all chunks)
// TODO: determine the missing chunks
c.CurrentFileChunks = utils.MissingChunks(
pathToFile,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
models.TCP_BUFFER_SIZE/2,
)
}
} else {
c.CurrentFile, errOpen = os.Create(pathToFile)
if errOpen != nil {
errOpen = errors.Wrap(errOpen, "could not create "+pathToFile)
log.Error(errOpen)
return errOpen
}
truncate = true
}
if truncate {
err := c.CurrentFile.Truncate(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
if err != nil {
err = errors.Wrap(err, "could not truncate "+pathToFile)
log.Error(err)
return err
}
}
// setup the progressbar
c.setBar()
c.TotalSent = 0
bRequest, _ := json.Marshal(RemoteFileRequest{
CurrentFileChunks: c.CurrentFileChunks,
FilesToTransferCurrentNum: c.FilesToTransferCurrentNum,
})
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "recipientready",
Bytes: bRequest,
})
if err != nil {
return
}
c.Step3RecipientRequestFile = true
}
if c.Options.IsSender && c.Step3RecipientRequestFile && !c.Step4FileTransfer {
log.Debug("start sending data!")
if !c.firstSend {
fmt.Fprintf(os.Stderr, "\nSending (->%s)\n", c.ExternalIPConnected)
c.firstSend = true
}
c.Step4FileTransfer = true
// setup the progressbar
c.setBar()
c.TotalSent = 0
for i := 0; i < len(c.Options.RelayPorts); i++ {
go c.sendData(i)
}
}
return
}
func (c *Client) setBar() {
description := fmt.Sprintf("%-28s", c.FilesToTransfer[c.FilesToTransferCurrentNum].Name)
if len(c.FilesToTransfer) == 1 {
description = c.FilesToTransfer[c.FilesToTransferCurrentNum].Name
}
c.bar = progressbar.NewOptions64(
c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
progressbar.OptionOnCompletion(func() {
fmt.Fprintf(os.Stderr, " ✔️\n")
}),
progressbar.OptionSetWidth(20),
progressbar.OptionSetDescription(description),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes64(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(100*time.Millisecond),
)
c.bar.Add(len(c.CurrentFileChunks) * models.TCP_BUFFER_SIZE / 2)
}
func (c *Client) receiveData(i int) {
for {
log.Debug("waiting for data")
data, err := c.conn[i+1].Receive()
if err != nil {
break
}
data, err = c.Key.Decrypt(data)
if err != nil {
panic(err)
}
data = compress.Decompress(data)
// get position
var position uint64
rbuf := bytes.NewReader(data[:8])
err = binary.Read(rbuf, binary.LittleEndian, &position)
if err != nil {
panic(err)
}
positionInt64 := int64(position)
c.mutex.Lock()
_, err = c.CurrentFile.WriteAt(data[8:], positionInt64)
c.mutex.Unlock()
if err != nil {
panic(err)
}
c.bar.Add(len(data[8:]))
c.TotalSent += int64(len(data[8:]))
c.TotalChunksTransfered++
log.Debugf("block: %+v", positionInt64)
if c.TotalChunksTransfered == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size {
log.Debug("finished receiving!")
c.CurrentFile.Close()
if c.Options.Stdout {
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
b, _ := ioutil.ReadFile(pathToFile)
fmt.Print(string(b))
}
log.Debug("sending close-sender")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: "close-sender",
})
if err != nil {
panic(err)
}
}
}
return
}
func (c *Client) sendData(i int) {
defer func() {
log.Debugf("finished with %d", i)
}()
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderSource,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
log.Debugf("opening %s to read", pathToFile)
f, err := os.Open(pathToFile)
if err != nil {
panic(err)
}
defer f.Close()
pos := uint64(0)
curi := float64(0)
for {
// Read file
data := make([]byte, models.TCP_BUFFER_SIZE/2)
n, err := f.Read(data)
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
if math.Mod(curi, float64(len(c.Options.RelayPorts))) == float64(i) {
// check to see if this is a chunk that the recipient wants
usableChunk := true
c.mutex.Lock()
if len(c.chunkMap) != 0 {
if _, ok := c.chunkMap[pos]; !ok {
usableChunk = false
} else {
delete(c.chunkMap, pos)
}
}
c.mutex.Unlock()
if usableChunk {
// log.Debugf("sending chunk %d", pos)
posByte := make([]byte, 8)
binary.LittleEndian.PutUint64(posByte, pos)
dataToSend, err := c.Key.Encrypt(
compress.Compress(
append(posByte, data[:n]...),
),
)
if err != nil {
panic(err)
}
err = c.conn[i+1].Send(dataToSend)
if err != nil {
panic(err)
}
c.bar.Add(n)
c.TotalSent += int64(n)
// time.Sleep(100 * time.Millisecond)
} else {
// log.Debugf("skipping chunk %d", pos)
}
}
curi++
pos += uint64(n)
}
time.Sleep(10 * time.Second)
return
}

View File

@ -1,81 +1,67 @@
package croc
import (
"crypto/rand"
"fmt"
"io/ioutil"
"os"
"sync"
"testing"
"time"
"github.com/schollz/croc/src/utils"
"github.com/stretchr/testify/assert"
log "github.com/cihub/seelog"
"github.com/schollz/croc/v6/src/tcp"
)
func sendAndReceive(t *testing.T, forceSend int, local bool) {
room := utils.GetRandomName()
var startTime time.Time
var durationPerMegabyte float64
megabytes := 1
if local {
megabytes = 100
func TestCroc(t *testing.T) {
defer log.Flush()
go tcp.Run("debug", "8081", "8082,8083,8084,8085")
go tcp.Run("debug", "8082")
go tcp.Run("debug", "8083")
go tcp.Run("debug", "8084")
go tcp.Run("debug", "8085")
time.Sleep(300 * time.Millisecond)
log.Flush()
log.Debug("setting up sender")
log.Flush()
sender, err := New(Options{
IsSender: true,
SharedSecret: "test",
Debug: true,
RelayAddress: "localhost:8081",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
})
if err != nil {
panic(err)
}
fname := generateRandomFile(megabytes)
log.Debug("setting up receiver")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "test",
Debug: true,
RelayAddress: "localhost:8081",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
})
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
c := Init(true)
c.NoLocal = !local
// c.AddressTCPPorts = []string{"8154", "8155"}
c.ForceSend = forceSend
c.UseCompression = true
c.UseEncryption = true
assert.Nil(t, c.Send(fname, room))
sender.Send(TransferOptions{
PathToFiles: []string{"../../README.md"},
})
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
go func() {
defer wg.Done()
time.Sleep(5 * time.Second)
os.MkdirAll("test", 0755)
os.Chdir("test")
c := Init(true)
c.NoLocal = !local
// c.AddressTCPPorts = []string{"8154", "8155"}
c.ForceSend = forceSend
startTime = time.Now()
assert.Nil(t, c.Receive(room))
durationPerMegabyte = float64(megabytes) / time.Since(startTime).Seconds()
assert.True(t, utils.Exists(fname))
receiver.Receive()
wg.Done()
}()
wg.Wait()
os.Chdir("..")
os.RemoveAll("test")
os.Remove(fname)
fmt.Printf("\n-----\n%2.1f MB/s\n----\n", durationPerMegabyte)
}
func TestSendReceivePubWebsockets(t *testing.T) {
sendAndReceive(t, 1, false)
}
func TestSendReceivePubTCP(t *testing.T) {
sendAndReceive(t, 2, false)
}
func TestSendReceiveLocalWebsockets(t *testing.T) {
sendAndReceive(t, 1, true)
}
// func TestSendReceiveLocalTCP(t *testing.T) {
// sendAndReceive(t, 2, true)
// }
func generateRandomFile(megabytes int) (fname string) {
// generate a random file
bigBuff := make([]byte, 1024*1024*megabytes)
rand.Read(bigBuff)
fname = fmt.Sprintf("%dmb.file", megabytes)
ioutil.WriteFile(fname, bigBuff, 0666)
return
}

View File

@ -1,7 +0,0 @@
package croc
type WebSocketMessage struct {
messageType int
message []byte
err error
}

View File

@ -1,624 +0,0 @@
package croc
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"sync"
"time"
log "github.com/cihub/seelog"
humanize "github.com/dustin/go-humanize"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/croc/src/comm"
"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/croc/src/zipper"
"github.com/schollz/pake"
"github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
)
var DebugLevel string
// Receive is the async operation to receive a file
func (cr *Croc) startRecipient(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, done chan error, c *websocket.Conn, codephrase string, noPrompt bool, useStdout bool) {
logger.SetLogLevel(DebugLevel)
err := cr.receive(forceSend, serverAddress, tcpPorts, isLocal, c, codephrase, noPrompt, useStdout)
if err != nil && strings.HasPrefix(err.Error(), "websocket: close 100") {
err = nil
}
done <- err
}
func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, c *websocket.Conn, codephrase string, noPrompt bool, useStdout bool) (err error) {
var sessionKey []byte
var transferTime time.Duration
var hash256 []byte
var progressFile string
var resumeFile bool
var tcpConnections []comm.Comm
var Q *pake.Pake
dataChan := make(chan []byte, 1024*1024)
isConnectedIfUsingTCP := make(chan bool)
blocks := []string{}
useWebsockets := true
switch forceSend {
case 0:
if !isLocal {
useWebsockets = false
}
case 1:
useWebsockets = true
case 2:
useWebsockets = false
}
// start a spinner
spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spin.Writer = os.Stderr
spin.Suffix = " connecting..."
cr.StateString = "Connecting as recipient..."
spin.Start()
defer spin.Stop()
// both parties should have a weak key
pw := []byte(codephrase)
// start the reader
websocketMessages := make(chan WebSocketMessage, 1024)
go func() {
defer func() {
if r := recover(); r != nil {
log.Debugf("recovered from %s", r)
}
}()
for {
messageType, message, err := c.ReadMessage()
websocketMessages <- WebSocketMessage{messageType, message, err}
}
}()
step := 0
for {
var websocketMessageMain WebSocketMessage
// websocketMessageMain = <-websocketMessages
timeWaitingForMessage := time.Now()
for {
done := false
select {
case websocketMessageMain = <-websocketMessages:
done = true
default:
time.Sleep(10 * time.Millisecond)
}
if done {
break
}
if time.Since(timeWaitingForMessage).Seconds() > 3 && step == 0 {
return fmt.Errorf("You are trying to receive a file with no sender.")
}
}
messageType := websocketMessageMain.messageType
message := websocketMessageMain.message
err := websocketMessageMain.err
if err != nil {
return err
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
if messageType == websocket.TextMessage && bytes.Equal(message, []byte("interrupt")) {
return errors.New("\rinterrupted by other party")
}
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
spin.Stop()
spin.Suffix = " performing PAKE..."
cr.StateString = "Performing PAKE..."
spin.Start()
// sender has initiated, sends their initial data
var initialData models.Initial
err = json.Unmarshal(message, &initialData)
if err != nil {
err = errors.Wrap(err, "incompatible versions of croc")
return err
}
cr.OtherIP = initialData.IPAddress
log.Debugf("sender IP: %s", cr.OtherIP)
// check whether the version strings are compatible
versionStringsOther := strings.Split(initialData.VersionString, ".")
versionStringsSelf := strings.Split(cr.Version, ".")
if len(versionStringsOther) == 3 && len(versionStringsSelf) == 3 {
if versionStringsSelf[0] != versionStringsOther[0] || versionStringsSelf[1] != versionStringsOther[1] {
return fmt.Errorf("version sender %s is not compatible with recipient %s", cr.Version, initialData.VersionString)
}
}
// initialize the PAKE with the curve sent from the sender
Q, err = pake.InitCurve(pw, 1, initialData.CurveType, 1*time.Millisecond)
if err != nil {
err = errors.Wrap(err, "incompatible curve type")
return err
}
// recipient begins by sending back initial data to sender
ip := ""
if isLocal {
ip = utils.LocalIP()
} else {
ip, _ = utils.PublicIP()
}
initialData.VersionString = cr.Version
initialData.IPAddress = ip
bInitialData, _ := json.Marshal(initialData)
c.WriteMessage(websocket.BinaryMessage, bInitialData)
case 1:
// 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 fmt.Errorf("Recipient is using wrong code phrase.")
}
// Q has the session key now, but we will still check if its valid
sessionKey, err = Q.SessionKey()
if err != nil {
return fmt.Errorf("Recipient is using wrong code phrase.")
}
log.Debugf("%x\n", sessionKey)
// initialize TCP connections if using (possible, but unlikely, race condition)
go func() {
log.Debug("initializing TCP connections")
if !useWebsockets {
log.Debugf("connecting to server")
tcpConnections = make([]comm.Comm, len(tcpPorts))
var wg sync.WaitGroup
wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts {
go func(i int, tcpPort string) {
defer wg.Done()
log.Debugf("connecting to %d", i)
var message string
tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
if err != nil {
log.Error(err)
}
if message != "recipient" {
log.Errorf("got wrong message: %s", message)
}
}(i, tcpPort)
}
wg.Wait()
log.Debugf("fully connected")
}
isConnectedIfUsingTCP <- true
}()
c.WriteMessage(websocket.BinaryMessage, Q.Bytes())
case 2:
log.Debugf("[%d] Q recieves H(k) from P", step)
// check if everything is still kosher with our computed session key
if err := Q.Update(message); err != nil {
log.Debug(err)
return fmt.Errorf("Recipient is using wrong code phrase.")
}
c.WriteMessage(websocket.BinaryMessage, []byte("ready"))
case 3:
spin.Stop()
cr.StateString = "Recieving file info..."
// unmarshal the file info
log.Debugf("[%d] recieve file info", step)
// do decryption on the file stats
enc, err := crypt.FromBytes(message)
if err != nil {
return err
}
decryptedFileData, err := enc.Decrypt(sessionKey)
if err != nil {
return err
}
err = json.Unmarshal(decryptedFileData, &cr.FileInfo)
if err != nil {
return err
}
log.Debugf("got file stats: %+v", cr.FileInfo)
// determine if the file is resuming or not
progressFile = fmt.Sprintf("%s.progress", cr.FileInfo.SentName)
overwritingOrReceiving := "Receiving"
if utils.Exists(cr.FileInfo.Name) || utils.Exists(cr.FileInfo.SentName) {
overwritingOrReceiving = "Overwriting"
if utils.Exists(progressFile) {
overwritingOrReceiving = "Resume receiving"
resumeFile = true
}
}
// send blocks
if resumeFile {
fileWithBlocks, _ := os.Open(progressFile)
scanner := bufio.NewScanner(fileWithBlocks)
for scanner.Scan() {
blocks = append(blocks, strings.TrimSpace(scanner.Text()))
}
fileWithBlocks.Close()
}
blocksBytes, _ := json.Marshal(blocks)
// encrypt the block data and send
encblockBytes := crypt.Encrypt(blocksBytes, sessionKey)
// wait for TCP connections if using them
_ = <-isConnectedIfUsingTCP
c.WriteMessage(websocket.BinaryMessage, encblockBytes.Bytes())
// prompt user about the file
fileOrFolder := "file"
if cr.FileInfo.IsDir {
fileOrFolder = "folder"
}
cr.WindowReceivingString = fmt.Sprintf("%s %s (%s) into: %s",
overwritingOrReceiving,
fileOrFolder,
humanize.Bytes(uint64(cr.FileInfo.Size)),
cr.FileInfo.Name,
)
fmt.Fprintf(os.Stderr, "\r%s\n",
cr.WindowReceivingString,
)
if !noPrompt {
if "y" != utils.GetInput("ok? (y/N): ") {
fmt.Fprintf(os.Stderr, "Cancelling request")
c.WriteMessage(websocket.BinaryMessage, []byte("no"))
return nil
}
}
if cr.WindowRecipientPrompt {
// wait until it switches to false
// the window should then set WindowRecipientAccept
for {
if !cr.WindowRecipientPrompt {
if cr.WindowRecipientAccept {
break
} else {
fmt.Fprintf(os.Stderr, "Cancelling request")
c.WriteMessage(websocket.BinaryMessage, []byte("no"))
return nil
}
}
time.Sleep(10 * time.Millisecond)
}
}
// await file
// erase file if overwriting
if overwritingOrReceiving == "Overwriting" {
os.Remove(cr.FileInfo.SentName)
}
var f *os.File
if utils.Exists(cr.FileInfo.SentName) && resumeFile {
if !useWebsockets {
f, err = os.OpenFile(cr.FileInfo.SentName, os.O_WRONLY, 0644)
} else {
f, err = os.OpenFile(cr.FileInfo.SentName, os.O_APPEND|os.O_WRONLY, 0644)
}
if err != nil {
log.Error(err)
return err
}
} else {
f, err = os.Create(cr.FileInfo.SentName)
if err != nil {
log.Error(err)
return err
}
if !useWebsockets {
if err = f.Truncate(cr.FileInfo.Size); err != nil {
log.Error(err)
return err
}
}
}
blockSize := 0
if useWebsockets {
blockSize = models.WEBSOCKET_BUFFER_SIZE / 8
} else {
blockSize = models.TCP_BUFFER_SIZE / 2
}
// start the ui for pgoress
cr.StateString = "Recieving file..."
bytesWritten := 0
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)...\n", cr.OtherIP)
cr.Bar = progressbar.NewOptions(
int(cr.FileInfo.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(cr.FileInfo.Size)),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(1/60*time.Second),
)
cr.Bar.Add((len(blocks) * blockSize))
finished := make(chan bool)
go func(finished chan bool, dataChan chan []byte) (err error) {
// remove previous progress
var fProgress *os.File
var progressErr error
if resumeFile {
fProgress, progressErr = os.OpenFile(progressFile, os.O_APPEND|os.O_WRONLY, 0644)
bytesWritten = len(blocks) * blockSize
} else {
os.Remove(progressFile)
fProgress, progressErr = os.Create(progressFile)
}
if progressErr != nil {
panic(progressErr)
}
defer fProgress.Close()
blocksWritten := 0.0
blocksToWrite := float64(cr.FileInfo.Size)
if useWebsockets {
blocksToWrite = blocksToWrite/float64(models.WEBSOCKET_BUFFER_SIZE/8) - float64(len(blocks))
} else {
blocksToWrite = blocksToWrite/float64(models.TCP_BUFFER_SIZE/2) - float64(len(blocks))
}
for {
message := <-dataChan
// do decryption
var enc crypt.Encryption
err = json.Unmarshal(message, &enc)
if err != nil {
// log.Errorf("%s: [%s] [%+v] (%d/%d) %+v", err.Error(), message, message, len(message), numBytes, bs)
log.Error(err)
return err
}
decrypted, err := enc.Decrypt(sessionKey, !cr.FileInfo.IsEncrypted)
if err != nil {
log.Error(err)
return err
}
// get location if TCP
var locationToWrite int
if !useWebsockets {
pieces := bytes.SplitN(decrypted, []byte("-"), 2)
decrypted = pieces[1]
locationToWrite, _ = strconv.Atoi(string(pieces[0]))
}
// do decompression
if cr.FileInfo.IsCompressed && !cr.FileInfo.IsDir {
decrypted = compress.Decompress(decrypted)
}
var n int
if !useWebsockets {
if err != nil {
log.Error(err)
return err
}
n, err = f.WriteAt(decrypted, int64(locationToWrite))
fProgress.WriteString(fmt.Sprintf("%d\n", locationToWrite))
log.Debugf("wrote %d bytes to location %d (%2.0f/%2.0f)", n, locationToWrite, blocksWritten, blocksToWrite)
} else {
// write to file
n, err = f.Write(decrypted)
log.Debugf("wrote %d bytes to location %d (%2.0f/%2.0f)", n, bytesWritten, blocksWritten, blocksToWrite)
fProgress.WriteString(fmt.Sprintf("%d\n", bytesWritten))
}
if err != nil {
log.Error(err)
return err
}
// update the bytes written
bytesWritten += n
blocksWritten += 1.0
// update the progress bar
cr.Bar.Add(n)
if int64(bytesWritten) == cr.FileInfo.Size || blocksWritten >= blocksToWrite {
log.Debug("finished", int64(bytesWritten), cr.FileInfo.Size, blocksWritten, blocksToWrite)
break
}
}
finished <- true
return
}(finished, dataChan)
log.Debug("telling sender i'm ready")
c.WriteMessage(websocket.BinaryMessage, []byte("ready"))
startTime := time.Now()
if useWebsockets {
for {
// read from websockets
websocketMessageData := <-websocketMessages
if bytes.HasPrefix(websocketMessageData.message, []byte("error")) {
return fmt.Errorf("%s", websocketMessageData.message)
}
if websocketMessageData.messageType != websocket.BinaryMessage {
continue
}
if err != nil {
log.Error(err)
return err
}
if bytes.Equal(websocketMessageData.message, []byte("magic")) {
log.Debug("got magic")
break
}
dataChan <- websocketMessageData.message
}
} else {
log.Debugf("starting listening with tcp with %d connections", len(tcpConnections))
// check to see if any messages are sent
stopMessageSignal := make(chan bool, 1)
errorsDuringTransfer := make(chan error, 24)
go func() {
for {
select {
case sig := <-stopMessageSignal:
errorsDuringTransfer <- nil
log.Debugf("got message signal: %+v", sig)
return
case wsMessage := <-websocketMessages:
log.Debugf("got message: %s", wsMessage.message)
if bytes.HasPrefix(wsMessage.message, []byte("error")) {
log.Debug("stopping transfer")
for i := 0; i < len(tcpConnections)+1; i++ {
errorsDuringTransfer <- fmt.Errorf("%s", wsMessage.message)
}
return
}
default:
continue
}
}
}()
// using TCP
go func() {
var wg sync.WaitGroup
wg.Add(len(tcpConnections))
for i := range tcpConnections {
defer func(i int) {
log.Debugf("closing connection %d", i)
tcpConnections[i].Close()
}(i)
go func(wg *sync.WaitGroup, j int) {
defer wg.Done()
for {
select {
case _ = <-errorsDuringTransfer:
log.Debugf("%d got stop", i)
return
default:
}
log.Debugf("waiting to read on %d", j)
// read from TCP connection
message, _, _, err := tcpConnections[j].Read()
// log.Debugf("message: %s", message)
if err != nil {
panic(err)
}
if bytes.Equal(message, []byte("magic")) {
log.Debugf("%d got magic, leaving", j)
return
}
dataChan <- message
}
}(&wg, i)
}
log.Debug("waiting for tcp goroutines")
wg.Wait()
errorsDuringTransfer <- nil
}()
// block until this is done
log.Debug("waiting for error")
errorDuringTransfer := <-errorsDuringTransfer
log.Debug("sending stop message signal")
stopMessageSignal <- true
if errorDuringTransfer != nil {
log.Debugf("got error during transfer: %s", errorDuringTransfer.Error())
return errorDuringTransfer
}
}
_ = <-finished
log.Debug("telling sender i'm done")
c.WriteMessage(websocket.BinaryMessage, []byte("done"))
// we are finished
transferTime = time.Since(startTime)
// close file
err = f.Close()
if err != nil {
return err
}
// finish bar
cr.Bar.Finish()
// check hash
hash256, err = utils.HashFile(cr.FileInfo.SentName)
if err != nil {
log.Error(err)
return err
}
// tell the sender the hash so they can quit
c.WriteMessage(websocket.BinaryMessage, append([]byte("hash:"), hash256...))
case 4:
// receive the hash from the sender so we can check it and quit
log.Debugf("got hash: %x", message)
if bytes.Equal(hash256, message) {
// open directory
if cr.FileInfo.IsDir {
err = zipper.UnzipFile(cr.FileInfo.SentName, ".")
if DebugLevel != "debug" {
os.Remove(cr.FileInfo.SentName)
}
} else {
err = nil
}
if err == nil {
if useStdout && !cr.FileInfo.IsDir {
var bFile []byte
bFile, err = ioutil.ReadFile(cr.FileInfo.SentName)
if err != nil {
return err
}
os.Stdout.Write(bFile)
os.Remove(cr.FileInfo.SentName)
}
transferRate := float64(cr.FileInfo.Size) / 1000000.0 / transferTime.Seconds()
transferType := "MB/s"
if transferRate < 1 {
transferRate = float64(cr.FileInfo.Size) / 1000.0 / transferTime.Seconds()
transferType = "kB/s"
}
folderOrFile := "file"
if cr.FileInfo.IsDir {
folderOrFile = "folder"
}
if useStdout {
cr.FileInfo.Name = "stdout"
}
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s (%2.1f %s)", folderOrFile, cr.FileInfo.Name, transferRate, transferType)
os.Remove(progressFile)
cr.StateString = fmt.Sprintf("Received %s written to %s (%2.1f %s)", folderOrFile, cr.FileInfo.Name, transferRate, transferType)
}
return err
} else {
if DebugLevel != "debug" {
log.Debug("removing corrupted file")
os.Remove(cr.FileInfo.SentName)
}
return errors.New("file corrupted")
}
default:
return fmt.Errorf("unknown step")
}
step++
}
}

View File

@ -1,570 +0,0 @@
package croc
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/croc/src/comm"
"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/croc/src/zipper"
"github.com/schollz/pake"
progressbar "github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
)
// Send is the async call to send data
func (cr *Croc) startSender(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, done chan error, c *websocket.Conn, fname string, codephrase string, useCompression bool, useEncryption bool) {
logger.SetLogLevel(DebugLevel)
log.Debugf("sending %s", fname)
err := cr.send(forceSend, serverAddress, tcpPorts, isLocal, c, fname, codephrase, useCompression, useEncryption)
if err != nil && strings.HasPrefix(err.Error(), "websocket: close 100") {
err = nil
}
done <- err
}
func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, c *websocket.Conn, fname string, codephrase string, useCompression bool, useEncryption bool) (err error) {
var f *os.File
defer f.Close() // ignore the error if it wasn't opened :(
var fileHash []byte
var startTransfer time.Time
var tcpConnections []comm.Comm
blocksToSkip := make(map[int64]struct{})
isConnectedIfUsingTCP := make(chan bool)
type DataChan struct {
b []byte
currentPostition int64
bytesRead int
err error
}
dataChan := make(chan DataChan, 1024*1024)
defer close(dataChan)
useWebsockets := true
switch forceSend {
case 0:
if !isLocal {
useWebsockets = false
}
case 1:
useWebsockets = true
case 2:
useWebsockets = false
}
fileReady := make(chan error)
// normalize the file name
fname, err = filepath.Abs(fname)
if err != nil {
return err
}
_, filename := filepath.Split(fname)
// get ready to generate session key
var sessionKey []byte
// start a spinner
spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spin.Writer = os.Stderr
defer spin.Stop()
// both parties should have a weak key
pw := []byte(codephrase)
// initialize sender P ("0" indicates sender)
P, err := pake.InitCurve(pw, 0, cr.CurveType, 1*time.Millisecond)
if err != nil {
return
}
// start the reader
websocketMessages := make(chan WebSocketMessage, 1024)
go func() {
defer func() {
if r := recover(); r != nil {
log.Debugf("recovered from %s", r)
}
}()
for {
messageType, message, err := c.ReadMessage()
websocketMessages <- WebSocketMessage{messageType, message, err}
}
}()
step := 0
for {
websocketMessage := <-websocketMessages
messageType := websocketMessage.messageType
message := websocketMessage.message
errRead := websocketMessage.err
if errRead != nil {
return errRead
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
if messageType == websocket.TextMessage && bytes.HasPrefix(message, []byte("interrupt")) {
return errors.New("\rinterrupted by other party")
}
if messageType == websocket.TextMessage && bytes.HasPrefix(message, []byte("err")) {
return errors.New("\r" + string(message))
}
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
// sender initiates communication
ip := ""
if isLocal {
ip = utils.LocalIP()
} else {
ip, _ = utils.PublicIP()
}
initialData := models.Initial{
CurveType: cr.CurveType,
IPAddress: ip,
VersionString: cr.Version, // version should match
}
bInitialData, _ := json.Marshal(initialData)
// send the initial data
c.WriteMessage(websocket.BinaryMessage, bInitialData)
case 1:
// first receive the initial data from the recipient
var initialData models.Initial
err = json.Unmarshal(message, &initialData)
if err != nil {
err = errors.Wrap(err, "incompatible versions of croc")
return
}
cr.OtherIP = initialData.IPAddress
log.Debugf("recipient IP: %s", cr.OtherIP)
go func() {
// recipient might want file! start gathering information about file
fstat, err := os.Stat(fname)
if err != nil {
fileReady <- err
return
}
cr.FileInfo = models.FileStats{
Name: filename,
Size: fstat.Size(),
ModTime: fstat.ModTime(),
IsDir: fstat.IsDir(),
SentName: fstat.Name(),
IsCompressed: useCompression,
IsEncrypted: useEncryption,
}
if cr.FileInfo.IsDir {
// zip the directory
cr.FileInfo.SentName, err = zipper.ZipFile(fname, true)
if err != nil {
log.Error(err)
fileReady <- err
return
}
fname = cr.FileInfo.SentName
fstat, err := os.Stat(fname)
if err != nil {
fileReady <- err
return
}
// get new size
cr.FileInfo.Size = fstat.Size()
}
// open the file
f, err = os.Open(fname)
if err != nil {
fileReady <- err
return
}
fileReady <- nil
}()
// send pake data
log.Debugf("[%d] first, P sends u to Q", step)
c.WriteMessage(websocket.BinaryMessage, P.Bytes())
// start PAKE spinnner
spin.Suffix = " performing PAKE..."
cr.StateString = "Performing PAKE..."
spin.Start()
case 2:
// P recieves H(k),v from Q
log.Debugf("[%d] P computes k, H(k), sends H(k) to Q", step)
err := P.Update(message)
c.WriteMessage(websocket.BinaryMessage, P.Bytes())
if err != nil {
return fmt.Errorf("Recipient is using wrong code phrase.")
}
sessionKey, _ = P.SessionKey()
// check(err)
log.Debugf("%x\n", sessionKey)
// wait for readiness
spin.Stop()
spin.Suffix = " waiting for recipient ok..."
cr.StateString = "Waiting for recipient ok...."
spin.Start()
case 3:
log.Debugf("[%d] recipient declares readiness for file info", step)
if !bytes.HasPrefix(message, []byte("ready")) {
return errors.New("Recipient refused file")
}
err = <-fileReady // block until file is ready
if err != nil {
return err
}
fstatsBytes, err := json.Marshal(cr.FileInfo)
if err != nil {
return err
}
// encrypt the file meta data
enc := crypt.Encrypt(fstatsBytes, sessionKey)
// send the file meta data
c.WriteMessage(websocket.BinaryMessage, enc.Bytes())
case 4:
log.Debugf("[%d] recipient gives blocks", step)
// recipient sends blocks, and sender does not send anything back
// determine if any blocks were sent to skip
enc, err := crypt.FromBytes(message)
if err != nil {
log.Error(err)
return err
}
decrypted, err := enc.Decrypt(sessionKey)
if err != nil {
err = errors.Wrap(err, "could not decrypt blocks with session key")
log.Error(err)
return err
}
var blocks []string
errBlocks := json.Unmarshal(decrypted, &blocks)
if errBlocks == nil {
for _, block := range blocks {
blockInt64, errBlock := strconv.Atoi(block)
if errBlock == nil {
blocksToSkip[int64(blockInt64)] = struct{}{}
}
}
}
log.Debugf("found blocks: %+v", blocksToSkip)
// connect to TCP in background
tcpConnections = make([]comm.Comm, len(tcpPorts))
go func() {
if !useWebsockets {
log.Debugf("connecting to server")
var wg sync.WaitGroup
wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts {
go func(i int, tcpPort string) {
defer wg.Done()
log.Debugf("connecting to %s on connection %d", tcpPort, i)
var message string
tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
if err != nil {
log.Error(err)
}
if message != "sender" {
log.Errorf("got wrong message: %s", message)
}
}(i, tcpPort)
}
wg.Wait()
}
isConnectedIfUsingTCP <- true
}()
// start loading the file into memory
// start streaming encryption/compression
if cr.FileInfo.IsDir {
// remove file if zipped
defer os.Remove(cr.FileInfo.SentName)
}
go func(dataChan chan DataChan) {
var buffer []byte
if useWebsockets {
buffer = make([]byte, models.WEBSOCKET_BUFFER_SIZE/8)
} else {
buffer = make([]byte, models.TCP_BUFFER_SIZE/2)
}
currentPostition := int64(0)
for {
bytesread, err := f.Read(buffer)
if bytesread > 0 {
if _, ok := blocksToSkip[currentPostition]; ok {
log.Debugf("skipping the sending of block %d", currentPostition)
currentPostition += int64(bytesread)
continue
}
// do compression
var compressedBytes []byte
if useCompression && !cr.FileInfo.IsDir {
compressedBytes = compress.Compress(buffer[:bytesread])
} else {
compressedBytes = buffer[:bytesread]
}
// if using TCP, prepend the location to write the data to in the resulting file
if !useWebsockets {
compressedBytes = append([]byte(fmt.Sprintf("%d-", currentPostition)), compressedBytes...)
}
// do encryption
enc := crypt.Encrypt(compressedBytes, sessionKey, !useEncryption)
encBytes, err := json.Marshal(enc)
if err != nil {
dataChan <- DataChan{
b: nil,
bytesRead: 0,
err: err,
}
return
}
dataChan <- DataChan{
b: encBytes,
bytesRead: bytesread,
err: nil,
}
currentPostition += int64(bytesread)
}
if err != nil {
if err != io.EOF {
log.Error(err)
}
break
}
}
// finish
log.Debug("sending magic")
dataChan <- DataChan{
b: []byte("magic"),
bytesRead: 0,
err: nil,
}
if !useWebsockets {
log.Debug("sending extra magic to %d others", len(tcpPorts)-1)
for i := 0; i < len(tcpPorts)-1; i++ {
log.Debug("sending magic")
dataChan <- DataChan{
b: []byte("magic"),
bytesRead: 0,
err: nil,
}
}
}
}(dataChan)
case 5:
spin.Stop()
log.Debugf("[%d] recipient declares readiness for file data", step)
if !bytes.HasPrefix(message, []byte("ready")) {
return errors.New("Recipient refused file")
}
cr.StateString = "Transfer in progress..."
fmt.Fprintf(os.Stderr, "\rSending (->%s)...\n", cr.OtherIP)
// send file, compure hash simultaneously
startTransfer = time.Now()
blockSize := 0
if useWebsockets {
blockSize = models.WEBSOCKET_BUFFER_SIZE / 8
} else {
blockSize = models.TCP_BUFFER_SIZE / 2
}
cr.Bar = progressbar.NewOptions(
int(cr.FileInfo.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(cr.FileInfo.Size)),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(1/60*time.Second),
)
cr.Bar.Add(blockSize * len(blocksToSkip))
if useWebsockets {
for {
data := <-dataChan
if data.err != nil {
return data.err
}
cr.Bar.Add(data.bytesRead)
// write data to websockets
err = c.WriteMessage(websocket.BinaryMessage, data.b)
if err != nil {
err = errors.Wrap(err, "problem writing message")
return err
}
if bytes.Equal(data.b, []byte("magic")) {
break
}
}
} else {
_ = <-isConnectedIfUsingTCP
log.Debug("connected and ready to send on tcp")
// check to see if any messages are sent
stopMessageSignal := make(chan bool, 1)
errorsDuringTransfer := make(chan error, 24)
go func() {
for {
select {
case sig := <-stopMessageSignal:
errorsDuringTransfer <- nil
log.Debugf("got message signal: %+v", sig)
return
case wsMessage := <-websocketMessages:
log.Debugf("got message: %s", wsMessage.message)
if bytes.HasPrefix(wsMessage.message, []byte("error")) {
log.Debug("stopping transfer")
for i := 0; i < len(tcpConnections)+1; i++ {
errorsDuringTransfer <- fmt.Errorf("%s", wsMessage.message)
}
return
}
default:
continue
}
}
}()
var wg sync.WaitGroup
wg.Add(len(tcpConnections))
for i := range tcpConnections {
defer func(i int) {
log.Debugf("closing connection %d", i)
tcpConnections[i].Close()
}(i)
go func(i int, wg *sync.WaitGroup, dataChan <-chan DataChan) {
defer wg.Done()
for data := range dataChan {
select {
case _ = <-errorsDuringTransfer:
log.Debugf("%d got stop", i)
return
default:
}
if data.err != nil {
log.Error(data.err)
return
}
cr.Bar.Add(data.bytesRead)
// write data to tcp connection
_, errTcp := tcpConnections[i].Write(data.b)
if errTcp != nil {
errTcp = errors.Wrap(errTcp, "problem writing message")
log.Debug(errTcp)
errorsDuringTransfer <- errTcp
return
}
if bytes.Equal(data.b, []byte("magic")) {
log.Debugf("%d got magic", i)
return
}
}
}(i, &wg, dataChan)
}
// block until this is done
log.Debug("waiting for tcp goroutines")
wg.Wait()
log.Debug("sending stop message signal")
stopMessageSignal <- true
log.Debug("waiting for error")
errorDuringTransfer := <-errorsDuringTransfer
if errorDuringTransfer != nil {
log.Debugf("got error during transfer: %s", errorDuringTransfer.Error())
return errorDuringTransfer
}
}
cr.Bar.Finish()
log.Debug("send hash to finish file")
fileHash, err = utils.HashFile(fname)
if err != nil {
return err
}
case 6:
// recevied something, maybe the file hash
transferTime := time.Since(startTransfer)
if !bytes.HasPrefix(message, []byte("hash:")) {
log.Debugf("%s", message)
continue
}
c.WriteMessage(websocket.BinaryMessage, fileHash)
message = bytes.TrimPrefix(message, []byte("hash:"))
log.Debugf("[%d] determing whether it went ok", step)
if bytes.Equal(message, fileHash) {
log.Debug("file transfered successfully")
transferRate := float64(cr.FileInfo.Size) / 1000000.0 / transferTime.Seconds()
transferType := "MB/s"
if transferRate < 1 {
transferRate = float64(cr.FileInfo.Size) / 1000.0 / transferTime.Seconds()
transferType = "kB/s"
}
fmt.Fprintf(os.Stderr, "\nTransfer complete (%2.1f %s)", transferRate, transferType)
cr.StateString = fmt.Sprintf("Transfer complete (%2.1f %s)", transferRate, transferType)
return nil
} else {
fmt.Fprintf(os.Stderr, "\nTransfer corrupted")
return errors.New("file not transfered succesfully")
}
default:
return fmt.Errorf("unknown step")
}
step++
}
}
func connectToTCPServer(room string, address string) (com comm.Comm, message string, err error) {
connection, err := net.DialTimeout("tcp", address, 3*time.Hour)
if err != nil {
return
}
connection.SetReadDeadline(time.Now().Add(3 * time.Hour))
connection.SetDeadline(time.Now().Add(3 * time.Hour))
connection.SetWriteDeadline(time.Now().Add(3 * time.Hour))
com = comm.New(connection)
ok, err := com.Receive()
if err != nil {
return
}
log.Debugf("server says: %s", ok)
err = com.Send(room)
if err != nil {
return
}
message, err = com.Receive()
log.Debugf("server says: %s", message)
return
}

View File

@ -1,217 +0,0 @@
package croc
import (
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/schollz/croc/src/relay"
"github.com/schollz/croc/src/utils"
"github.com/schollz/peerdiscovery"
)
// Send the file
func (c *Croc) Send(fname, codephrase string) (err error) {
defer log.Flush()
log.Debugf("sending %s", fname)
errChan := make(chan error)
// normally attempt two connections
waitingFor := 2
// use public relay
if !c.LocalOnly {
go func() {
// atttempt to connect to public relay
errChan <- c.sendReceive(c.Address, c.AddressWebsocketPort, c.AddressTCPPorts, fname, codephrase, true, false)
}()
} else {
waitingFor = 1
}
// use local relay
if !c.NoLocal {
defer func() {
log.Debug("sending relay stop signal")
relay.Stop()
}()
go func() {
// start own relay and connect to it
go relay.Run(c.RelayWebsocketPort, c.RelayTCPPorts)
time.Sleep(250 * time.Millisecond) // race condition here, but this should work most of the time :(
// broadcast for peer discovery
go func() {
log.Debug("starting local discovery...")
discovered, err := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
TimeLimit: 600 * time.Second,
Delay: 50 * time.Millisecond,
Payload: []byte(c.RelayWebsocketPort + "- " + strings.Join(c.RelayTCPPorts, ",")),
MulticastAddress: fmt.Sprintf("239.255.255.%d", 230+int64(time.Now().Minute()/5)),
})
log.Debug(discovered, err)
}()
// connect to own relay
errChan <- c.sendReceive("localhost", c.RelayWebsocketPort, c.RelayTCPPorts, fname, codephrase, true, true)
}()
} else {
waitingFor = 1
}
err = <-errChan
if err == nil || waitingFor == 1 {
log.Debug("returning")
return
}
log.Debug(err)
return <-errChan
}
// Receive the file
func (c *Croc) Receive(codephrase string) (err error) {
defer log.Flush()
log.Debug("receiving")
// use local relay first
if !c.NoLocal {
log.Debug("trying to discover")
// 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"),
AllowSelf: true,
DisableBroadcast: true,
MulticastAddress: fmt.Sprintf("239.255.255.%d", 230+int64(time.Now().Minute()/5)),
})
log.Debug("finished")
log.Debug(discovered)
if errDiscover != nil {
log.Debug(errDiscover)
}
if len(discovered) > 0 {
if discovered[0].Address == utils.LocalIP() {
discovered[0].Address = "localhost"
}
log.Debugf("discovered %s:%s", discovered[0].Address, discovered[0].Payload)
// see if we can actually connect to it
timeout := time.Duration(200 * time.Millisecond)
client := http.Client{
Timeout: timeout,
}
ports := strings.Split(string(discovered[0].Payload), "-")
if len(ports) != 2 {
return errors.New("bad payload")
}
resp, err := client.Get(fmt.Sprintf("http://%s:%s/", discovered[0].Address, ports[0]))
if err == nil {
if resp.StatusCode == http.StatusOK {
// we connected, so use this
return c.sendReceive(discovered[0].Address, strings.TrimSpace(ports[0]), strings.Split(strings.TrimSpace(ports[1]), ","), "", codephrase, false, true)
}
} else {
log.Debugf("could not connect: %s", err.Error())
}
} else {
log.Debug("discovered no peers")
}
}
// use public relay
if !c.LocalOnly {
log.Debug("using public relay")
return c.sendReceive(c.Address, c.AddressWebsocketPort, c.AddressTCPPorts, "", codephrase, false, false)
}
return errors.New("must use local or public relay")
}
func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fname string, codephrase string, isSender bool, isLocal bool) (err error) {
defer log.Flush()
if len(codephrase) < 4 {
return fmt.Errorf("codephrase is too short")
}
// allow interrupts from Ctl+C
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
done := make(chan error)
// connect to server
websocketAddress := ""
if len(websocketPort) > 0 {
websocketAddress = fmt.Sprintf("ws://%s:%s/ws?room=%s", address, websocketPort, codephrase[:3])
} else {
websocketAddress = fmt.Sprintf("ws://%s/ws?room=%s", address, codephrase[:3])
}
log.Debugf("connecting to %s", websocketAddress)
sock, _, err := websocket.DefaultDialer.Dial(websocketAddress, nil)
if err != nil {
log.Error(err)
return
}
defer sock.Close()
// tell the websockets we are connected
err = sock.WriteMessage(websocket.BinaryMessage, []byte("connected"))
if err != nil {
log.Error(err)
return err
}
if isSender {
go c.startSender(c.ForceSend, address, tcpPorts, isLocal, done, sock, fname, codephrase, c.UseCompression, c.UseEncryption)
} else {
go c.startRecipient(c.ForceSend, address, tcpPorts, isLocal, done, sock, codephrase, c.NoRecipientPrompt, c.Stdout)
}
for {
select {
case doneError := <-done:
log.Debug("received done signal")
if doneError != nil {
c.StateString = doneError.Error()
sock.WriteMessage(websocket.TextMessage, []byte("error: "+doneError.Error()))
time.Sleep(50 * time.Millisecond)
}
return doneError
case <-interrupt:
if !c.Debug {
SetDebugLevel("critical")
}
log.Debug("interrupt")
err = sock.WriteMessage(websocket.TextMessage, []byte("error: interrupted by other party"))
if err != nil {
return err
}
time.Sleep(50 * time.Millisecond)
// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
err := sock.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Debug("write close:", err)
return nil
}
select {
case <-done:
case <-time.After(100 * time.Millisecond):
}
return nil
}
}
}
// Relay will start a relay on the specified port
func (c *Croc) Relay() (err error) {
return relay.Run(c.RelayWebsocketPort, c.RelayTCPPorts)
}

View File

@ -5,85 +5,79 @@ import (
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"strings"
"golang.org/x/crypto/pbkdf2"
)
// Encryption stores the data
type Encryption struct {
Encrypted []byte `json:"e"`
Salt []byte `json:"s"`
IV []byte `json:"i"`
key []byte
passphrase []byte
salt []byte
}
func (e Encryption) Bytes() []byte {
return []byte(base64.StdEncoding.EncodeToString(e.Encrypted) + "-" + base64.StdEncoding.EncodeToString(e.Salt) + "-" + base64.StdEncoding.EncodeToString(e.IV))
}
func FromBytes(b []byte) (enc Encryption, err error) {
enc = Encryption{}
items := strings.Split(string(b), "-")
if len(items) != 3 {
err = errors.New("not valid")
// New generates a new Encryption, using the supplied passphrase and
// an optional supplied salt.
// Passing nil passphrase will not use decryption.
func New(passphrase []byte, salt []byte) (e Encryption, err error) {
if passphrase == nil {
e = Encryption{nil, nil, nil}
return
}
enc.Encrypted, err = base64.StdEncoding.DecodeString(items[0])
if err != nil {
return
}
enc.Salt, err = base64.StdEncoding.DecodeString(items[1])
if err != nil {
return
}
enc.IV, err = base64.StdEncoding.DecodeString(items[2])
return
}
// 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) {
e.passphrase = passphrase
if salt == nil {
salt = make([]byte, 8)
e.salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
// Salt.
rand.Read(salt)
rand.Read(e.salt)
} else {
e.salt = salt
}
return pbkdf2.Key([]byte(passphrase), salt, 100, 32, sha256.New), salt
e.key = pbkdf2.Key([]byte(passphrase), e.salt, 100, 32, sha256.New)
return
}
func (e Encryption) Salt() []byte {
return e.salt
}
// Encrypt will generate an Encryption, prefixed with the IV
func (e Encryption) Encrypt(plaintext []byte) (encrypted []byte, err error) {
if e.passphrase == nil {
encrypted = plaintext
return
}
// generate a random iv each time
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
// Section 8.2
ivBytes := make([]byte, 12)
rand.Read(ivBytes)
b, err := aes.NewCipher(e.key)
if err != nil {
return
}
aesgcm, err := cipher.NewGCM(b)
if err != nil {
return
}
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
encrypted = append(ivBytes, encrypted...)
return
}
// Decrypt an Encryption
func (e Encryption) Decrypt(encrypted []byte) (plaintext []byte, err error) {
if e.passphrase == nil {
plaintext = encrypted
return
}
b, err := aes.NewCipher(e.key)
if err != nil {
return
}
aesgcm, err := cipher.NewGCM(b)
if err != nil {
return
}
plaintext, err = aesgcm.Open(nil, encrypted[:12], encrypted[12:], nil)
return
}

View File

@ -1,41 +1,61 @@
package crypt
import "testing"
import (
"testing"
func BenchmarkEncryption(b *testing.B) {
"github.com/stretchr/testify/assert"
)
func BenchmarkEncryptionNew(b *testing.B) {
for i := 0; i < b.N; i++ {
Encrypt([]byte(`
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse laoreet justo quis augue vehicula ornare. Nullam nec risus volutpat nulla dictum consectetur. Phasellus porttitor, justo non tincidunt finibus, massa justo iaculis urna, eget mattis nulla libero vitae risus. Vestibulum vehicula nunc id dignissim rutrum. Cras varius ac nulla a imperdiet. Sed finibus, libero in tempor hendrerit, turpis erat faucibus nisl, a consectetur massa mi eget mi. Vestibulum pulvinar lorem id ipsum elementum auctor.
Morbi at odio a eros eleifend faucibus. Sed at tempor urna, in interdum neque. Curabitur condimentum rhoncus orci, vel vulputate risus ultricies efficitur. Curabitur eu vehicula ligula. Curabitur suscipit ex vitae nunc faucibus vehicula. Mauris sed dictum mauris. Vivamus nec dui at urna porttitor suscipit. Integer ut eros finibus orci consectetur hendrerit id quis dolor. Proin dapibus orci quis massa viverra finibus. Sed quis ligula neque.
Aenean et fringilla nulla. In et venenatis massa, vel feugiat diam. Sed ornare felis nec egestas suscipit. Sed a ultricies sapien. Aliquam ex leo, tincidunt faucibus neque non, vehicula commodo velit. Vestibulum molestie efficitur velit in vestibulum. Aliquam eget leo felis. Etiam pharetra vulputate egestas. Cras gravida nibh eu sollicitudin facilisis. Nulla facilisi. Nulla tristique arcu vitae arcu pulvinar feugiat. Suspendisse finibus a urna a cursus. Donec bibendum sodales nunc eget tincidunt. Pellentesque blandit ac nunc at consectetur.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce congue nibh nisl, et porttitor mauris tempor eget. Vivamus sit amet sagittis ligula. Quisque aliquam orci non odio egestas, vel semper felis efficitur. Vivamus non leo lacus. Etiam non ligula eget erat porta laoreet id id ligula. Proin imperdiet erat id efficitur vehicula. Suspendisse potenti. Morbi ornare finibus metus, eu pretium leo tristique sit amet. Praesent placerat elit quis porttitor rhoncus. Vestibulum consectetur turpis sed lacus placerat, vel laoreet est interdum. Proin id quam ut risus tempor hendrerit nec eu lacus. Duis vel aliquam ex.
Sed facilisis in ex vitae pellentesque. Donec tempor lobortis dui. Praesent sagittis, elit ac dictum hendrerit, eros risus auctor erat, in pretium nulla mi et felis. Nullam imperdiet erat id erat rutrum fermentum. Praesent ultrices, diam non efficitur gravida, sem quam fermentum est, vitae tristique libero velit eget turpis. Nunc sem risus, venenatis nec suscipit quis, accumsan eu nunc. Fusce fringilla sit amet purus vel ornare. Donec nulla dolor, gravida ac metus nec, tincidunt feugiat sem. Fusce semper varius nunc.
Nulla facilisi. Quisque in euismod ex, ac sollicitudin dolor. Aliquam erat volutpat. Etiam nisl sem, posuere et pellentesque fermentum, pretium non lorem. Nunc blandit nisl at leo interdum gravida. Proin ullamcorper ultrices dictum. Pellentesque non ligula magna. Sed justo nibh, finibus vitae malesuada ultricies, molestie ac ante. Suspendisse maximus congue viverra.
Donec a est eu arcu tristique fermentum a quis velit. Aenean eu mollis turpis. Morbi euismod risus eros, at vestibulum lectus iaculis sed. Pellentesque varius eu justo in viverra. Phasellus feugiat tincidunt urna ut accumsan. In ullamcorper vehicula hendrerit. Sed sapien diam, rhoncus nec porta non, gravida et ante. Nulla et volutpat quam, nec venenatis ex. Donec vitae dictum libero, id ornare sapien. Morbi id aliquam ipsum. Cras a ultricies purus.
Morbi sit amet sagittis metus, vitae dictum nunc. Pellentesque ornare consequat diam, vitae maximus dolor facilisis at. Cras semper imperdiet mollis. Fusce maximus augue quis elit pulvinar, vitae varius nunc tempus. Maecenas et suscipit leo. Morbi tempus neque enim, sit amet vestibulum ex sollicitudin ac. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vel rutrum nisl. Nullam ac vehicula velit. Sed suscipit lacus libero, ac laoreet tortor vehicula a. In aliquam tellus tincidunt ligula fringilla, ut finibus lorem commodo. Sed sit amet porta magna, et aliquam lacus.
Nulla at libero in velit lacinia feugiat sed sit amet erat. Cras nec iaculis magna. Curabitur efficitur turpis vel risus euismod, sit amet posuere orci efficitur. Nunc tincidunt ante eu nunc lacinia, varius dignissim tellus eleifend. Pellentesque enim urna, porttitor eget tincidunt ut, sollicitudin in enim. Donec sit amet hendrerit orci, ut viverra arcu. Nam vitae semper est. Vestibulum aliquet dolor sed turpis blandit eleifend. Nullam auctor accumsan mauris eu mattis. Etiam interdum purus sit amet libero placerat vehicula.
Ut ac odio risus. Sed ut dolor ut metus tincidunt egestas. Curabitur ullamcorper lectus interdum nisi euismod, et faucibus dui fringilla. Nunc at purus vel erat hendrerit dapibus a ut ligula. Nulla facilisi. Etiam gravida, dui nec posuere gravida, nunc quam sodales dui, ut congue mauris tortor vitae nunc. Praesent dapibus mi quis pulvinar consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Pellentesque ultricies placerat dolor tempus cursus. Etiam vehicula, dui vel varius ullamcorper, sapien sem malesuada felis, quis suscipit est nibh vel massa.
Nulla sed mollis enim. Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque magna purus, faucibus vel ornare vel, semper et odio. Donec non interdum nunc. Maecenas metus augue, maximus ac volutpat id, euismod eget sem. Sed efficitur non diam sed ultrices. Fusce ultrices nisl et suscipit tristique. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi luctus augue vel vehicula iaculis.
Vivamus iaculis luctus nisl, quis molestie purus mollis vitae. Proin vehicula rutrum finibus. Nulla lacinia auctor tincidunt. Vivamus sollicitudin orci eu porttitor vehicula. Vivamus auctor sem sed risus porta cursus. Donec orci dui, lacinia vitae mi quis, tincidunt feugiat elit. Mauris pharetra faucibus justo non volutpat. Praesent pellentesque condimentum quam quis porta. Praesent sed nisi id eros iaculis condimentum nec in libero. Donec eu blandit enim. Aliquam nec elementum libero. Donec nec nisi suscipit felis blandit rutrum. Morbi magna nulla, porttitor id nisl vitae, imperdiet efficitur leo. Ut eu vestibulum justo, vitae dapibus felis.
Quisque ac est nec metus sagittis lobortis mollis id metus. Duis a augue eu erat vulputate facilisis varius ac nunc. Suspendisse eleifend enim suscipit erat aliquet, sed feugiat ligula tempor. Aenean dapibus felis porttitor mauris ultricies vehicula. Vestibulum lacinia scelerisque turpis, ut varius nunc suscipit vitae. Praesent blandit mauris eu semper lacinia. Cras mauris augue, tincidunt quis risus tincidunt, blandit euismod erat. Etiam non sagittis leo, eget pharetra risus.
Donec sodales ultricies neque, non accumsan velit blandit non. Aliquam lacinia orci mauris, commodo porttitor ipsum venenatis sed. Integer sit amet pretium nisi. Ut porttitor, sapien quis gravida egestas, nulla tellus ullamcorper ante, et eleifend diam turpis et odio. Quisque dui orci, commodo sit amet rhoncus ut, pharetra ac leo. Suspendisse non vehicula ex. Morbi gravida lacus vitae ex lacinia, nec aliquam purus consequat. Donec aliquam pretium massa, id viverra nunc blandit sit amet. Donec sed dapibus elit. Cras hendrerit efficitur eros quis malesuada. Aenean a massa in dolor gravida volutpat a et nisi. Nullam sit amet est tempus, condimentum odio egestas, dignissim nibh. Integer tempor id sapien at sagittis.
Aliquam in urna semper, suscipit metus in, pharetra lectus. Proin tempor nibh turpis, a pulvinar justo mollis sit amet. Fusce massa turpis, tristique id semper sit amet, venenatis in mauris. Maecenas id consectetur purus, sed efficitur ipsum. Sed pretium nisi ut sem pulvinar ullamcorper. Nullam at nunc et quam rhoncus egestas eu eu odio. Nullam commodo urna cursus massa porta consectetur. Sed blandit erat ut imperdiet malesuada. Cras nec fringilla ante. Nam sed gravida urna. Nam non dui quis turpis efficitur feugiat. Fusce in quam ex.
Nullam purus libero, egestas eget luctus et, malesuada eget est. Curabitur tristique sollicitudin est, imperdiet cras amet. `), []byte(`password`))
bob, _ := New([]byte("password"), nil)
bob.Encrypt([]byte("hello, world"))
}
}
func BenchmarkEncryption(b *testing.B) {
bob, _ := New([]byte("password"), nil)
for i := 0; i < b.N; i++ {
bob.Encrypt([]byte("hello, world"))
}
}
func TestEncryption(t *testing.T) {
bob, err := New([]byte("password"), nil)
assert.Nil(t, err)
jane, err := New([]byte("password"), bob.Salt())
assert.Nil(t, err)
enc, err := bob.Encrypt([]byte("hello, world"))
assert.Nil(t, err)
dec, err := jane.Decrypt(enc)
assert.Nil(t, err)
assert.Equal(t, dec, []byte("hello, world"))
jane2, err := New([]byte("password"), nil)
assert.Nil(t, err)
dec, err = jane2.Decrypt(enc)
assert.NotNil(t, err)
assert.NotEqual(t, dec, []byte("hello, world"))
jane3, err := New([]byte("passwordwrong"), bob.Salt())
assert.Nil(t, err)
dec, err = jane3.Decrypt(enc)
assert.NotNil(t, err)
assert.NotEqual(t, dec, []byte("hello, world"))
}
func TestNoEncryption(t *testing.T) {
bob, err := New(nil, nil)
assert.Nil(t, err)
jane, err := New(nil,nil)
assert.Nil(t, err)
enc, err := bob.Encrypt([]byte("hello, world"))
assert.Nil(t, err)
dec, err := jane.Decrypt(enc)
assert.Nil(t, err)
assert.Equal(t, dec, []byte("hello, world"))
assert.Equal(t, enc, []byte("hello, world"))
}

View File

@ -0,0 +1,69 @@
package main
import (
"fmt"
"io/ioutil"
"os/exec"
"strings"
)
func main() {
err := run()
if err != nil {
fmt.Println(err)
}
}
func run() (err error) {
version, err := exec.Command("git", "describe").Output()
if err != nil {
return
}
versionNew := strings.TrimSpace(string(version))
err = replaceInFile("src/cli/cli.go", `Version = "`, `"`, versionNew)
if err == nil {
fmt.Printf("updated cli.go to version %s\n", versionNew)
}
err = replaceInFile("README.md", `version-`, `-b`, strings.Split(versionNew, "-")[0])
if err == nil {
fmt.Printf("updated README to version %s\n", strings.Split(versionNew, "-")[0])
}
return
}
func replaceInFile(fname, start, end, replacement string) (err error) {
b, err := ioutil.ReadFile(fname)
if err != nil {
return
}
oldVersion := GetStringInBetween(string(b), start, end)
if oldVersion == "" {
err = fmt.Errorf("nothing")
return
}
newF := strings.Replace(
string(b),
fmt.Sprintf("%s%s%s", start, oldVersion, end),
fmt.Sprintf("%s%s%s", start, replacement, end),
1,
)
err = ioutil.WriteFile(fname, []byte(newF), 0644)
return
}
// GetStringInBetween Returns empty string if no start string found
func GetStringInBetween(str string, start string, end string) (result string) {
s := strings.Index(str, start)
if s == -1 {
return
}
s += len(start)
e := strings.Index(str[s:], end)
if e == -1 {
return
}
e += s
return str[s:e]
}

54
src/message/message.go Normal file
View File

@ -0,0 +1,54 @@
package message
import (
"encoding/json"
"github.com/schollz/croc/v6/src/comm"
"github.com/schollz/croc/v6/src/compress"
"github.com/schollz/croc/v6/src/crypt"
)
// Message is the possible payload for messaging
type Message struct {
Type string `json:"t,omitempty"`
Message string `json:"m,omitempty"`
Bytes []byte `json:"b,omitempty"`
Num int `json:"n,omitempty"`
}
func (m Message) String() string {
b, _ := json.Marshal(m)
return string(b)
}
// Send will send out
func Send(c *comm.Comm, key crypt.Encryption, m Message) (err error) {
mSend, err := Encode(key, m)
if err != nil {
return
}
_, err = c.Write(mSend)
return
}
// Encode will convert to bytes
func Encode(key crypt.Encryption, m Message) (b []byte, err error) {
b, err = json.Marshal(m)
if err != nil {
return
}
b = compress.Compress(b)
b, err = key.Encrypt(b)
return
}
// Decode will convert from bytes
func Decode(key crypt.Encryption, b []byte) (m Message, err error) {
b, err = key.Decrypt(b)
if err != nil {
return
}
b = compress.Decompress(b)
err = json.Unmarshal(b, &m)
return
}

View File

@ -0,0 +1,23 @@
package message
import (
"fmt"
"testing"
"github.com/schollz/croc/v6/src/crypt"
"github.com/stretchr/testify/assert"
)
func TestMessage(t *testing.T) {
m := Message{Type: "message", Message: "hello, world"}
e, err := crypt.New(nil, nil)
assert.Nil(t, err)
fmt.Println(e.Salt())
b, err := Encode(e, m)
assert.Nil(t, err)
fmt.Printf("%x\n", b)
m2, err := Decode(e, b)
assert.Nil(t, err)
assert.Equal(t, m, m2)
}

View File

@ -1,4 +1,3 @@
package models
const WEBSOCKET_BUFFER_SIZE = 1024 * 1024 * 32
const TCP_BUFFER_SIZE = 1024 * 64

View File

@ -1,14 +0,0 @@
package models
import "time"
// FileStats are the file stats transfered to the other
type FileStats struct {
Name string
Size int64
ModTime time.Time
IsDir bool
SentName string
IsCompressed bool
IsEncrypted bool
}

View File

@ -1,7 +0,0 @@
package models
type Initial struct {
CurveType string
IPAddress string
VersionString string
}

View File

@ -1,123 +0,0 @@
package relay
import (
"net/http"
"time"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/schollz/croc/src/models"
)
const (
// Time allowed to write a message to the peer.
writeWait = 6000 * 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 = models.WEBSOCKET_BUFFER_SIZE / 2
)
var upgrader = websocket.Upgrader{
ReadBufferSize: models.WEBSOCKET_BUFFER_SIZE,
WriteBufferSize: models.WEBSOCKET_BUFFER_SIZE,
}
// 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.Debugf("unexpected close: %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 {
err := c.write(websocket.CloseMessage, []byte{})
if err != nil {
log.Debug(err)
}
return
}
if err := c.write(message.messageType, message.data); err != nil {
log.Debug(err)
return
}
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
log.Debug(err)
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.Debug(err)
return
}
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()
}

View File

@ -1,105 +0,0 @@
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 {
if stop {
log.Debug("stopping hub")
return
}
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)
}
log.Debugf("deleting room %s", s.room)
delete(h.rooms.rooms, s.room)
}
h.rooms.Unlock()
case s := <-h.unregister:
// if one leaves, close all of them
h.rooms.Lock()
if _, ok := h.rooms.rooms[s.room]; ok {
for connection := range h.rooms.rooms[s.room] {
close(connection.send)
}
log.Debugf("deleting room %s", s.room)
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 {
log.Debugf("deleting room %s", m.room)
delete(h.rooms.rooms, m.room)
}
}
}
h.rooms.Unlock()
}
}
}

View File

@ -1,55 +0,0 @@
package relay
import (
"context"
"fmt"
"net/http"
"time"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/tcp"
)
var DebugLevel string
var stop bool
func Stop() {
log.Debug("got stop signal")
stop = true
}
// Run is the async operation for running a server
func Run(port string, tcpPorts []string) (err error) {
logger.SetLogLevel(DebugLevel)
if len(tcpPorts) > 0 {
for _, tcpPort := range tcpPorts {
go tcp.Run(DebugLevel, tcpPort)
}
}
go h.run()
log.Debug("running relay on " + port)
m := http.NewServeMux()
s := http.Server{Addr: ":" + port, Handler: m}
m.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(w, r)
})
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
})
go func() {
for {
if stop {
s.Shutdown(context.Background())
log.Debug("stopping http server")
return
}
time.Sleep(10 * time.Millisecond)
}
}()
s.ListenAndServe()
log.Debug("finished")
return
}

View File

@ -1,20 +1,32 @@
package tcp
import (
"bytes"
"fmt"
"net"
"strings"
"sync"
"time"
log "github.com/cihub/seelog"
"github.com/pkg/errors"
"github.com/schollz/croc/src/comm"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/models"
"github.com/schollz/croc/v6/src/comm"
"github.com/schollz/croc/v6/src/logger"
"github.com/schollz/croc/v6/src/models"
)
type server struct {
port string
debugLevel string
banner string
rooms roomMap
}
type roomInfo struct {
receiver comm.Comm
opened time.Time
first *comm.Comm
second *comm.Comm
opened time.Time
full bool
}
type roomMap struct {
@ -22,40 +34,49 @@ type roomMap struct {
sync.Mutex
}
var rooms roomMap
// Run starts a tcp listener, run async
func Run(debugLevel, port string) {
logger.SetLogLevel(debugLevel)
rooms.Lock()
rooms.rooms = make(map[string]roomInfo)
rooms.Unlock()
func Run(debugLevel, port string, banner ...string) (err error) {
s := new(server)
s.port = port
s.debugLevel = debugLevel
if len(banner) > 0 {
s.banner = banner[0]
}
return s.start()
}
func (s *server) start() (err error) {
logger.SetLogLevel(s.debugLevel)
s.rooms.Lock()
s.rooms.rooms = make(map[string]roomInfo)
s.rooms.Unlock()
// delete old rooms
go func() {
for {
time.Sleep(10 * time.Minute)
rooms.Lock()
for room := range rooms.rooms {
if time.Since(rooms.rooms[room].opened) > 3*time.Hour {
delete(rooms.rooms, room)
s.rooms.Lock()
for room := range s.rooms.rooms {
if time.Since(s.rooms.rooms[room].opened) > 3*time.Hour {
delete(s.rooms.rooms, room)
}
}
rooms.Unlock()
s.rooms.Unlock()
}
}()
err := run(port)
err = s.run()
if err != nil {
log.Error(err)
}
return
}
func run(port string) (err error) {
log.Debugf("starting TCP server on " + port)
server, err := net.Listen("tcp", "0.0.0.0:"+port)
func (s *server) run() (err error) {
log.Infof("starting TCP server on " + s.port)
server, err := net.Listen("tcp", ":"+s.port)
if err != nil {
return errors.Wrap(err, "Error listening on :"+port)
return errors.Wrap(err, "Error listening on :"+s.port)
}
defer server.Close()
// spawn a new goroutine whenever a client connects
@ -66,81 +87,115 @@ func run(port string) (err error) {
}
log.Debugf("client %s connected", connection.RemoteAddr().String())
go func(port string, connection net.Conn) {
errCommunication := clientCommuncation(port, comm.New(connection))
errCommunication := s.clientCommuncation(port, comm.New(connection))
if errCommunication != nil {
log.Warnf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
}
}(port, connection)
}(s.port, connection)
}
}
func clientCommuncation(port string, c comm.Comm) (err error) {
func (s *server) clientCommuncation(port string, c *comm.Comm) (err error) {
// send ok to tell client they are connected
log.Debug("sending ok")
err = c.Send("ok")
banner := s.banner
if len(banner) == 0 {
banner = "ok"
}
log.Debugf("sending '%s'", banner)
err = c.Send([]byte(banner + "|||" + c.Connection().RemoteAddr().String()))
if err != nil {
return
}
// wait for client to tell me which room they want
log.Debug("waiting for answer")
room, err := c.Receive()
roomBytes, err := c.Receive()
if err != nil {
return
}
room := string(roomBytes)
rooms.Lock()
// first connection is always the receiver
if _, ok := rooms.rooms[room]; !ok {
rooms.rooms[room] = roomInfo{
receiver: c,
opened: time.Now(),
s.rooms.Lock()
// create the room if it is new
if _, ok := s.rooms.rooms[room]; !ok {
s.rooms.rooms[room] = roomInfo{
first: c,
opened: time.Now(),
}
rooms.Unlock()
s.rooms.Unlock()
// tell the client that they got the room
err = c.Send("recipient")
err = c.Send([]byte("ok"))
if err != nil {
log.Error(err)
s.deleteRoom(room)
return
}
log.Debug("recipient connected")
log.Debugf("room %s has 1", room)
return nil
}
log.Debug("sender connected")
receiver := rooms.rooms[room].receiver
rooms.Unlock()
if s.rooms.rooms[room].full {
s.rooms.Unlock()
err = c.Send([]byte("room full"))
if err != nil {
log.Error(err)
s.deleteRoom(room)
return
}
return nil
}
log.Debugf("room %s has 2", room)
s.rooms.rooms[room] = roomInfo{
first: s.rooms.rooms[room].first,
second: c,
opened: s.rooms.rooms[room].opened,
full: true,
}
otherConnection := s.rooms.rooms[room].first
s.rooms.Unlock()
// second connection is the sender, time to staple connections
var wg sync.WaitGroup
wg.Add(1)
// start piping
go func(com1, com2 comm.Comm, wg *sync.WaitGroup) {
go func(com1, com2 *comm.Comm, wg *sync.WaitGroup) {
log.Debug("starting pipes")
pipe(com1.Connection(), com2.Connection())
wg.Done()
log.Debug("done piping")
}(c, receiver, &wg)
}(otherConnection, c, &wg)
// tell the sender everything is ready
err = c.Send("sender")
err = c.Send([]byte("ok"))
if err != nil {
s.deleteRoom(room)
return
}
wg.Wait()
// delete room
rooms.Lock()
log.Debugf("deleting room: %s", room)
delete(rooms.rooms, room)
rooms.Unlock()
s.deleteRoom(room)
return nil
}
func (s *server) deleteRoom(room string) {
s.rooms.Lock()
defer s.rooms.Unlock()
if _, ok := s.rooms.rooms[room]; !ok {
return
}
log.Debugf("deleting room: %s", room)
s.rooms.rooms[room].first.Close()
s.rooms.rooms[room].second.Close()
s.rooms.rooms[room] = roomInfo{first: nil, second: nil}
delete(s.rooms.rooms, room)
}
// 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)
c := make(chan []byte, 1)
go func() {
b := make([]byte, models.TCP_BUFFER_SIZE)
@ -159,6 +214,7 @@ func chanFromConn(conn net.Conn) chan []byte {
break
}
}
log.Debug("exiting")
}()
return c
@ -186,3 +242,33 @@ func pipe(conn1 net.Conn, conn2 net.Conn) {
}
}
}
func ConnectToTCPServer(address, room string) (c *comm.Comm, banner string, ipaddr string, err error) {
c, err = comm.NewConnection(address)
if err != nil {
return
}
log.Debug("waiting for first ok")
data, err := c.Receive()
if err != nil {
return
}
banner = strings.Split(string(data), "|||")[0]
ipaddr = strings.Split(string(data), "|||")[1]
log.Debug("sending room")
err = c.Send([]byte(room))
if err != nil {
return
}
log.Debug("waiting for room confirmation")
data, err = c.Receive()
if err != nil {
return
}
if !bytes.Equal(data, []byte("ok")) {
err = fmt.Errorf("got bad response: %s", data)
return
}
log.Debug("all set")
return
}

36
src/tcp/tcp_test.go Normal file
View File

@ -0,0 +1,36 @@
package tcp
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTCP(t *testing.T) {
go Run("debug", "8081", "8082")
time.Sleep(100 * time.Millisecond)
c1, banner, ipaddr, err := ConnectToTCPServer("localhost:8081", "testRoom")
assert.Equal(t, banner, "8082")
assert.True(t, strings.HasPrefix(ipaddr, "127.0.0.1"))
assert.Nil(t, err)
c2, _, _, err := ConnectToTCPServer("localhost:8081", "testRoom")
assert.Nil(t, err)
_, _, _, err = ConnectToTCPServer("localhost:8081", "testRoom")
assert.NotNil(t, err)
// try sending data
assert.Nil(t, c1.Send([]byte("hello, c2")))
data, err := c2.Receive()
assert.Nil(t, err)
assert.Equal(t, []byte("hello, c2"), data)
assert.Nil(t, c2.Send([]byte("hello, c1")))
data, err = c1.Receive()
assert.Nil(t, err)
assert.Equal(t, []byte("hello, c1"), data)
c1.Close()
time.Sleep(300 * time.Millisecond)
}

View File

@ -1,13 +0,0 @@
package utils
import "os"
// 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
}

View File

@ -1,15 +0,0 @@
package utils
import (
"bufio"
"fmt"
"os"
"strings"
)
func GetInput(prompt string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stderr, "%s", prompt)
text, _ := reader.ReadString('\n')
return strings.TrimSpace(text)
}

View File

@ -1,33 +0,0 @@
package utils
import (
"crypto/md5"
"crypto/sha256"
"fmt"
"io"
"os"
)
// HashFile returns the md5 hash of a file
func HashFile(fname string) (hash256 []byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := md5.New()
if _, err = io.Copy(h, f); err != nil {
return
}
hash256 = h.Sum(nil)
return
}
// SHA256 returns sha256 sum
func SHA256(s string) string {
sha := sha256.New()
sha.Write([]byte(s))
return fmt.Sprintf("%x", sha.Sum(nil))
}

View File

@ -1,39 +0,0 @@
package utils
import (
"io/ioutil"
"log"
"net"
"net/http"
"strings"
)
func PublicIP() (ip string, err error) {
resp, err := http.Get("https://canhazip.com")
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
ip = strings.TrimSpace(string(bodyBytes))
}
return
}
// Get preferred outbound ip of this machine
func LocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}

View File

@ -1,11 +0,0 @@
package utils
import (
"fmt"
"testing"
)
func TestGetIP(t *testing.T) {
fmt.Println(PublicIP())
fmt.Println(LocalIP())
}

View File

@ -1,16 +0,0 @@
package utils
import (
"crypto/rand"
"strings"
"github.com/schollz/mnemonicode"
)
func GetRandomName() string {
result := []string{}
bs := make([]byte, 4)
rand.Read(bs)
result = mnemonicode.EncodeWordList(result, bs)
return strings.Join(result, "-")
}

154
src/utils/utils.go Normal file
View File

@ -0,0 +1,154 @@
package utils
import (
"bufio"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"log"
"bytes"
"net"
"math"
"net/http"
"os"
"strings"
"github.com/schollz/mnemonicode"
)
// 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
}
// GetInput returns the input with a given prompt
func GetInput(prompt string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stderr, "%s", prompt)
text, _ := reader.ReadString('\n')
return strings.TrimSpace(text)
}
// HashFile returns the md5 hash of a file
func HashFile(fname string) (hash256 []byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := md5.New()
if _, err = io.Copy(h, f); err != nil {
return
}
hash256 = h.Sum(nil)
return
}
// SHA256 returns sha256 sum
func SHA256(s string) string {
sha := sha256.New()
sha.Write([]byte(s))
return fmt.Sprintf("%x", sha.Sum(nil))
}
func PublicIP() (ip string, err error) {
resp, err := http.Get("https://canhazip.com")
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
ip = strings.TrimSpace(string(bodyBytes))
}
return
}
// Get preferred outbound ip of this machine
func LocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
func GetRandomName() string {
result := []string{}
bs := make([]byte, 4)
rand.Read(bs)
result = mnemonicode.EncodeWordList(result, bs)
return strings.Join(result, "-")
}
func ByteCountDecimal(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
// MissingChunks returns the positions of missing chunks.
// If file doesn't exist, it returns an empty chunk list (all chunks).
// If the file size is not the same as requested, it returns an empty chunk list (all chunks).
func MissingChunks(fname string, fsize int64, chunkSize int) (chunks []int64) {
fstat, err := os.Stat(fname)
if fstat.Size() != fsize || err != nil {
return
}
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
emptyBuffer := make([]byte, chunkSize)
chunkNum := 0
chunks = make([]int64, int64(math.Ceil(float64(fsize)/float64(chunkSize))))
var currentLocation int64
for {
buffer := make([]byte, chunkSize)
bytesread, err := f.Read(buffer)
if err != nil {
break
}
if bytes.Equal(buffer[:bytesread], emptyBuffer[:bytesread]) {
chunks[chunkNum] = currentLocation
chunkNum++
}
currentLocation += int64(bytesread)
}
if chunkNum == 0 {
chunks = []int64{}
} else {
chunks = chunks[:chunkNum]
}
return
}

29
src/utils/utils_test.go Normal file
View File

@ -0,0 +1,29 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// Exists reports whether the named file or directory exists.
func TestExists(t *testing.T) {
assert.True(t, Exists("utils.go"))
}
// HashFile returns the md5 hash of a file
func TestHashFile(t *testing.T) {
b, err := HashFile("utils.go")
assert.Nil(t, err)
assert.Equal(t, "314965d4170cd0c70bd2e4f6c977750a", fmt.Sprintf("%x", b))
}
// SHA256 returns sha256 sum
func TestSHA256(t *testing.T) {
assert.Equal(t, "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b", SHA256("hello, world"))
}
func TestByteCountDecimal(t *testing.T) {
assert.Equal(t, "10.0 kB", ByteCountDecimal(10000))
}

View File

@ -1,24 +0,0 @@
VERSION=$(shell git describe --tags --abbrev=0)
LDFLAGS=-ldflags "-X main.Version=${VERSION}"
.PHONY: linux
linux:
GO111MODULE=off qtdeploy ${LDFLAGS} --tags='wincroc' --debug build desktop
.PHONY: fast
fast:
GO111MODULE=off qtdeploy ${LDFLAGS} --fast --tags='wincroc' --debug build desktop
windows:
GO111MODULE=off qtdeploy ${LDFLAGS} --tags='wincroc' --debug --docker build windows_64_static
release: linux windows
mv deploy/linux/win croc
tar -czvf croc_${VERSION}_Linux-64bit_GUI.tar.gz croc
mv deploy/windows/win.exe croc.exe
zip croc_${VERSION}_Windows-64bit_GUI.zip croc.exe
rm -rf dist
mkdir dist
mv *zip dist/
mv *tar.gz dist/

View File

@ -1,14 +0,0 @@
REM getting the output of an executed command in a batch file is not trivial, we use a temp file for it
git describe --tags --abbrev=0 > temp.txt
set /P VERSION=< temp.txt
echo %VERSION%
del temp.txt
REM build a 32 bit Windows application, this way it will run on both 32 but and 64 bit Windows machines
set GOARCH=386
REM -s and -w strip the program of debugging information, making it smaller
REM -H=windowsgui makes the program not have a console window on start-up
go build -ldflags="-s -w -H=windowsgui -X main.Version=%VERSION%" -o croc.exe
if errorlevel 1 pause

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "icon.ico"

Binary file not shown.

Binary file not shown.

View File

@ -1,230 +0,0 @@
// +build wincroc
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/schollz/croc/src/cli"
"github.com/schollz/croc/src/croc"
"github.com/schollz/croc/src/utils"
"github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/widgets"
)
type CustomLabel struct {
widgets.QLabel
_ func(string) `signal:"updateTextFromGoroutine,auto(this.QLabel.setText)"` //TODO: support this.setText as well
}
var Version string
func main() {
if len(os.Args) > 1 {
cli.Run()
return
}
var isWorking bool
app := widgets.NewQApplication(len(os.Args), os.Args)
window := widgets.NewQMainWindow(nil, 0)
if runtime.GOOS == "windows" {
window.SetFixedSize2(300, 150)
window.SetWindowTitle("croc " + Version)
} else {
window.SetFixedSize2(400, 150)
window.SetWindowTitle("🐊📦 croc " + Version)
}
widget := widgets.NewQWidget(nil, 0)
widget.SetLayout(widgets.NewQVBoxLayout())
window.SetCentralWidget(widget)
labels := make([]*CustomLabel, 3)
for i := range labels {
label := NewCustomLabel(nil, 0)
label.SetAlignment(core.Qt__AlignCenter)
widget.Layout().AddWidget(label)
labels[i] = label
}
labels[0].SetText("secure data transfer")
labels[1].SetText("Click 'Send' or 'Receive' to start")
button := widgets.NewQPushButton2("Send", nil)
button.ConnectClicked(func(bool) {
if isWorking {
dialog("Can only do one send or receive at a time")
return
}
isWorking = true
var fileDialog = widgets.NewQFileDialog2(window, "Open file to send...", "", "")
fileDialog.SetAcceptMode(widgets.QFileDialog__AcceptOpen)
fileDialog.SetFileMode(widgets.QFileDialog__AnyFile)
if fileDialog.Exec() != int(widgets.QDialog__Accepted) {
isWorking = false
return
}
var fn = fileDialog.SelectedFiles()[0]
if len(fn) == 0 {
dialog(fmt.Sprintf("No file selected"))
isWorking = false
return
}
go func() {
cr := croc.Init(false)
done := make(chan bool)
codePhrase := utils.GetRandomName()
_, fname := filepath.Split(fn)
labels[0].UpdateTextFromGoroutine(fmt.Sprintf("Sending '%s'", fname))
labels[1].UpdateTextFromGoroutine(fmt.Sprintf("Code phrase: %s", codePhrase))
go func(done chan bool) {
for {
if cr.OtherIP != "" && cr.FileInfo.SentName != "" {
bytesString := humanize.Bytes(uint64(cr.FileInfo.Size))
fileOrFolder := "file"
if cr.FileInfo.IsDir {
fileOrFolder = "folder"
}
labels[0].UpdateTextFromGoroutine(fmt.Sprintf("Sending %s %s '%s' to %s", bytesString, fileOrFolder, cr.FileInfo.SentName, cr.OtherIP))
}
if cr.Bar != nil {
barState := cr.Bar.State()
labels[1].UpdateTextFromGoroutine(fmt.Sprintf("%2.1f%% [%2.0fs:%2.0fs]", barState.CurrentPercent*100, barState.SecondsSince, barState.SecondsLeft))
}
labels[2].UpdateTextFromGoroutine(cr.StateString)
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].UpdateTextFromGoroutine(cr.StateString)
return
default:
continue
}
}
}(done)
cr.Send(fn, codePhrase)
done <- true
isWorking = false
}()
})
widget.Layout().AddWidget(button)
receiveButton := widgets.NewQPushButton2("Receive", nil)
receiveButton.ConnectClicked(func(bool) {
if isWorking {
dialog("Can only do one send or receive at a time")
return
}
labels[1].SetText("")
labels[2].SetText("please wait...")
isWorking = true
defer func() {
isWorking = false
}()
// determine the folder to save the file
var folderDialog = widgets.NewQFileDialog2(window, "Open folder to receive file...", "", "")
folderDialog.SetAcceptMode(widgets.QFileDialog__AcceptOpen)
folderDialog.SetFileMode(widgets.QFileDialog__DirectoryOnly)
if folderDialog.Exec() != int(widgets.QDialog__Accepted) {
return
}
var fn = folderDialog.SelectedFiles()[0]
if len(fn) == 0 {
labels[2].SetText(fmt.Sprintf("No folder selected"))
return
}
var codePhrase = widgets.QInputDialog_GetText(window, "croc", "Enter code phrase:",
widgets.QLineEdit__Normal, "", true, core.Qt__Dialog, core.Qt__ImhNone)
if len(codePhrase) < 3 {
labels[2].SetText(fmt.Sprintf("Invalid codephrase: '%s'", codePhrase))
return
}
cr := croc.Init(false)
cr.WindowRecipientPrompt = true
done := make(chan bool)
go func() {
// change into the receiving directory
cwd, _ := os.Getwd()
defer os.Chdir(cwd)
os.Chdir(fn)
err := cr.Receive(codePhrase)
if err == nil {
open.Run(fn)
}
done <- true
done <- true
isWorking = false
}()
go func() {
for {
if cr.Bar != nil {
barState := cr.Bar.State()
labels[1].UpdateTextFromGoroutine(fmt.Sprintf("%2.1f%% [%2.0fs:%2.0fs]", barState.CurrentPercent*100, barState.SecondsSince, barState.SecondsLeft))
}
if cr.StateString != "" {
labels[2].UpdateTextFromGoroutine(cr.StateString)
}
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].UpdateTextFromGoroutine(cr.StateString)
return
default:
continue
}
}
}()
for {
if cr.WindowReceivingString != "" {
var question = widgets.QMessageBox_Question(window, "croc", fmt.Sprintf("%s?", cr.WindowReceivingString), widgets.QMessageBox__Yes|widgets.QMessageBox__No, 0)
if question == widgets.QMessageBox__Yes {
cr.WindowRecipientAccept = true
labels[0].SetText(cr.WindowReceivingString)
} else {
cr.WindowRecipientAccept = false
labels[2].SetText("canceled")
}
cr.WindowRecipientPrompt = false
cr.WindowReceivingString = ""
break
}
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].SetText(cr.StateString)
return
default:
continue
}
}
})
widget.Layout().AddWidget(receiveButton)
window.Show()
app.Exec()
}
func dialog(s string) {
var info = widgets.NewQMessageBox(nil)
info.SetWindowTitle("Info")
info.SetText(s)
info.Exec()
}

View File

@ -1,250 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/gonutz/w32"
"github.com/gonutz/wui"
"github.com/schollz/croc/src/cli"
"github.com/schollz/croc/src/croc"
"github.com/schollz/croc/src/utils"
"github.com/skratchdot/open-golang/open"
)
var Version string
func main() {
if len(os.Args) > 1 {
cli.Run()
return
}
var isWorking bool
font, _ := wui.NewFont(wui.FontDesc{
Name: "Tahoma",
Height: -11,
})
window := wui.NewWindow()
window.SetFont(font)
window.SetIconFromMem(icon)
window.SetStyle(w32.WS_OVERLAPPED | w32.WS_CAPTION | w32.WS_SYSMENU | w32.WS_MINIMIZEBOX)
if runtime.GOOS == "windows" {
window.SetClientSize(300, 150)
window.SetTitle("croc " + Version)
} else {
window.SetClientSize(400, 150)
window.SetTitle("🐊📦 croc " + Version)
}
labels := make([]*wui.Label, 3)
for i := range labels {
label := wui.NewLabel()
label.SetCenterAlign()
label.SetBounds(0, 10+i*20, window.ClientWidth(), 20)
window.Add(label)
labels[i] = label
}
labels[0].SetText("secure data transfer")
labels[1].SetText("Click 'Send' or 'Receive' to start")
button := wui.NewButton()
button.SetText("Send")
window.Add(button)
button.SetBounds(10, window.ClientHeight()-70, window.ClientWidth()-20, 25)
button.SetOnClick(func() {
if isWorking {
wui.MessageBoxError(window, "Error", "Can only do one send or receive at a time")
return
}
isWorking = true
fileDialog := wui.NewFileOpenDialog()
fileDialog.SetTitle("Open file to send...")
accepted, fn := fileDialog.ExecuteSingleSelection(window)
if !accepted {
isWorking = false
return
}
if fn == "" {
wui.MessageBoxError(window, "Error", "No file selected")
isWorking = false
return
}
go func() {
cr := croc.Init(false)
done := make(chan bool)
codePhrase := utils.GetRandomName()
_, fname := filepath.Split(fn)
labels[0].SetText(fmt.Sprintf("Sending '%s'", fname))
labels[1].SetText(fmt.Sprintf("Code phrase: %s", codePhrase))
go func(done chan bool) {
for {
if cr.OtherIP != "" && cr.FileInfo.SentName != "" {
bytesString := humanize.Bytes(uint64(cr.FileInfo.Size))
fileOrFolder := "file"
if cr.FileInfo.IsDir {
fileOrFolder = "folder"
}
labels[0].SetText(fmt.Sprintf("Sending %s %s '%s' to %s", bytesString, fileOrFolder, cr.FileInfo.SentName, cr.OtherIP))
}
if cr.Bar != nil {
barState := cr.Bar.State()
labels[1].SetText(fmt.Sprintf("%2.1f%% [%2.0fs:%2.0fs]", barState.CurrentPercent*100, barState.SecondsSince, barState.SecondsLeft))
}
labels[2].SetText(cr.StateString)
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].SetText(cr.StateString)
return
default:
continue
}
}
}(done)
cr.Send(fn, codePhrase)
done <- true
isWorking = false
}()
})
receiveButton := wui.NewButton()
receiveButton.SetText("Receive")
window.Add(receiveButton)
receiveButton.SetBounds(10, window.ClientHeight()-35, window.ClientWidth()-20, 25)
receiveButton.SetOnClick(func() {
if isWorking {
wui.MessageBoxError(window, "Error", "Can only do one send or receive at a time")
return
}
labels[1].SetText("")
labels[2].SetText("please wait...")
isWorking = true
defer func() {
isWorking = false
}()
// determine the folder to save the file
folderDialog := wui.NewFolderSelectDialog()
folderDialog.SetTitle("Open folder to receive file...")
accepted, fn := folderDialog.Execute(window)
if !accepted {
return
}
if len(fn) == 0 {
labels[2].SetText(fmt.Sprintf("No folder selected"))
return
}
passDlg := wui.NewDialogWindow()
passDlg.SetTitle("Enter code phrase")
passDlg.SetClientSize(window.ClientWidth()-20, 40)
pass := wui.NewEditLine()
passDlg.Add(pass)
pass.SetPassword(true)
pass.SetBounds(10, 10, passDlg.ClientWidth()-20, 20)
var passAccepted bool
passDlg.SetShortcut(wui.ShortcutKeys{Key: w32.VK_RETURN}, func() {
passAccepted = true
passDlg.Close()
})
passDlg.SetShortcut(wui.ShortcutKeys{Key: w32.VK_ESCAPE}, func() {
passDlg.Close()
})
var codePhrase string
passDlg.SetOnShow(func() {
passDlg.SetX(window.X() + (window.Width()-passDlg.Width())/2)
passDlg.SetY(window.Y() + (window.Height()-passDlg.Height())/2)
pass.Focus()
})
passDlg.SetOnClose(func() {
if passAccepted {
codePhrase = pass.Text()
}
})
passDlg.ShowModal(window)
passDlg.Destroy()
if len(codePhrase) < 3 {
labels[2].SetText(fmt.Sprintf("Invalid codephrase: '%s'", codePhrase))
return
}
cr := croc.Init(false)
cr.WindowRecipientPrompt = true
done := make(chan bool)
go func() {
// change into the receiving directory
cwd, _ := os.Getwd()
defer os.Chdir(cwd)
os.Chdir(fn)
err := cr.Receive(codePhrase)
if err == nil {
open.Run(fn)
}
done <- true
done <- true
isWorking = false
}()
go func() {
for {
if cr.Bar != nil {
barState := cr.Bar.State()
labels[1].SetText(fmt.Sprintf("%2.1f%% [%2.0fs:%2.0fs]", barState.CurrentPercent*100, barState.SecondsSince, barState.SecondsLeft))
}
if cr.StateString != "" {
labels[2].SetText(cr.StateString)
}
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].SetText(cr.StateString)
return
default:
continue
}
}
}()
for {
if cr.WindowReceivingString != "" {
question := wui.MessageBoxYesNo(
window,
"croc",
fmt.Sprintf("%s?", cr.WindowReceivingString),
)
if question {
cr.WindowRecipientAccept = true
labels[0].SetText(cr.WindowReceivingString)
} else {
cr.WindowRecipientAccept = false
labels[2].SetText("canceled")
}
cr.WindowRecipientPrompt = false
cr.WindowReceivingString = ""
break
}
time.Sleep(100 * time.Millisecond)
select {
case _ = <-done:
labels[2].SetText(cr.StateString)
return
default:
continue
}
}
})
window.Show()
}

View File

@ -1,230 +0,0 @@
package zipper
import (
"archive/zip"
"compress/flate"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/logger"
)
var DebugLevel string
func init() {
DebugLevel = "info"
}
// UnzipFile will unzip the src directory into the dest
func UnzipFile(src, dest string) (err error) {
logger.SetLogLevel(DebugLevel)
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
}
// ZipFiles will zip all the files and the folders as if they were in the same directory
func ZipFiles(fnames []string, compress bool) (writtenFilename string, err error) {
logger.SetLogLevel(DebugLevel)
if len(fnames) == 0 {
err = fmt.Errorf("must provide files to zip")
return
}
log.Debugf("zipping %s with compression? %v", fnames, compress)
writtenFilename = fmt.Sprintf("%d_files.croc.zip", len(fnames))
err = makeZip(writtenFilename, fnames, compress)
return
}
// ZipFile will zip the folder
func ZipFile(fname string, compress bool) (writtenFilename string, err error) {
logger.SetLogLevel(DebugLevel)
// get path to file and the filename
_, filename := filepath.Split(fname)
writtenFilename = filename + ".croc.zip"
err = makeZip(writtenFilename, []string{fname}, compress)
return
}
func makeZip(writtenFilename string, fnames []string, compress bool) (err error) {
log.Debugf("creating file: %s", writtenFilename)
f, err := os.Create(writtenFilename)
if err != nil {
log.Error(err)
return
}
defer f.Close()
zipWriter := zip.NewWriter(f)
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()
err = zipFiles(fnames, compress, zipWriter)
if err == nil {
log.Debugf("wrote zip file to %s", writtenFilename)
} else {
log.Error(err)
}
return
}
func zipFiles(fnames []string, compress bool, zipWriter *zip.Writer) (err error) {
for _, fname := range fnames {
// get absolute filename
absPath, err := filepath.Abs(filepath.Clean(fname))
if err != nil {
log.Error(err)
return err
}
absPath = filepath.ToSlash(absPath)
// get path to file and the filename
fpath, fname := filepath.Split(absPath)
// Get the file information for the target
log.Debugf("checking %s", absPath)
ftarget, err := os.Open(absPath)
if err != nil {
log.Error(err)
return err
}
defer ftarget.Close()
info, err := ftarget.Stat()
if err != nil {
log.Error(err)
return err
}
// write header informaiton
header, err := zip.FileInfoHeader(info)
if err != nil {
log.Error(err)
return err
}
var writer io.Writer
if info.IsDir() {
baseDir := filepath.Clean(path.Join(fpath, fname))
log.Debugf("walking base dir: %s", baseDir)
filepath.Walk(baseDir, func(curpath string, info os.FileInfo, err error) error {
curpath = filepath.Clean(curpath)
if err != nil {
log.Error(err)
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
log.Error(err)
return err
}
if baseDir != "" {
header.Name = path.Join(fname, strings.TrimPrefix(curpath, baseDir))
}
header.Name = filepath.ToSlash(filepath.Clean(header.Name))
log.Debug(header.Name)
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err = zipWriter.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(curpath)
if err != nil {
log.Error(err)
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
}
_, err = io.Copy(writer, ftarget)
if err != nil {
log.Error(err)
return err
}
}
}
return nil
}

View File

@ -1,47 +0,0 @@
package zipper
import (
"os"
"path"
"testing"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/utils"
"github.com/stretchr/testify/assert"
)
func TestZip(t *testing.T) {
defer log.Flush()
DebugLevel = "debug"
writtenFilename1, err := ZipFile("../croc", true)
assert.Nil(t, err)
err = UnzipFile(writtenFilename1, ".")
assert.Nil(t, err)
assert.True(t, utils.Exists("croc"))
writtenFilename2, err := ZipFile("../../README.md", false)
assert.Nil(t, err)
err = UnzipFile(writtenFilename2, ".")
assert.Nil(t, err)
assert.True(t, utils.Exists("README.md"))
os.Remove("README.md")
os.RemoveAll("croc")
os.Remove(writtenFilename1)
os.Remove(writtenFilename2)
}
func TestZipFiles(t *testing.T) {
defer log.Flush()
DebugLevel = "debug"
writtenFilename, err := ZipFiles([]string{"../../LICENSE", "../win/Makefile", "../utils"}, true)
assert.Nil(t, err)
err = UnzipFile(writtenFilename, "zipfilestest")
assert.Nil(t, err)
assert.True(t, utils.Exists("zipfilestest/LICENSE"))
assert.True(t, utils.Exists("zipfilestest/Makefile"))
assert.True(t, utils.Exists("zipfilestest/utils/exists.go"))
os.RemoveAll("zipfilestest")
err = os.Remove(path.Join(".", writtenFilename))
assert.Nil(t, err)
}