Compare commits

..

No commits in common. "main" and "v4.0.4" have entirely different histories.
main ... v4.0.4

72 changed files with 2659 additions and 8110 deletions

BIN
.github/1.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
.github/2.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
.github/3.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
.github/4.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
# These are supported funding model platforms
github: schollz

View File

@ -1,9 +0,0 @@
---
name: New issue
about: Create an issue
title: ''
assignees: ''
---
*Read this and delete before submitting:* Thanks for starting a discussion! Please provide as much context as possible so that others can understand your thoughts. If you have a specific change in mind, consider submitting a pull request instead.

View File

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"

View File

@ -1,52 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
name: Go unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- run: go version
- run: go test -v ./...
- name: Build files
run: |
go version
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc.exe
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.20.7' # go1.20.8+ refuses to build go1.22 code...
- name: Build Windows 7
run: |
go version
rm go.mod go.sum
go mod init github.com/schollz/croc/v10
go mod tidy
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe

View File

@ -1,64 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CD
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches:
- '*'
tags:
- 'v*'
pull_request:
branches:
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Docker meta
id: docker_meta
uses: docker/metadata-action@v5
with:
images: schollz/croc
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64,linux/386
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@ -1,131 +0,0 @@
name: Make release
on:
release:
types: [created]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Prepare source tarball
run: |
git clone -b ${{ github.event.release.name }} --depth 1 https://github.com/schollz/croc croc-${{ github.event.release.name }}
cd croc-${{ github.event.release.name }} && go mod tidy && go mod vendor
cd .. && tar -czvf croc_${{ github.event.release.name }}_src.tar.gz croc-${{ github.event.release.name }}
- name: Build files
run: |
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows-64bit.zip croc.exe LICENSE
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows-32bit.zip croc.exe LICENSE
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows-ARM.zip croc.exe LICENSE
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows-ARM64.zip croc.exe LICENSE
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc
tar -czvf croc_${{ github.event.release.name }}_Linux-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc
tar -czvf croc_${{ github.event.release.name }}_Linux-32bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM.tar.gz croc LICENSE
GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
tar -czvf croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
tar -czvf croc_${{ github.event.release.name }}_macOS-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
tar -czvf croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz croc LICENSE
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz croc LICENSE
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.20.7' # go1.20.8+ refuses to build go1.22 code...
- name: Build Windows 7
run: |
go version
rm go.mod go.sum
go mod init github.com/schollz/croc/v10
go mod tidy
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows7-64bit.zip croc.exe
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
zip croc_${{ github.event.release.name }}_Windows7-32bit.zip croc.exe
- name: Create checksums.txt
run: |
touch croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_src.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows-ARM.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows-ARM64.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows7-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Windows7-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Linux-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Linux-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Linux-ARM.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_macOS-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
sha256sum croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |
croc_${{ github.event.release.name }}_checksums.txt
croc_${{ github.event.release.name }}_src.tar.gz
croc_${{ github.event.release.name }}_Windows-64bit.zip
croc_${{ github.event.release.name }}_Windows-32bit.zip
croc_${{ github.event.release.name }}_Windows-ARM.zip
croc_${{ github.event.release.name }}_Windows-ARM64.zip
croc_${{ github.event.release.name }}_Windows7-64bit.zip
croc_${{ github.event.release.name }}_Windows7-32bit.zip
croc_${{ github.event.release.name }}_Linux-64bit.tar.gz
croc_${{ github.event.release.name }}_Linux-32bit.tar.gz
croc_${{ github.event.release.name }}_Linux-ARM.tar.gz
croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz
croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz
croc_${{ github.event.release.name }}_macOS-64bit.tar.gz
croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz
croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz
croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz
croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz
croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz
croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz
croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz
croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz
croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz

View File

@ -1,27 +0,0 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '19 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
stale-pr-message: 'Stale pull request message'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'

View File

@ -1,14 +0,0 @@
name: Publish to Winget
on:
release:
types: [released]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: schollz.croc
installers-regex: '_Windows-\w+\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

13
.gitignore vendored
View File

@ -1,13 +0,0 @@
# Binaries
/croc
/croc.exe
zsh_autocomplete
bash_autocomplete
dist
bin
croc-stdin*
.idea/
.vscode/
src/utils/bigfile.test
test1/

View File

@ -1,100 +0,0 @@
project_name: croc
build:
main: main.go
binary: croc
ldflags: -s -w -X main.Version="v{{.Version}}-{{.Date}}"
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
- freebsd
- netbsd
- openbsd
- dragonfly
goarch:
- amd64
- 386
- arm
- arm64
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
goarm:
- 7
nfpms:
-
formats:
- deb
vendor: "schollz.com"
homepage: "https://schollz.com/software/croc/"
maintainer: "Zack Scholl <zack.scholl@gmail.com>"
description: "A simple, secure, and fast way to transfer data."
license: "MIT"
file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
archives:
-
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
files:
- README.md
- LICENSE
- zsh_autocomplete
- bash_autocomplete
brews:
-
tap:
owner: schollz
name: homebrew-tap
folder: Formula
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
homepage: "https://schollz.com/software/croc/"
install: |
bin.install "croc"
test: |
system "#{bin}/croc --version"
scoop:
bucket:
owner: schollz
name: scoop-bucket
homepage: "https://schollz.com/software/croc/"
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
license: MIT
announce:
twitter:
# Wether its enabled or not.
# Defaults to false.
enabled: false

View File

@ -6,18 +6,6 @@ go:
env:
- "PATH=/home/travis/gopath/bin:$PATH"
install: true
script:
- env GO111MODULE=on go build -v
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/compress
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/croc
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/crypt
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/tcp
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/utils
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/comm
branches:
except:
- dev
- win
- go build -v
- go test -v -cover ./...

View File

@ -1,16 +0,0 @@
FROM golang:1.22-alpine as builder
RUN apk add --no-cache git gcc musl-dev
WORKDIR /go/croc
COPY . .
RUN go build -v -ldflags="-s -w"
FROM alpine:latest
EXPOSE 9009
EXPOSE 9010
EXPOSE 9011
EXPOSE 9012
EXPOSE 9013
COPY --from=builder /go/croc/croc /go/croc/croc-entrypoint.sh /
USER nobody
ENTRYPOINT ["/croc-entrypoint.sh"]
CMD ["relay"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2024 Zack
Copyright (c) 2017 Zack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

410
README.md
View File

@ -1,281 +1,129 @@
<p align="center">
<img
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-v10.1.1-brightgreen.svg?style=flat-square" alt="Version"></a>
<a href="https://github.com/schollz/croc/actions/workflows/ci.yml"><img
src="https://github.com/schollz/croc/actions/workflows/ci.yml/badge.svg" alt="Build
Status"></a>
<p align="center">This project is supported by <a href="https://github.com/sponsors/schollz">Github sponsors</a>.</p>
`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 that does **all** of the following:
- 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
- local server or port-forwarding **not needed**
- **ipv6-first** with ipv4 fallback
- can **use proxy**, like tor
For more information about `croc`, see [my blog post](https://schollz.com/software/croc6) or read a [recent interview I did](https://console.substack.com/p/console-91).
![Example](src/install/customization.gif)
## Install
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
```
On macOS you can install the latest release with [Homebrew](https://brew.sh/):
```
brew install croc
```
On macOS you can also install the latest release with [MacPorts](https://macports.org/):
```
sudo port selfupdate
sudo port install croc
```
On Windows you can install the latest release with [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org), or [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
```
scoop install croc
```
```
choco install croc
```
```
winget install schollz.croc
```
On Unix you can install the latest release with [Nix](https://nixos.org/nix):
```
nix-env -i croc
```
On Alpine Linux you have to install dependencies first:
```
apk add bash coreutils
wget -qO- https://getcroc.schollz.com | bash
```
On Arch Linux you can install the latest release with `pacman`:
```
pacman -S croc
```
On Fedora you can install with `dnf`:
```
dnf install croc
```
On Gentoo you can install with `portage`:
```
emerge net-misc/croc
```
On Termux you can install with `pkg`:
```
pkg install croc
```
On FreeBSD you can install with `pkg`:
```
pkg install croc
```
On Linux, macOS, and Windows you can install from [conda-forge](https://github.com/conda-forge/croc-feedstock/) globally with [`pixi`](https://pixi.sh/):
```
pixi global install croc
```
or into a particular environment with [`conda`](https://docs.conda.io/projects/conda/):
```
conda install --channel conda-forge croc
```
Or, you can [install Go](https://golang.org/dl/) and build from source (requires Go 1.17+):
```
go install github.com/schollz/croc/v10@latest
```
On Android there is a 3rd party F-Droid app [available to download](https://f-droid.org/en/packages/com.github.howeyc.crocgui/).
## Usage
To send a file, simply do:
```
$ croc send [file(s)-or-folder]
Sending 'file-or-folder' (X MB)
Code is: code-phrase
```
Then to receive the file (or folder) on another computer, you can just do
```
croc code-phrase
```
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.
There are a number of configurable options (see `--help`). A set of options (like custom relay, ports, and code phrase) can be set using `--remember`.
### Using `croc` on Linux or Mac OS
On Linux and Mac OS, the sending & receiving is slightly different to avoid [leaking the secret via the process name](https://nvd.nist.gov/vuln/detail/CVE-2023-43621). On these systems you will need to run `croc` with the secret as an environment variable. For example, to receive with the secret `***`:
```
CROC_SECRET=*** croc
```
This will show only `croc` in the process list of a multi-user system and not leak the secret.
For a single-user system the default behavior can be permanently enabled by running
```
croc --classic
```
and confirming.
Run this command again to disable classic mode.
### Custom code phrase
You can send with your own code phrase (must be more than 6 characters).
```
croc send --code [code-phrase] [file(s)-or-folder]
```
### Allow overwriting without prompt
By default, croc will prompt whether to overwrite a file. You can automatically overwrite files by using the `--overwrite` flag (recipient only). For example, receive a file to automatically overwrite:
```
croc --yes --overwrite <code>
```
### Use pipes - stdin and stdout
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` will automatically approve the transfer and pipe it out to `stdout`.
```
croc --yes [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`.
### Send text
Sometimes you want to send URLs or short text. In addition to piping, you can easily send text with `croc`:
```
croc send --text "hello world"
```
This will automatically tell the receiver to use `stdout` when they receive the text so it will be displayed.
### Use a proxy
You can use a proxy as your connection to the relay by adding a proxy address with `--socks5`. For example, you can send via a tor relay:
```
croc --socks5 "127.0.0.1:9050" send SOMEFILE
```
### Change encryption curve
You can choose from several different elliptic curves to use for encryption by using the `--curve` flag. Only the recipient can choose the curve. For example, receive a file using the P-521 curve:
```
croc --curve p521 <codephrase>
```
Available curves are P-256, P-348, P-521 and SIEC. P-256 is the default curve.
### Change hash algorithm
You can choose from several different hash algorithms. The default is the `xxhash` algorithm which is fast and thorough. If you want to optimize for speed you can use the `imohash` algorithm which is even faster, but since it samples files (versus reading the whole file) it can mistakenly determine that a file is the same on the two computers transferring - though this is only a problem if you are syncing files versus sending a new file to a computer.
```
croc send --hash imohash SOMEFILE
```
### Self-host relay
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
```
By default it uses TCP ports 9009-9013. Make sure to open those up. You can customize the ports (e.g. `croc relay --ports 1111,1112`), but you must have a minimum of **2** ports for the relay. The first port is for communication and the subsequent ports are used for the multiplexed data transfer.
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 --relay "myrelay.example.com:9009" send [filename]
```
Note, when sending, you only need to include the first port (the communication port). The subsequent ports for data transfer will be transmitted back to the user from the relay.
#### Self-host relay (docker)
If it's easier you can also run a relay with Docker:
```
docker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' schollz/croc
```
Be sure to include the password for the relay otherwise any requests will be rejected.
```
croc --pass YOURPASSWORD --relay "myreal.example.com:9009" send [filename]
```
Note: when including `--pass YOURPASSWORD` you can instead pass a file with the password, e.g. `--pass FILEWITHPASSWORD`.
## License
MIT
## Acknowledgements
`croc` has gone 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, or ask a question.
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 [@maximbaz](https://github.com/maximbaz), [@meyermarcel](https://github.com/meyermarcel), [@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), [@fbartels](https://github.com/fbartels), [@rkuprov](https://github.com/rkuprov), [@hreese](https://github.com/hreese), [@xenrox](https://github.com/xenrox) and [Ipar](https://github.com/lpar)!
<p align="center">
<img
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.0.3-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://travis-ci.org/schollz/croc"><img
src="https://img.shields.io/travis-ci/schollz/croc.svg?style=flat-square" alt="Build
Status"></a>
<a href="https://saythanks.io/to/schollz"><img src="https://img.shields.io/badge/Say%20Thanks-!-brightgreen.svg?style=flat-square" alt="Say thanks"></a>
</p>
<p align="center">Easily and securely transfer stuff from one computer to another.</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 on any platform *and* has secure peer-to-peer transferring *and* has the capability to resume broken transfers.
## Overview
*croc* uses "code phrases" to securely transfer files. A code phrase is a combination of three random words (mnemonicoded 4 bytes) which the sender shares with the recipient. The code phrase is used by the sender and recipient for password authenticated key exchange ([PAKE](https://github.com/schollz/pake)) to validate parties and generate a secure session key for end-to-end encryption. Since a code phrase can only be used once between two parties, an attacker has a chance of less than 1 in *4 billion* to guess the right code phrase to steal the file. Any attacker with the wrong code phrase will fail the PAKE and the sender will be notified. Only two people with the right code phrase will be able to computers transfer encrypted data through a relay.
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.
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 (sending a file requires opening a terminal still, though).
## 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)
## 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`.
## Usage
The basic usage is to just do
```
$ croc send FILE
```
to send and then on the other computer you can just do
```
$ 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).
### Custom code phrase
You can send with your own code phrase (must be more than 4 characters).
```
$ 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.
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:
```
$ 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.
```
$ 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.
### 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, `ws://198.199.67.130:8153`. You can also run your own relay, it is very easy, just run:
```
$ 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).
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 -relay "ws://myrelay.example.com" send [filename]
```
## License
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)).
Thanks...
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu)!

View File

@ -1,6 +0,0 @@
#!/bin/sh
set -e
if [ -n "$CROC_PASS" ]; then
set -- --pass "$CROC_PASS" "$@"
fi
exec /croc "$@"

View File

@ -1,12 +0,0 @@
[Unit]
Description=croc relay
After=network.target
[Service]
Type=simple
DynamicUser=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ExecStart=/usr/bin/croc relay
[Install]
WantedBy=multi-user.target

56
go.mod
View File

@ -1,39 +1,23 @@
module github.com/schollz/croc/v10
go 1.22
toolchain go1.23.1
module github.com/schollz/croc
require (
github.com/cespare/xxhash v1.1.0
github.com/chzyer/readline v1.5.1
github.com/denisbrodbeck/machineid v1.0.1
github.com/kalafut/imohash v1.1.0
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b
github.com/minio/highwayhash v1.0.3
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/schollz/cli/v2 v2.2.1
github.com/schollz/logger v1.2.0
github.com/schollz/pake/v3 v3.0.5
github.com/schollz/peerdiscovery v1.7.5
github.com/schollz/progressbar/v3 v3.17.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.29.0
golang.org/x/net v0.31.0
golang.org/x/sys v0.27.0
golang.org/x/term v0.26.0
golang.org/x/time v0.8.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d
github.com/fatih/color v1.7.0 // indirect
github.com/gorilla/websocket v1.4.0
github.com/mars9/crypt v0.0.0-20150406101210-65899cf653ff // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/pkg/errors v0.8.0
github.com/schollz/mnemonicode v1.0.1
github.com/schollz/pake v1.0.2
github.com/schollz/peerdiscovery v1.2.2
github.com/schollz/progressbar/v2 v2.5.3
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9
github.com/schollz/utils v1.0.0
github.com/stretchr/testify v1.2.2
github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937
github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 // indirect
)

148
go.sum
View File

@ -1,148 +0,0 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kalafut/imohash v1.1.0 h1:Lldcmx0SXgMSoABB2WBD8mTgf0OlVnISn2Dyrfg2Ep8=
github.com/kalafut/imohash v1.1.0/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU=
github.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s=
github.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho=
github.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM=
github.com/schollz/pake/v3 v3.0.5 h1:MnZVdI987lkjln9BSx/zUb724TZISa2jbO+dPj6BvgQ=
github.com/schollz/pake/v3 v3.0.5/go.mod h1:OGbG6htRwSKo6V8R5tg61ufpFmZM1b/PrrSp6g2ZLLc=
github.com/schollz/peerdiscovery v1.7.5 h1:0cEhO+o8i4fpeKBwl7u0UY3Kt3XVt5fSzS4rg17ZPb4=
github.com/schollz/peerdiscovery v1.7.5/go.mod h1:Crht2FOfD1/eL3U/AIM0vvwVZDPePlBgSX3Xw+TnJoE=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tscholl2/siec v0.0.0-20210707234609-9bdfc483d499/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U=
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

66
goreleaser.yml Normal file
View File

@ -0,0 +1,66 @@
project_name: croc
build:
main: main.go
binary: croc
ldflags: -s -w -X main.Version="v{{.Version}}-{{.Date}}"
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
- freebsd
- netbsd
- openbsd
- dragonfly
goarch:
- amd64
- 386
- arm
- arm64
goarm:
- 7
nfpm:
formats:
- deb
vendor: "schollz.com"
homepage: "https://schollz.com/software/croc/"
maintainer: "Zack Scholl <zack.scholl@gmail.com>"
description: "A simple, secure, and fast way to transfer data."
license: "MIT"
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
archive:
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
files:
- README.md
- LICENSE
release:
draft: true

239
main.go
View File

@ -1,55 +1,210 @@
package main
//go:generate go run src/install/updateversion.go
//go:generate git commit -am "bump $VERSION"
//go:generate git tag -af v$VERSION -m "v$VERSION"
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/signal"
"syscall"
"path/filepath"
"strings"
"time"
"github.com/schollz/croc/v10/src/cli"
"github.com/schollz/croc/v10/src/utils"
humanize "github.com/dustin/go-humanize"
"github.com/schollz/croc/src/croc"
"github.com/schollz/croc/src/utils"
"github.com/urfave/cli"
)
var Version string
var codePhrase string
var cr *croc.Croc
func main() {
// "github.com/pkg/profile"
// go func() {
// for {
// f, err := os.Create("croc.pprof")
// if err != nil {
// panic(err)
// }
// runtime.GC() // get up-to-date statistics
// if err := pprof.WriteHeapProfile(f); err != nil {
// panic(err)
// }
// f.Close()
// time.Sleep(3 * time.Second)
// fmt.Println("wrote profile")
// }
// }()
app := cli.NewApp()
app.Name = "croc"
if Version == "" {
Version = "dev"
}
// Create a channel to receive OS signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := cli.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
app.Version = Version
app.Compiled = time.Now()
app.Usage = "easily and securely transfer stuff from one computer to another"
app.UsageText = "croc allows any two computers to directly and securely transfer files"
// app.ArgsUsage = "[args and such]"
app.Commands = []cli.Command{
{
Name: "send",
Usage: "send a file",
Description: "send a file over the relay",
ArgsUsage: "[filename]",
Flags: []cli.Flag{
cli.BoolFlag{Name: "no-compress, o", Usage: "disable compression"},
cli.BoolFlag{Name: "no-encrypt, e", Usage: "disable encryption"},
cli.StringFlag{Name: "code, c", Usage: "codephrase used to connect to relay"},
},
HelpName: "croc send",
Action: func(c *cli.Context) error {
return send(c)
},
},
{
Name: "relay",
Usage: "start a croc relay",
Description: "the croc relay will handle websocket and TCP connections",
Flags: []cli.Flag{},
HelpName: "croc relay",
Action: func(c *cli.Context) error {
return relay(c)
},
},
}
app.Flags = []cli.Flag{
cli.StringFlag{Name: "addr", Value: "198.199.67.130", 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 (p224, p256, p384, p521, siec)"},
}
app.EnableBashCompletion = true
app.HideHelp = false
app.HideVersion = false
app.BashComplete = func(c *cli.Context) {
fmt.Fprintf(c.App.Writer, "send\nreceive\relay")
}
app.Action = func(c *cli.Context) error {
return receive(c)
}
app.Before = func(c *cli.Context) error {
cr = croc.Init(c.GlobalBool("debug"))
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
}
// Exit the program gracefully
utils.RemoveMarkedFiles()
os.Exit(0)
}()
if c.GlobalBool("force-web") {
cr.ForceSend = 1
}
return nil
}
// Wait for a termination signal
_ = <-sigs
utils.RemoveMarkedFiles()
// Exit the program gracefully
os.Exit(0)
err := app.Run(os.Args)
if err != nil {
fmt.Printf("\nerror: %s", err.Error())
}
}
func send(c *cli.Context) error {
stat, _ := os.Stdin.Stat()
var fname string
if (stat.Mode() & os.ModeCharDevice) == 0 {
f, err := ioutil.TempFile(".", "croc-stdin-")
if err != nil {
return err
}
_, err = io.Copy(f, os.Stdin)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
fname = f.Name()
defer func() {
err = os.Remove(fname)
if err != nil {
log.Println(err)
}
}()
} else {
fname = c.Args().First()
}
if fname == "" {
return errors.New("must specify file: croc send [filename]")
}
cr.UseCompression = !c.Bool("no-compress")
cr.UseEncryption = !c.Bool("no-encrypt")
if c.String("code") != "" {
codePhrase = c.String("code")
}
if len(codePhrase) == 0 {
// generate code phrase
codePhrase = 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)
if err != nil {
return err
}
}
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,
codePhrase,
codePhrase,
)
return cr.Send(fname, codePhrase)
}
func receive(c *cli.Context) error {
if c.GlobalString("code") != "" {
codePhrase = c.GlobalString("code")
}
if c.Args().First() != "" {
codePhrase = c.Args().First()
}
if codePhrase == "" {
codePhrase = utils.GetInput("Enter receive code: ")
}
return cr.Receive(codePhrase)
}
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
})
return size, err
}

View File

@ -1,718 +0,0 @@
package cli
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/chzyer/readline"
"github.com/schollz/cli/v2"
"github.com/schollz/croc/v10/src/comm"
"github.com/schollz/croc/v10/src/croc"
"github.com/schollz/croc/v10/src/mnemonicode"
"github.com/schollz/croc/v10/src/models"
"github.com/schollz/croc/v10/src/tcp"
"github.com/schollz/croc/v10/src/utils"
log "github.com/schollz/logger"
"github.com/schollz/pake/v3"
)
// Version specifies the version
var Version string
// Run will run the command line program
func Run() (err error) {
// use all of the processors
runtime.GOMAXPROCS(runtime.NumCPU())
app := cli.NewApp()
app.Name = "croc"
if Version == "" {
Version = "v10.1.0"
}
app.Version = Version
app.Compiled = time.Now()
app.Usage = "easily and securely transfer stuff from one computer to another"
app.UsageText = `croc [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder]
USAGE EXAMPLES:
Send a file:
croc send file.txt
-git to respect your .gitignore
Send multiple files:
croc send file1.txt file2.txt file3.txt
or
croc send *.jpg
Send everything in a folder:
croc send example-folder-name
Send a file with a custom code:
croc send --code secret-code file.txt
Receive a file using code:
croc secret-code`
app.Commands = []*cli.Command{
{
Name: "send",
Usage: "send file(s), or folder (see options with croc send -h)",
Description: "send file(s), or folder, over the relay",
ArgsUsage: "[filename(s) or folder]",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "zip", Usage: "zip folder before sending"},
&cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay"},
&cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5)"},
&cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"},
&cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"},
&cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"},
&cli.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"},
&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
&cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"},
&cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show receive code as a qrcode"},
},
HelpName: "croc send",
Action: send,
},
{
Name: "relay",
Usage: "start your own relay (optional)",
Description: "start relay",
HelpName: "croc relay",
Action: relay,
Flags: []cli.Flag{
&cli.StringFlag{Name: "host", Usage: "host of the relay"},
&cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"},
&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
&cli.IntFlag{Name: "transfers", Value: 5, Usage: "number of ports to use for relay"},
},
},
}
app.Flags = []cli.Flag{
&cli.BoolFlag{Name: "internal-dns", Usage: "use a built-in DNS stub resolver rather than the host operating system"},
&cli.BoolFlag{Name: "classic", Usage: "toggle between the classic mode (insecure due to local attack vector) and new mode (secure)"},
&cli.BoolFlag{Name: "remember", Usage: "save these settings to reuse next time"},
&cli.BoolFlag{Name: "debug", Usage: "toggle debug mode"},
&cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
&cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
&cli.BoolFlag{Name: "no-compress", Usage: "disable compression"},
&cli.BoolFlag{Name: "ask", Usage: "make sure sender and recipient are prompted"},
&cli.BoolFlag{Name: "local", Usage: "force to use only local connections"},
&cli.BoolFlag{Name: "ignore-stdin", Usage: "ignore piped stdin"},
&cli.BoolFlag{Name: "overwrite", Usage: "do not prompt to overwrite or resume"},
&cli.BoolFlag{Name: "testing", Usage: "flag for testing purposes"},
&cli.StringFlag{Name: "multicast", Value: "239.255.255.250", Usage: "multicast address to use for local discovery"},
&cli.StringFlag{Name: "curve", Value: "p256", Usage: "choose an encryption curve (" + strings.Join(pake.AvailableCurves(), ", ") + ")"},
&cli.StringFlag{Name: "ip", Value: "", Usage: "set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009"},
&cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}},
&cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}},
&cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
&cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}},
&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
&cli.StringFlag{Name: "throttleUpload", Value: "", Usage: "Throttle the upload speed e.g. 500k"},
}
app.EnableBashCompletion = true
app.HideHelp = false
app.HideVersion = false
app.Action = func(c *cli.Context) error {
allStringsAreFiles := func(strs []string) bool {
for _, str := range strs {
if !utils.Exists(str) {
return false
}
}
return true
}
// check if "classic" is set
classicFile := getClassicConfigFile(true)
classicInsecureMode := utils.Exists(classicFile)
if c.Bool("classic") {
if classicInsecureMode {
// classic mode not enabled
fmt.Print(`Classic mode is currently ENABLED.
Disabling this mode will prevent the shared secret from being visible
on the host's process list when passed via the command line. On a
multi-user system, this will help ensure that other local users cannot
access the shared secret and receive the files instead of the intended
recipient.
Do you wish to continue to DISABLE the classic mode? (y/N) `)
choice := strings.ToLower(utils.GetInput(""))
if choice == "y" || choice == "yes" {
os.Remove(classicFile)
fmt.Print("\nClassic mode DISABLED.\n\n")
fmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase:
Send: CROC_SECRET=*** croc send file.txt
Receive: CROC_SECRET=*** croc` + "\n\n")
} else {
fmt.Print("\nClassic mode ENABLED.\n")
}
} else {
// enable classic mode
// touch the file
fmt.Print(`Classic mode is currently DISABLED.
Please note that enabling this mode will make the shared secret visible
on the host's process list when passed via the command line. On a
multi-user system, this could allow other local users to access the
shared secret and receive the files instead of the intended recipient.
Do you wish to continue to enable the classic mode? (y/N) `)
choice := strings.ToLower(utils.GetInput(""))
if choice == "y" || choice == "yes" {
fmt.Print("\nClassic mode ENABLED.\n\n")
os.WriteFile(classicFile, []byte("enabled"), 0o644)
fmt.Print(`To send and receive, use the code phrase:
Send: croc send --code *** file.txt
Receive: croc ***` + "\n\n")
} else {
fmt.Print("\nClassic mode DISABLED.\n")
}
}
os.Exit(0)
}
// if trying to send but forgot send, let the user know
if c.Args().Present() && allStringsAreFiles(c.Args().Slice()) {
fnames := []string{}
for _, fpath := range c.Args().Slice() {
_, basename := filepath.Split(fpath)
fnames = append(fnames, "'"+basename+"'")
}
promptMessage := fmt.Sprintf("Did you mean to send %s? (Y/n) ", strings.Join(fnames, ", "))
choice := strings.ToLower(utils.GetInput(promptMessage))
if choice == "" || choice == "y" || choice == "yes" {
return send(c)
}
}
return receive(c)
}
return app.Run(os.Args)
}
func setDebugLevel(c *cli.Context) {
if c.Bool("debug") {
log.SetLevel("debug")
log.Debug("debug mode on")
// print the public IP address
ip, err := utils.PublicIP()
if err == nil {
log.Debugf("public IP address: %s", ip)
} else {
log.Debug(err)
}
} else {
log.SetLevel("info")
}
}
func getSendConfigFile(requireValidPath bool) string {
configFile, err := utils.GetConfigDir(requireValidPath)
if err != nil {
log.Error(err)
return ""
}
return path.Join(configFile, "send.json")
}
func getClassicConfigFile(requireValidPath bool) string {
configFile, err := utils.GetConfigDir(requireValidPath)
if err != nil {
log.Error(err)
return ""
}
return path.Join(configFile, "classic_enabled")
}
func getReceiveConfigFile(requireValidPath bool) (string, error) {
configFile, err := utils.GetConfigDir(requireValidPath)
if err != nil {
log.Error(err)
return "", err
}
return path.Join(configFile, "receive.json"), nil
}
func determinePass(c *cli.Context) (pass string) {
pass = c.String("pass")
b, err := os.ReadFile(pass)
if err == nil {
pass = strings.TrimSpace(string(b))
}
return
}
func send(c *cli.Context) (err error) {
setDebugLevel(c)
comm.Socks5Proxy = c.String("socks5")
comm.HttpProxy = c.String("connect")
portParam := c.Int("port")
if portParam == 0 {
portParam = 9009
}
transfersParam := c.Int("transfers")
if transfersParam == 0 {
transfersParam = 4
}
ports := make([]string, transfersParam+1)
for i := 0; i <= transfersParam; i++ {
ports[i] = strconv.Itoa(portParam + i)
}
crocOptions := croc.Options{
SharedSecret: c.String("code"),
IsSender: true,
Debug: c.Bool("debug"),
NoPrompt: c.Bool("yes"),
RelayAddress: c.String("relay"),
RelayAddress6: c.String("relay6"),
Stdout: c.Bool("stdout"),
DisableLocal: c.Bool("no-local"),
OnlyLocal: c.Bool("local"),
IgnoreStdin: c.Bool("ignore-stdin"),
RelayPorts: ports,
Ask: c.Bool("ask"),
NoMultiplexing: c.Bool("no-multi"),
RelayPassword: determinePass(c),
SendingText: c.String("text") != "",
NoCompress: c.Bool("no-compress"),
Overwrite: c.Bool("overwrite"),
Curve: c.String("curve"),
HashAlgorithm: c.String("hash"),
ThrottleUpload: c.String("throttleUpload"),
ZipFolder: c.Bool("zip"),
GitIgnore: c.Bool("git"),
ShowQrCode: c.Bool("qrcode"),
MulticastAddress: c.String("multicast"),
}
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
crocOptions.RelayAddress6 = ""
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
crocOptions.RelayAddress = ""
}
b, errOpen := os.ReadFile(getSendConfigFile(false))
if errOpen == nil && !c.Bool("remember") {
var rememberedOptions croc.Options
err = json.Unmarshal(b, &rememberedOptions)
if err != nil {
log.Error(err)
return
}
// update anything that isn't explicitly set
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
crocOptions.RelayAddress = rememberedOptions.RelayAddress
}
if !c.IsSet("no-local") {
crocOptions.DisableLocal = rememberedOptions.DisableLocal
}
if !c.IsSet("ports") && len(rememberedOptions.RelayPorts) > 0 {
crocOptions.RelayPorts = rememberedOptions.RelayPorts
}
if !c.IsSet("code") {
crocOptions.SharedSecret = rememberedOptions.SharedSecret
}
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
crocOptions.RelayPassword = rememberedOptions.RelayPassword
}
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
}
if !c.IsSet("overwrite") {
crocOptions.Overwrite = rememberedOptions.Overwrite
}
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
crocOptions.Curve = rememberedOptions.Curve
}
if !c.IsSet("local") {
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
}
if !c.IsSet("hash") {
crocOptions.HashAlgorithm = rememberedOptions.HashAlgorithm
}
if !c.IsSet("git") {
crocOptions.GitIgnore = rememberedOptions.GitIgnore
}
}
var fnames []string
stat, _ := os.Stdin.Stat()
if ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool("ignore-stdin") {
fnames, err = getStdin()
if err != nil {
return
}
utils.MarkFileForRemoval(fnames[0])
defer func() {
e := os.Remove(fnames[0])
if e != nil {
log.Error(e)
}
}()
} else if c.String("text") != "" {
fnames, err = makeTempFileWithString(c.String("text"))
if err != nil {
return
}
utils.MarkFileForRemoval(fnames[0])
defer func() {
e := os.Remove(fnames[0])
if e != nil {
log.Error(e)
}
}()
} else {
fnames = c.Args().Slice()
}
if len(fnames) == 0 {
return errors.New("must specify file: croc send [filename(s) or folder]")
}
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
if !classicInsecureMode {
// if operating system is UNIX, then use environmental variable to set the code
if (!(runtime.GOOS == "windows") && c.IsSet("code")) || os.Getenv("CROC_SECRET") != "" {
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
if crocOptions.SharedSecret == "" {
fmt.Printf(`On UNIX systems, to send with a custom code phrase,
you need to set the environmental variable CROC_SECRET:
CROC_SECRET=**** croc send file.txt
Or you can have the code phrase automatically generated:
croc send file.txt
Or you can go back to the classic croc behavior by enabling classic mode:
croc --classic
`)
os.Exit(0)
}
}
}
if len(crocOptions.SharedSecret) == 0 {
// generate code phrase
crocOptions.SharedSecret = utils.GetRandomName()
}
minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore)
if err != nil {
return
}
cr, err := croc.New(crocOptions)
if err != nil {
return
}
// save the config
saveConfig(c, crocOptions)
err = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders)
return
}
func getStdin() (fnames []string, err error) {
f, err := os.CreateTemp(".", "croc-stdin-")
if err != nil {
return
}
_, err = io.Copy(f, os.Stdin)
if err != nil {
return
}
err = f.Close()
if err != nil {
return
}
fnames = []string{f.Name()}
return
}
func makeTempFileWithString(s string) (fnames []string, err error) {
f, err := os.CreateTemp(".", "croc-stdin-")
if err != nil {
return
}
_, err = f.WriteString(s)
if err != nil {
return
}
err = f.Close()
if err != nil {
return
}
fnames = []string{f.Name()}
return
}
func saveConfig(c *cli.Context, crocOptions croc.Options) {
if c.Bool("remember") {
configFile := getSendConfigFile(true)
log.Debug("saving config file")
var bConfig []byte
// if the code wasn't set, don't save it
if c.String("code") == "" {
crocOptions.SharedSecret = ""
}
bConfig, err := json.MarshalIndent(crocOptions, "", " ")
if err != nil {
log.Error(err)
return
}
err = os.WriteFile(configFile, bConfig, 0o644)
if err != nil {
log.Error(err)
return
}
log.Debugf("wrote %s", configFile)
}
}
type TabComplete struct{}
func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
var words = strings.SplitAfter(string(line), "-")
var lastPartialWord = words[len(words)-1]
var nbCharacter = len(lastPartialWord)
if nbCharacter == 0 {
// No completion
return [][]rune{[]rune("")}, 0
}
if len(words) == 1 && nbCharacter == utils.NbPinNumbers {
// Check if word is indeed a number
_, err := strconv.Atoi(lastPartialWord)
if err == nil {
return [][]rune{[]rune("-")}, nbCharacter
}
}
var strArray [][]rune
for _, s := range mnemonicode.WordList {
if strings.HasPrefix(s, lastPartialWord) {
var completionCandidate = s[nbCharacter:]
if len(words) <= mnemonicode.WordsRequired(utils.NbBytesWords) {
completionCandidate += "-"
}
strArray = append(strArray, []rune(completionCandidate))
}
}
return strArray, nbCharacter
}
func receive(c *cli.Context) (err error) {
comm.Socks5Proxy = c.String("socks5")
comm.HttpProxy = c.String("connect")
crocOptions := croc.Options{
SharedSecret: c.String("code"),
IsSender: false,
Debug: c.Bool("debug"),
NoPrompt: c.Bool("yes"),
RelayAddress: c.String("relay"),
RelayAddress6: c.String("relay6"),
Stdout: c.Bool("stdout"),
Ask: c.Bool("ask"),
RelayPassword: determinePass(c),
OnlyLocal: c.Bool("local"),
IP: c.String("ip"),
Overwrite: c.Bool("overwrite"),
Curve: c.String("curve"),
TestFlag: c.Bool("testing"),
MulticastAddress: c.String("multicast"),
}
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
crocOptions.RelayAddress6 = ""
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
crocOptions.RelayAddress = ""
}
switch c.Args().Len() {
case 1:
crocOptions.SharedSecret = c.Args().First()
case 3:
fallthrough
case 4:
var phrase []string
phrase = append(phrase, c.Args().First())
phrase = append(phrase, c.Args().Tail()...)
crocOptions.SharedSecret = strings.Join(phrase, "-")
}
// load options here
setDebugLevel(c)
doRemember := c.Bool("remember")
configFile, err := getReceiveConfigFile(doRemember)
if err != nil && doRemember {
return
}
b, errOpen := os.ReadFile(configFile)
if errOpen == nil && !doRemember {
var rememberedOptions croc.Options
err = json.Unmarshal(b, &rememberedOptions)
if err != nil {
log.Error(err)
return
}
// update anything that isn't explicitly Globally set
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
crocOptions.RelayAddress = rememberedOptions.RelayAddress
}
if !c.IsSet("yes") {
crocOptions.NoPrompt = rememberedOptions.NoPrompt
}
if crocOptions.SharedSecret == "" {
crocOptions.SharedSecret = rememberedOptions.SharedSecret
}
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
crocOptions.RelayPassword = rememberedOptions.RelayPassword
}
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
}
if !c.IsSet("overwrite") {
crocOptions.Overwrite = rememberedOptions.Overwrite
}
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
crocOptions.Curve = rememberedOptions.Curve
}
if !c.IsSet("local") {
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
}
}
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
if crocOptions.SharedSecret == "" && os.Getenv("CROC_SECRET") != "" {
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
} else if !(runtime.GOOS == "windows") && crocOptions.SharedSecret != "" && !classicInsecureMode {
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
if crocOptions.SharedSecret == "" {
fmt.Printf(`On UNIX systems, to receive with croc you either need
to set a code phrase using your environmental variables:
CROC_SECRET=**** croc
Or you can specify the code phrase when you run croc without
declaring the secret on the command line:
croc
Enter receive code: ****
Or you can go back to the classic croc behavior by enabling classic mode:
croc --classic
`)
os.Exit(0)
}
}
if crocOptions.SharedSecret == "" {
l, err := readline.NewEx(&readline.Config{
Prompt: "Enter receive code: ",
AutoComplete: TabComplete{},
})
if err != nil {
return err
}
crocOptions.SharedSecret, err = l.Readline()
if err != nil {
return err
}
}
if c.String("out") != "" {
if err = os.Chdir(c.String("out")); err != nil {
return err
}
}
cr, err := croc.New(crocOptions)
if err != nil {
return
}
// save the config
if doRemember {
log.Debug("saving config file")
var bConfig []byte
bConfig, err = json.MarshalIndent(crocOptions, "", " ")
if err != nil {
log.Error(err)
return
}
err = os.WriteFile(configFile, bConfig, 0o644)
if err != nil {
log.Error(err)
return
}
log.Debugf("wrote %s", configFile)
}
err = cr.Receive()
return
}
func relay(c *cli.Context) (err error) {
log.Infof("starting croc relay version %v", Version)
debugString := "info"
if c.Bool("debug") {
debugString = "debug"
}
host := c.String("host")
var ports []string
if c.IsSet("ports") {
ports = strings.Split(c.String("ports"), ",")
} else {
portString := c.Int("port")
if portString == 0 {
portString = 9009
}
transfersString := c.Int("transfers")
if transfersString == 0 {
transfersString = 4
}
ports = make([]string, transfersString)
for i := range ports {
ports[i] = strconv.Itoa(portString + i)
}
}
tcpPorts := strings.Join(ports[1:], ",")
for i, port := range ports {
if i == 0 {
continue
}
go func(portStr string) {
err := tcp.Run(debugString, host, portStr, determinePass(c))
if err != nil {
panic(err)
}
}(port)
}
return tcp.Run(debugString, host, ports[0], determinePass(c), tcpPorts)
}

View File

@ -2,199 +2,119 @@ package comm
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/magisterquis/connectproxy"
"github.com/schollz/croc/v10/src/utils"
log "github.com/schollz/logger"
"golang.org/x/net/proxy"
"github.com/pkg/errors"
)
var Socks5Proxy = ""
var HttpProxy = ""
var MAGIC_BYTES = []byte("croc")
// Comm is some basic TCP communication
type Comm struct {
connection net.Conn
}
// NewConnection gets a new comm to a tcp address
func NewConnection(address string, timelimit ...time.Duration) (c *Comm, err error) {
tlimit := 30 * time.Second
if len(timelimit) > 0 {
tlimit = timelimit[0]
}
var connection net.Conn
if Socks5Proxy != "" && !utils.IsLocalIP(address) {
var dialer proxy.Dialer
// prepend schema if no schema is given
if !strings.Contains(Socks5Proxy, `://`) {
Socks5Proxy = `socks5://` + Socks5Proxy
}
socks5ProxyURL, urlParseError := url.Parse(Socks5Proxy)
if urlParseError != nil {
err = fmt.Errorf("unable to parse socks proxy url: %s", urlParseError)
log.Debug(err)
return
}
dialer, err = proxy.FromURL(socks5ProxyURL, proxy.Direct)
if err != nil {
err = fmt.Errorf("proxy failed: %w", err)
log.Debug(err)
return
}
log.Debug("dialing with dialer.Dial")
connection, err = dialer.Dial("tcp", address)
} else if HttpProxy != "" && !utils.IsLocalIP(address) {
var dialer proxy.Dialer
// prepend schema if no schema is given
if !strings.Contains(HttpProxy, `://`) {
HttpProxy = `http://` + HttpProxy
}
HttpProxyURL, urlParseError := url.Parse(HttpProxy)
if urlParseError != nil {
err = fmt.Errorf("unable to parse http proxy url: %s", urlParseError)
log.Debug(err)
return
}
dialer, err = connectproxy.New(HttpProxyURL, proxy.Direct)
if err != nil {
err = fmt.Errorf("proxy failed: %w", err)
log.Debug(err)
return
}
log.Debug("dialing with dialer.Dial")
connection, err = dialer.Dial("tcp", address)
} else {
log.Debugf("dialing to %s with timelimit %s", address, tlimit)
connection, err = net.DialTimeout("tcp", address, tlimit)
}
if err != nil {
err = fmt.Errorf("comm.NewConnection failed: %w", err)
log.Debug(err)
return
}
c = New(connection)
log.Debugf("connected to '%s'", address)
return
}
// New returns a new comm
func New(c net.Conn) *Comm {
if err := c.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
log.Warnf("error setting read deadline: %v", err)
}
if err := c.SetDeadline(time.Now().Add(3 * time.Hour)); err != nil {
log.Warnf("error setting overall deadline: %v", err)
}
if err := c.SetWriteDeadline(time.Now().Add(3 * time.Hour)); err != nil {
log.Errorf("error setting write deadline: %v", err)
}
comm := new(Comm)
comm.connection = c
return 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}
}
// 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() {
if err := c.connection.Close(); err != nil {
log.Warnf("error closing connection: %v", err)
}
func (c Comm) Close() {
c.connection.Close()
}
func (c *Comm) Write(b []byte) (n int, err 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...)
tmpCopy = append(MAGIC_BYTES, tmpCopy...)
n, err = c.connection.Write(tmpCopy)
if err != nil {
err = fmt.Errorf("connection.Write failed: %w", err)
return
}
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)
n, err := c.connection.Write(tmpCopy)
if n != len(tmpCopy) {
err = fmt.Errorf("wanted to write %d but wrote %d", len(b), n)
return
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("wanted to write %d but wrote %d", len(b), n))
} else {
err = fmt.Errorf("wanted to write %d but wrote %d", len(b), n)
}
}
return
// log.Printf("wanted to write %d but wrote %d", n, len(b))
return n, err
}
func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
// long read deadline in case waiting for file
if err = c.connection.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
log.Warnf("error setting read deadline: %v", err)
}
// must clear the timeout setting
defer c.connection.SetDeadline(time.Time{})
// read until we get 4 bytes for the magic
header := make([]byte, 4)
_, err = io.ReadFull(c.connection, header)
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 {
log.Debugf("initial read error: %v", err)
return
}
if !bytes.Equal(header, MAGIC_BYTES) {
err = fmt.Errorf("initial bytes are not magic: %x", header)
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)
for {
// see if we have enough bytes
bs = bytes.Trim(bs, "\x00")
if len(bs) == 5 {
break
}
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...)
}
// read until we get 4 bytes for the header
header = make([]byte, 4)
_, err = io.ReadFull(c.connection, header)
numBytes, err = strconv.Atoi(strings.TrimLeft(string(bs), "0"))
if err != nil {
log.Debugf("initial read error: %v", err)
return
return nil, 0, nil, err
}
var numBytesUint32 uint32
rbuf := bytes.NewReader(header)
err = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32)
if err != nil {
err = fmt.Errorf("binary.Read failed: %w", err)
log.Debug(err.Error())
return
}
numBytes = int(numBytesUint32)
// shorten the reading deadline in case getting weird data
if err = c.connection.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
log.Warnf("error setting read deadline: %v", err)
}
buf = make([]byte, numBytes)
_, err = io.ReadFull(c.connection, buf)
if err != nil {
log.Debugf("consecutive read error: %v", err)
return
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 {
break
}
}
// log.Printf("wanted %d and got %d", numBytes, len(buf))
return
}
// Send a message
func (c *Comm) Send(message []byte) (err error) {
_, err = c.Write(message)
func (c Comm) Send(message string) (err error) {
_, err = c.Write([]byte(message))
return
}
// Receive a message
func (c *Comm) Receive() (b []byte, err error) {
b, _, _, err = c.Read()
func (c Comm) Receive() (s string, err error) {
b, _, _, err := c.Read()
s = string(b)
return
}

View File

@ -1,66 +0,0 @@
package comm
import (
"crypto/rand"
"net"
"testing"
"time"
log "github.com/schollz/logger"
"github.com/stretchr/testify/assert"
)
func TestComm(t *testing.T) {
token := make([]byte, 3000)
if _, err := rand.Read(token); err != nil {
t.Error(err)
}
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(_ 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("127.0.0.1:"+port, 10*time.Minute)
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))
_ = a.Connection()
a.Close()
assert.NotNil(t, a.Send(token))
_, err = a.Write(token)
assert.NotNil(t, err)
}

View File

@ -4,21 +4,12 @@ import (
"bytes"
"compress/flate"
"io"
log "github.com/schollz/logger"
)
// CompressWithOption returns compressed data using the specified level
func CompressWithOption(src []byte, level int) []byte {
compressedData := new(bytes.Buffer)
compress(src, compressedData, level)
return compressedData.Bytes()
}
// Compress returns a compressed byte slice.
func Compress(src []byte) []byte {
compressedData := new(bytes.Buffer)
compress(src, compressedData, -2)
compress(src, compressedData, 9)
return compressedData.Bytes()
}
@ -32,22 +23,14 @@ func Decompress(src []byte) []byte {
// compress uses flate to compress a byte slice to a corresponding level
func compress(src []byte, dest io.Writer, level int) {
compressor, err := flate.NewWriter(dest, level)
if err != nil {
log.Debugf("error level data: %v", err)
return
}
if _, err := compressor.Write(src); err != nil {
log.Debugf("error writing data: %v", err)
}
compressor, _ := flate.NewWriter(dest, level)
compressor.Write(src)
compressor.Close()
}
// compress uses flate to decompress an io.Reader
func decompress(src io.Reader, dest io.Writer) {
decompressor := flate.NewReader(src)
if _, err := io.Copy(dest, decompressor); err != nil {
log.Debugf("error copying data: %v", err)
}
io.Copy(dest, decompressor)
decompressor.Close()
}

View File

@ -1,105 +0,0 @@
package compress
import (
"crypto/rand"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
var fable = []byte(`The Frog and the Crocodile
Once, there was a frog who lived in the middle of a swamp. His entire family had lived in that swamp for generations, but this particular frog decided that he had had quite enough wetness to last him a lifetime. He decided that he was going to find a dry place to live instead.
The only thing that separated him from dry land was a swampy, muddy, swiftly flowing river. But the river was home to all sorts of slippery, slittering snakes that loved nothing better than a good, plump frog for dinner, so Frog didn't dare try to swim across.
So for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.
The snakes hissed and jeered at him, daring him to come closer, but he refused. Occasionally they would slither closer, jaws open to attack, but the frog always leaped out of the way. But no matter how far upstream he searched or how far downstream, the frog wasn't able to find a way across the water.
He had felt certain that there would be a bridge, or a place where the banks came together, yet all he found was more reeds and water. After a while, even the snakes stopped teasing him and went off in search of easier prey.
The frog sighed in frustration and sat to sulk in the rushes. Suddenly, he spotted two big eyes staring at him from the water. The giant log-shaped animal opened its mouth and asked him, "What are you doing, Frog? Surely there are enough flies right there for a meal."
The frog croaked in surprise and leaped away from the crocodile. That creature could swallow him whole in a moment without thinking about it! Once he was a satisfied that he was a safe distance away, he answered. "I'm tired of living in swampy waters, and I want to travel to the other side of the river. But if I swim across, the snakes will eat me."
The crocodile harrumphed in agreement and sat, thinking, for a while. "Well, if you're afraid of the snakes, I could give you a ride across," he suggested.
"Oh no, I don't think so," Frog answered quickly. "You'd eat me on the way over, or go underwater so the snakes could get me!"
"Now why would I let the snakes get you? I think they're a terrible nuisance with all their hissing and slithering! The river would be much better off without them altogether! Anyway, if you're so worried that I might eat you, you can ride on my tail."
The frog considered his offer. He did want to get to dry ground very badly, and there didn't seem to be any other way across the river. He looked at the crocodile from his short, squat buggy eyes and wondered about the crocodile's motives. But if he rode on the tail, the croc couldn't eat him anyway. And he was right about the snakes--no self-respecting crocodile would give a meal to the snakes.
"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail."
The crocodile flopped his tail into the marshy mud and let the frog climb on, then he waddled out to the river. But he couldn't stick his tail into the water as a rudder because the frog was on it -- and if he put his tail in the water, the snakes would eat the frog. They clumsily floated downstream for a ways, until the crocodile said, "Hop onto my back so I can steer straight with my tail." The frog moved, and the journey smoothed out.
From where he was sitting, the frog couldn't see much except the back of Crocodile's head. "Why don't you hop up on my head so you can see everything around us?" Crocodile invited. `)
func BenchmarkCompressLevelMinusTwo(b *testing.B) {
for i := 0; i < b.N; i++ {
CompressWithOption(fable, -2)
}
}
func BenchmarkCompressLevelNine(b *testing.B) {
for i := 0; i < b.N; i++ {
CompressWithOption(fable, 9)
}
}
func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
data := make([]byte, 1000000)
if _, err := rand.Read(data); err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
CompressWithOption(data, -2)
}
}
func BenchmarkCompressLevelNineBinary(b *testing.B) {
data := make([]byte, 1000000)
if _, err := rand.Read(data); err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
CompressWithOption(data, 9)
}
}
func TestCompress(t *testing.T) {
compressedB := CompressWithOption(fable, 9)
dataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
fmt.Printf("Level 9: %2.0f%% percent space savings\n", dataRateSavings)
assert.True(t, len(compressedB) < len(fable))
assert.Equal(t, fable, Decompress(compressedB))
compressedB = CompressWithOption(fable, -2)
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))
compressedB = Compress(fable)
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)
if _, err := rand.Read(data); err != nil {
t.Fatal(err)
}
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)
if _, err := rand.Read(data); err != nil {
t.Fatal(err)
}
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)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,400 +1,81 @@
package croc
import (
"crypto/rand"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/schollz/croc/v10/src/tcp"
log "github.com/schollz/logger"
"github.com/schollz/croc/src/utils"
"github.com/stretchr/testify/assert"
)
func init() {
log.SetLevel("trace")
go tcp.Run("debug", "127.0.0.1", "8281", "pass123", "8282,8283,8284,8285")
go tcp.Run("debug", "127.0.0.1", "8282", "pass123")
go tcp.Run("debug", "127.0.0.1", "8283", "pass123")
go tcp.Run("debug", "127.0.0.1", "8284", "pass123")
go tcp.Run("debug", "127.0.0.1", "8285", "pass123")
time.Sleep(1 * time.Second)
}
func TestCrocReadme(t *testing.T) {
defer os.Remove("README.md")
log.Debug("setting up sender")
sender, err := New(Options{
IsSender: true,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPorts: []string{"8281"},
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
panic(err)
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
}
log.Debug("setting up receiver")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
panic(err)
}
fname := generateRandomFile(megabytes)
var wg sync.WaitGroup
wg.Add(2)
go func() {
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../README.md"}, false, false)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
if err != nil {
t.Errorf("send failed: %v", err)
}
wg.Done()
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))
}()
time.Sleep(100 * time.Millisecond)
go func() {
err := receiver.Receive()
if err != nil {
t.Errorf("receive failed: %v", err)
}
wg.Done()
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))
}()
wg.Wait()
os.Chdir("..")
os.RemoveAll("test")
os.Remove(fname)
fmt.Printf("\n-----\n%2.1f MB/s\n----\n", durationPerMegabyte)
}
func TestCrocEmptyFolder(t *testing.T) {
pathName := "../../testEmpty"
defer os.RemoveAll(pathName)
defer os.RemoveAll("./testEmpty")
os.MkdirAll(pathName, 0o755)
log.Debug("setting up sender")
sender, err := New(Options{
IsSender: true,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPorts: []string{"8281"},
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
panic(err)
}
log.Debug("setting up receiver")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
if err != nil {
t.Errorf("send failed: %v", err)
}
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
go func() {
err := receiver.Receive()
if err != nil {
t.Errorf("receive failed: %v", err)
}
wg.Done()
}()
wg.Wait()
func TestSendReceivePubWebsockets(t *testing.T) {
sendAndReceive(t, 1, false)
}
func TestCrocSymlink(t *testing.T) {
pathName := "../link-in-folder"
defer os.RemoveAll(pathName)
defer os.RemoveAll("./link-in-folder")
os.MkdirAll(pathName, 0o755)
os.Symlink("../../README.md", filepath.Join(pathName, "README.link"))
log.Debug("setting up sender")
sender, err := New(Options{
IsSender: true,
SharedSecret: "8124-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPorts: []string{"8281"},
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
panic(err)
}
log.Debug("setting up receiver")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "8124-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
if err != nil {
t.Errorf("send failed: %v", err)
}
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
go func() {
err = receiver.Receive()
if err != nil {
t.Errorf("receive failed: %v", err)
}
wg.Done()
}()
wg.Wait()
s, err := filepath.EvalSymlinks(path.Join(pathName, "README.link"))
if s != "../../README.md" && s != "..\\..\\README.md" {
log.Debug(s)
t.Errorf("symlink failed to transfer in folder")
}
if err != nil {
t.Errorf("symlink transfer failed: %s", err.Error())
}
}
func TestCrocIgnoreGit(t *testing.T) {
log.SetLevel("trace")
defer os.Remove(".gitignore")
time.Sleep(300 * time.Millisecond)
time.Sleep(1 * time.Second)
file, err := os.Create(".gitignore")
if err != nil {
log.Errorf("error creating file")
}
_, err = file.WriteString("LICENSE")
if err != nil {
log.Errorf("error writing to file")
}
time.Sleep(1 * time.Second)
// due to how files are ignored in this function, all we have to do to test is make sure LICENSE doesn't get included in FilesInfo.
filesInfo, _, _, errGet := GetFilesInfo([]string{"../../LICENSE", ".gitignore", "croc.go"}, false, true)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
for _, file := range filesInfo {
if strings.Contains(file.Name, "LICENSE") {
t.Errorf("test failed, should ignore LICENSE")
}
}
func TestSendReceivePubTCP(t *testing.T) {
sendAndReceive(t, 2, false)
}
func TestCrocLocal(t *testing.T) {
log.SetLevel("trace")
defer os.Remove("LICENSE")
defer os.Remove("touched")
time.Sleep(300 * time.Millisecond)
log.Debug("setting up sender")
sender, err := New(Options{
IsSender: true,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8181",
RelayPorts: []string{"8181", "8182"},
RelayPassword: "pass123",
Stdout: true,
NoPrompt: true,
DisableLocal: false,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
panic(err)
}
time.Sleep(1 * time.Second)
log.Debug("setting up receiver")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "8123-testingthecroc",
Debug: true,
RelayAddress: "127.0.0.1:8181",
RelayPassword: "pass123",
Stdout: true,
NoPrompt: true,
DisableLocal: false,
Curve: "siec",
Overwrite: true,
})
if err != nil {
panic(err)
}
var wg sync.WaitGroup
os.Create("touched")
wg.Add(2)
go func() {
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../LICENSE", "touched"}, false, false)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
if err != nil {
t.Errorf("send failed: %v", err)
}
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
go func() {
err := receiver.Receive()
if err != nil {
t.Errorf("send failed: %v", err)
}
wg.Done()
}()
wg.Wait()
func TestSendReceiveLocalWebsockets(t *testing.T) {
sendAndReceive(t, 1, true)
}
func TestCrocError(t *testing.T) {
content := []byte("temporary file's content")
tmpfile, err := os.CreateTemp("", "example")
if err != nil {
panic(err)
}
// func TestSendReceiveLocalTCP(t *testing.T) {
// sendAndReceive(t, 2, true)
// }
defer os.Remove(tmpfile.Name()) // clean up
if _, err = tmpfile.Write(content); err != nil {
panic(err)
}
if err = tmpfile.Close(); err != nil {
panic(err)
}
Debug(false)
log.SetLevel("warn")
sender, _ := New(Options{
IsSender: true,
SharedSecret: "8123-testingthecroc2",
Debug: true,
RelayAddress: "doesntexistok.com:8381",
RelayPorts: []string{"8381", "8382"},
RelayPassword: "pass123",
Stdout: true,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tmpfile.Name()}, false, false)
if errGet != nil {
t.Errorf("failed to get minimal info: %v", errGet)
}
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
log.Debug(err)
assert.NotNil(t, err)
}
func TestCleanUp(t *testing.T) {
// windows allows files to be deleted only if they
// are not open by another program so the remove actions
// from the above tests will not always do a good clean up
// This "test" will make sure
operatingSystem := runtime.GOOS
log.Debugf("The operating system is %s", operatingSystem)
if operatingSystem == "windows" {
time.Sleep(1 * time.Second)
log.Debug("Full cleanup")
var err error
for _, file := range []string{"README.md", "./README.md"} {
err = os.Remove(file)
if err == nil {
log.Debugf("Successfully purged %s", file)
} else {
log.Debugf("%s was already purged.", file)
}
}
for _, folder := range []string{"./testEmpty", "./link-in-folder"} {
err = os.RemoveAll(folder)
if err == nil {
log.Debugf("Successfully purged %s", folder)
} else {
log.Debugf("%s was already purged.", folder)
}
}
}
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
}

203
src/croc/sending.go Normal file
View File

@ -0,0 +1,203 @@
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/recipient"
"github.com/schollz/croc/src/relay"
"github.com/schollz/croc/src/sender"
"github.com/schollz/peerdiscovery"
"github.com/schollz/utils"
)
// Send the file
func (c *Croc) Send(fname, codephrase string) (err error) {
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 {
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, ",")),
})
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) {
log.Debug("receiving")
// use local relay first
if !c.NoLocal {
log.Debug("trying discovering")
// 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,
})
log.Debug("finished")
log.Debug(discovered)
if errDiscover != nil {
log.Debug(errDiscover)
}
if len(discovered) > 0 {
if discovered[0].Address == utils.GetLocalIP() {
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 struct{})
// 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 {
return
}
defer sock.Close()
// tell the websockets we are connected
err = sock.WriteMessage(websocket.BinaryMessage, []byte("connected"))
if err != nil {
return err
}
if isSender {
go sender.Send(c.ForceSend, address, tcpPorts, isLocal, done, sock, fname, codephrase, c.UseCompression, c.UseEncryption)
} else {
go recipient.Receive(c.ForceSend, address, tcpPorts, isLocal, done, sock, codephrase, c.NoRecipientPrompt, c.Stdout)
}
for {
select {
case <-done:
return nil
case <-interrupt:
if !c.Debug {
SetDebugLevel("critical")
}
log.Debug("interrupt")
err = sock.WriteMessage(websocket.TextMessage, []byte("interrupt"))
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,121 +5,85 @@ import (
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"encoding/base64"
"errors"
"strings"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/pbkdf2"
)
// New generates a new key based on a passphrase and salt
func New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, err error) {
if len(passphrase) < 1 {
err = fmt.Errorf("need more than that for passphrase")
// Encryption stores the data
type Encryption struct {
Encrypted []byte `json:"e"`
Salt []byte `json:"s"`
IV []byte `json:"i"`
}
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")
return
}
if usersalt == nil {
salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
// Salt.
if _, err := rand.Read(salt); err != nil {
log.Fatalf("can't get random salt: %v", err)
}
} else {
salt = usersalt
enc.Encrypted, err = base64.StdEncoding.DecodeString(items[0])
if err != nil {
return
}
key = pbkdf2.Key(passphrase, salt, 100, 32, sha256.New)
enc.Salt, err = base64.StdEncoding.DecodeString(items[1])
if err != nil {
return
}
enc.IV, err = base64.StdEncoding.DecodeString(items[2])
return
}
// Encrypt will encrypt using the pre-generated key
func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {
// generate a random iv each time
// 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
ivBytes := make([]byte, 12)
if _, err = rand.Read(ivBytes); err != nil {
log.Fatalf("can't initialize crypto: %v", err)
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,
}
b, err := aes.NewCipher(key)
if err != nil {
return
}
// 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
}
aesgcm, err := cipher.NewGCM(b)
if err != nil {
return
}
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
encrypted = append(ivBytes, encrypted...)
key, _ := deriveKey(passphrase, e.Salt)
b, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(b)
plaintext, err = aesgcm.Open(nil, e.IV, e.Encrypted, nil)
return
}
// Decrypt using the pre-generated key
func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {
if len(encrypted) < 13 {
err = fmt.Errorf("incorrect passphrase")
return
}
b, err := aes.NewCipher(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
}
// NewArgon2 generates a new key based on a passphrase and salt
// using argon2
// https://pkg.go.dev/golang.org/x/crypto/argon2
func NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, salt []byte, err error) {
if len(passphrase) < 1 {
err = fmt.Errorf("need more than that for passphrase")
return
}
if usersalt == nil {
func deriveKey(passphrase []byte, salt []byte) ([]byte, []byte) {
if salt == nil {
salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
// Salt.
if _, err = rand.Read(salt); err != nil {
log.Fatalf("can't get random salt: %v", err)
}
} else {
salt = usersalt
rand.Read(salt)
}
aead, err = chacha20poly1305.NewX(argon2.IDKey(passphrase, salt, 1, 64*1024, 4, 32))
return
}
// EncryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
func EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte, err error) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
panic(err)
}
// Encrypt the message and append the ciphertext to the nonce.
encrypted = aead.Seal(nonce, nonce, plaintext, nil)
return
}
// DecryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (encrypted []byte, err error) {
if len(encryptedMsg) < aead.NonceSize() {
err = fmt.Errorf("ciphertext too short")
return
}
// Split nonce and ciphertext.
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
encrypted, err = aead.Open(nil, nonce, ciphertext, nil)
return
return pbkdf2.Key([]byte(passphrase), salt, 100, 32, sha256.New), salt
}

View File

@ -1,119 +1,41 @@
package crypt
import (
"fmt"
"testing"
import "testing"
"github.com/stretchr/testify/assert"
)
func BenchmarkEncrypt(b *testing.B) {
bob, _, _ := New([]byte("password"), nil)
func BenchmarkEncryption(b *testing.B) {
for i := 0; i < b.N; i++ {
Encrypt([]byte("hello, world"), bob)
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`))
}
}
func BenchmarkDecrypt(b *testing.B) {
key, _, _ := New([]byte("password"), nil)
msg := []byte("hello, world")
enc, _ := Encrypt(msg, key)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Decrypt(enc, key)
}
}
func BenchmarkNewPbkdf2(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
New([]byte("password"), nil)
}
}
func BenchmarkNewArgon2(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
NewArgon2([]byte("password"), nil)
}
}
func BenchmarkEncryptChaCha(b *testing.B) {
bob, _, _ := NewArgon2([]byte("password"), nil)
for i := 0; i < b.N; i++ {
EncryptChaCha([]byte("hello, world"), bob)
}
}
func BenchmarkDecryptChaCha(b *testing.B) {
key, _, _ := NewArgon2([]byte("password"), nil)
msg := []byte("hello, world")
enc, _ := EncryptChaCha(msg, key)
b.ResetTimer()
for i := 0; i < b.N; i++ {
DecryptChaCha(enc, key)
}
}
func TestEncryption(t *testing.T) {
key, salt, err := New([]byte("password"), nil)
assert.Nil(t, err)
msg := []byte("hello, world")
enc, err := Encrypt(msg, key)
assert.Nil(t, err)
dec, err := Decrypt(enc, key)
assert.Nil(t, err)
assert.Equal(t, msg, dec)
// check reusing the salt
key2, _, _ := New([]byte("password"), salt)
dec, err = Decrypt(enc, key2)
assert.Nil(t, err)
assert.Equal(t, msg, dec)
// check reusing the salt
key2, _, _ = New([]byte("wrong password"), salt)
dec, err = Decrypt(enc, key2)
assert.NotNil(t, err)
assert.NotEqual(t, msg, dec)
// error with no password
_, err = Decrypt([]byte(""), key)
assert.NotNil(t, err)
// error with small password
_, _, err = New([]byte(""), nil)
assert.NotNil(t, err)
}
func TestEncryptionChaCha(t *testing.T) {
key, salt, err := NewArgon2([]byte("password"), nil)
fmt.Printf("key: %x\n", key)
assert.Nil(t, err)
msg := []byte("hello, world")
enc, err := EncryptChaCha(msg, key)
assert.Nil(t, err)
dec, err := DecryptChaCha(enc, key)
assert.Nil(t, err)
assert.Equal(t, msg, dec)
// check reusing the salt
key2, _, _ := NewArgon2([]byte("password"), salt)
dec, err = DecryptChaCha(enc, key2)
assert.Nil(t, err)
assert.Equal(t, msg, dec)
// check reusing the salt
key2, _, _ = NewArgon2([]byte("wrong password"), salt)
dec, err = DecryptChaCha(enc, key2)
assert.NotNil(t, err)
assert.NotEqual(t, msg, dec)
// error with no password
_, err = DecryptChaCha([]byte(""), key)
assert.NotNil(t, err)
// error with small password
_, _, err = NewArgon2([]byte(""), nil)
assert.NotNil(t, err)
}

View File

@ -1,49 +0,0 @@
//go:build !windows
// +build !windows
package diskusage
import (
"golang.org/x/sys/unix"
)
// DiskUsage contains usage data and provides user-friendly access methods
type DiskUsage struct {
stat *unix.Statfs_t
}
// NewDiskUsage returns an object holding the disk usage of volumePath
// or nil in case of error (invalid path, etc)
func NewDiskUsage(volumePath string) *DiskUsage {
stat := unix.Statfs_t{}
err := unix.Statfs(volumePath, &stat)
if err != nil {
return nil
}
return &DiskUsage{&stat}
}
// Free returns total free bytes on file system
func (du *DiskUsage) Free() uint64 {
return uint64(du.stat.Bfree) * uint64(du.stat.Bsize)
}
// Available return total available bytes on file system to an unprivileged user
func (du *DiskUsage) Available() uint64 {
return uint64(du.stat.Bavail) * uint64(du.stat.Bsize)
}
// Size returns total size of the file system
func (du *DiskUsage) Size() uint64 {
return uint64(du.stat.Blocks) * uint64(du.stat.Bsize)
}
// Used returns total bytes used in file system
func (du *DiskUsage) Used() uint64 {
return du.Size() - du.Free()
}
// Usage returns percentage of use on the file system
func (du *DiskUsage) Usage() float32 {
return float32(du.Used()) / float32(du.Size())
}

View File

@ -1,17 +0,0 @@
package diskusage
import (
"fmt"
"testing"
)
var KB = uint64(1024)
func TestNewDiskUsage(t *testing.T) {
usage := NewDiskUsage(".")
fmt.Println("Free:", usage.Free()/(KB*KB))
fmt.Println("Available:", usage.Available()/(KB*KB))
fmt.Println("Size:", usage.Size()/(KB*KB))
fmt.Println("Used:", usage.Used()/(KB*KB))
fmt.Println("Usage:", usage.Usage()*100, "%")
}

View File

@ -1,55 +0,0 @@
package diskusage
import (
"unsafe"
"golang.org/x/sys/windows"
)
type DiskUsage struct {
freeBytes int64
totalBytes int64
availBytes int64
}
// NewDiskUsage returns an object holding the disk usage of volumePath
// or nil in case of error (invalid path, etc)
func NewDiskUsage(volumePath string) *DiskUsage {
h := windows.MustLoadDLL("kernel32.dll")
c := h.MustFindProc("GetDiskFreeSpaceExW")
du := &DiskUsage{}
c.Call(
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(volumePath))),
uintptr(unsafe.Pointer(&du.freeBytes)),
uintptr(unsafe.Pointer(&du.totalBytes)),
uintptr(unsafe.Pointer(&du.availBytes)))
return du
}
// Free returns total free bytes on file system
func (du *DiskUsage) Free() uint64 {
return uint64(du.freeBytes)
}
// Available returns total available bytes on file system to an unprivileged user
func (du *DiskUsage) Available() uint64 {
return uint64(du.availBytes)
}
// Size returns total size of the file system
func (du *DiskUsage) Size() uint64 {
return uint64(du.totalBytes)
}
// Used returns total bytes used in file system
func (du *DiskUsage) Used() uint64 {
return du.Size() - du.Free()
}
// Usage returns percentage of use on the file system
func (du *DiskUsage) Usage() float32 {
return float32(du.Used()) / float32(du.Size())
}

View File

@ -1,19 +0,0 @@
# VERSION=8.X.Y make release
release:
cd ../../ && go run src/install/updateversion.go
git commit -am "bump ${VERSION}"
git tag -af v${VERSION} -m "v${VERSION}"
git push
git push --tags
cp zsh_autocomplete ../../
cp bash_autocomplete ../../
cd ../../ && goreleaser release
cd ../../ && ./src/install/prepare-sources-tarball.sh
cd ../../ && ./src/install/upload-src-tarball.sh
test:
cp zsh_autocomplete ../../
cp bash_autocomplete ../../
cd ../../ && go generate
cd ../../ && goreleaser release --skip-publish

View File

@ -1,19 +0,0 @@
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
unset PROG

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

View File

@ -1,767 +0,0 @@
#!/bin/bash -
#===============================================================================
#
# FILE: default.txt
#
# USAGE: curl https://getcroc.schollz.com | bash
# OR
# wget -qO- https://getcroc.schollz.com | bash
#
# DESCRIPTION: croc Installer Script.
#
# This script installs croc into a specified prefix.
# Default prefix = /usr/local/bin
#
# OPTIONS: -p, --prefix "${INSTALL_PREFIX}"
# Prefix to install croc into. Defaults to /usr/local/bin
# REQUIREMENTS: bash, uname, tar/unzip, curl/wget, sudo (if not run
# as root), install, mktemp, sha256sum/shasum/sha256
#
# BUGS: ...hopefully not. Please report.
#
# NOTES: Homepage: https://schollz.com/software/croc
# Issues: https://github.com/schollz/croc/issues
#
# CREATED: 08/10/2019 16:41
# REVISION: 0.9.2
#===============================================================================
set -o nounset # Treat unset variables as an error
#-------------------------------------------------------------------------------
# DEFAULTS
#-------------------------------------------------------------------------------
PREFIX="${PREFIX:-}"
ANDROID_ROOT="${ANDROID_ROOT:-}"
# Termux on Android has ${PREFIX} set which already ends with '/usr'
if [[ -n "${ANDROID_ROOT}" && -n "${PREFIX}" ]]; then
INSTALL_PREFIX="${PREFIX}/bin"
else
INSTALL_PREFIX="/usr/local/bin"
fi
#-------------------------------------------------------------------------------
# FUNCTIONS
#-------------------------------------------------------------------------------
#--- FUNCTION ----------------------------------------------------------------
# NAME: print_banner
# DESCRIPTION: Prints a banner
# PARAMETERS: none
# RETURNS: 0
#-------------------------------------------------------------------------------
print_banner() {
cat <<-'EOF'
=================================================
____
/ ___|_ __ ___ ___
| | | '__/ _ \ / __|
| |___| | | (_) | (__
\____|_| \___/ \___|
___ _ _ _
|_ _|_ __ ___| |_ __ _| | | ___ _ __
| || '_ \/ __| __/ _` | | |/ _ \ '__|
| || | | \__ \ || (_| | | | __/ |
|___|_| |_|___/\__\__,_|_|_|\___|_|
==================================================
EOF
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: print_help
# DESCRIPTION: Prints out a help message
# PARAMETERS: none
# RETURNS: 0
#-------------------------------------------------------------------------------
print_help() {
local help_header
local help_message
help_header="croc Installer Script"
help_message="Usage:
-p INSTALL_PREFIX
Prefix to install croc into. Directory must already exist.
Default = /usr/local/bin ('\${PREFIX}/bin' on Termux for Android)
-h
Prints this helpful message and exit."
echo "${help_header}"
echo ""
echo "${help_message}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: print_message
# DESCRIPTION: Prints a message all fancy like
# PARAMETERS: $1 = Message to print
# $2 = Severity. info, ok, error, warn
# RETURNS: Formatted Message to stdout
#-------------------------------------------------------------------------------
print_message() {
local message
local severity
local red
local green
local yellow
local nc
message="${1}"
severity="${2}"
red='\e[0;31m'
green='\e[0;32m'
yellow='\e[1;33m'
nc='\e[0m'
case "${severity}" in
"info" ) echo -e "${nc}${message}${nc}";;
"ok" ) echo -e "${green}${message}${nc}";;
"error" ) echo -e "${red}${message}${nc}";;
"warn" ) echo -e "${yellow}${message}${nc}";;
esac
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: make_tempdir
# DESCRIPTION: Makes a temp dir using mktemp if available
# PARAMETERS: $1 = Directory template
# RETURNS: 0 = Created temp dir. Also prints temp file path to stdout
# 1 = Failed to create temp dir
# 20 = Failed to find mktemp
#-------------------------------------------------------------------------------
make_tempdir() {
local template
local tempdir
local tempdir_rcode
template="${1}.XXXXXX"
if command -v mktemp >/dev/null 2>&1; then
tempdir="$(mktemp -d -t "${template}")"
tempdir_rcode="${?}"
if [[ "${tempdir_rcode}" == "0" ]]; then
echo "${tempdir}"
return 0
else
return 1
fi
else
return 20
fi
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: determine_os
# DESCRIPTION: Attempts to determine host os using uname
# PARAMETERS: none
# RETURNS: 0 = OS Detected. Also prints detected os to stdout
# 1 = Unknown OS
# 20 = 'uname' not found in path
#-------------------------------------------------------------------------------
determine_os() {
local uname_out
if command -v uname >/dev/null 2>&1; then
uname_out="$(uname)"
if [[ "${uname_out}" == "" ]]; then
return 1
else
echo "${uname_out}"
return 0
fi
else
return 20
fi
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: determine_arch
# DESCRIPTION: Attempt to determine architecture of host
# PARAMETERS: none
# RETURNS: 0 = Arch Detected. Also prints detected arch to stdout
# 1 = Unknown arch
# 20 = 'uname' not found in path
#-------------------------------------------------------------------------------
determine_arch() {
local uname_out
if command -v uname >/dev/null 2>&1; then
uname_out="$(uname -m)"
if [[ "${uname_out}" == "" ]]; then
return 1
else
echo "${uname_out}"
return 0
fi
else
return 20
fi
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: download_file
# DESCRIPTION: Downloads a file into the specified directory. Attempts to
# use curl, then wget. If neither is found, fail.
# PARAMETERS: $1 = url of file to download
# $2 = location to download file into on host system
# RETURNS: If curl or wget found, returns the return code of curl or wget
# 20 = Could not find curl and wget
#-------------------------------------------------------------------------------
download_file() {
local url
local dir
local filename
local rcode
url="${1}"
dir="${2}"
filename="${3}"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "${url}" -o "${dir}/${filename}"
rcode="${?}"
elif command -v wget >/dev/null 2>&1; then
wget --quiet "${url}" -O "${dir}/${filename}"
rcode="${?}"
else
rcode="20"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: checksum_check
# DESCRIPTION: Attempt to verify checksum of downloaded file to ensure
# integrity. Tries multiple tools before failing.
# PARAMETERS: $1 = path to checksum file
# $2 = location of file to check
# $3 = working directory
# RETURNS: 0 = checkusm verified
# 1 = checksum verification failed
# 20 = failed to determine tool to use to check checksum
# 30 = failed to change into or go back from working dir
#-------------------------------------------------------------------------------
checksum_check() {
local checksum_file
local file
local dir
local rcode
local shasum_1
local shasum_2
local shasum_c
checksum_file="${1}"
file="${2}"
dir="${3}"
cd "${dir}" || return 30
if command -v sha256sum >/dev/null 2>&1; then
## Not all sha256sum versions seem to have --ignore-missing, so filter the checksum file
## to only include the file we downloaded.
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
shasum_c="$(sha256sum -c "filtered_checksum.txt")"
rcode="${?}"
elif command -v shasum >/dev/null 2>&1; then
## With shasum on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
## to only include the file we downloaded.
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
shasum_c="$(shasum -a 256 -c "filtered_checksum.txt")"
rcode="${?}"
elif command -v sha256 >/dev/null 2>&1; then
## With sha256 on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
## to only include the file we downloaded.
## Also sha256 -c option seems to fail, so fall back to an if statement
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
shasum_1="$(sha256 -q "${file}")"
shasum_2="$(awk '{print $1}' filtered_checksum.txt)"
if [[ "${shasum_1}" == "${shasum_2}" ]]; then
rcode="0"
else
rcode="1"
fi
shasum_c="Expected: ${shasum_1}, Got: ${shasum_2}"
else
return 20
fi
cd - >/dev/null 2>&1 || return 30
if [[ "${rcode}" -gt "0" ]]; then
echo "${shasum_c}"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: extract_file
# DESCRIPTION: Extracts a file into a location. Attempts to determine which
# tool to use by checking file extension.
# PARAMETERS: $1 = file to extract
# $2 = location to extract file into
# $3 = extension
# RETURNS: Return code of the tool used to extract the file
# 20 = Failed to determine which tool to use
# 30 = Failed to find tool in path
#-------------------------------------------------------------------------------
extract_file() {
local file
local dir
local ext
local rcode
file="${1}"
dir="${2}"
ext="${3}"
case "${ext}" in
"zip" ) if command -v unzip >/dev/null 2>&1; then
unzip "${file}" -d "${dir}"
rcode="${?}"
else
rcode="30"
fi
;;
"tar.gz" ) if command -v tar >/dev/null 2>&1; then
tar -xf "${file}" -C "${dir}"
rcode="${?}"
else
rcode="31"
fi
;;
* ) rcode="20";;
esac
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: create_prefix
# DESCRIPTION: Creates the install prefix (and any parent directories). If
# EUID not 0, then attempt to use sudo.
# PARAMETERS: $1 = prefix
# RETURNS: Return code of the tool used to make the directory
# 0 = Created the directory
# >0 = Failed to create directory
# 20 = Could not find mkdir command
# 21 = Could not find sudo command
#-------------------------------------------------------------------------------
create_prefix() {
local prefix
local rcode
prefix="${1}"
if command -v mkdir >/dev/null 2>&1; then
if [[ "${EUID}" == "0" ]]; then
mkdir -p "${prefix}"
rcode="${?}"
else
if command -v sudo >/dev/null 2>&1; then
sudo mkdir -p "${prefix}"
rcode="${?}"
else
rcode="21"
fi
fi
else
rcode="20"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: install_file_freebsd
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
# 0, then attempt to use sudo.
# PARAMETERS: $1 = file to install
# $2 = location to install file into
# RETURNS: 0 = File Installed
# 1 = File not installed
# 20 = Could not find install command
# 21 = Could not find sudo command
#-------------------------------------------------------------------------------
install_file_freebsd() {
local file
local prefix
local rcode
file="${1}"
prefix="${2}"
if command -v install >/dev/null 2>&1; then
if [[ "${EUID}" == "0" ]]; then
install -C -b -B '_old' -m 755 "${file}" "${prefix}"
rcode="${?}"
else
if command -v sudo >/dev/null 2>&1; then
sudo install -C -b -B '_old' -m 755 "${file}" "${prefix}"
rcode="${?}"
else
rcode="21"
fi
fi
else
rcode="20"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: install_file_linux
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
# 0, then attempt to use sudo (unless on android).
# PARAMETERS: $1 = file to install
# $2 = location to install file into
# RETURNS: 0 = File Installed
# 1 = File not installed
# 20 = Could not find install command
# 21 = Could not find sudo command
#-------------------------------------------------------------------------------
install_file_linux() {
local file
local prefix
local rcode
file="${1}"
prefix="${2}"
if command -v install >/dev/null 2>&1; then
if [[ "${EUID}" == "0" ]]; then
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
rcode="${?}"
else
if command -v sudo >/dev/null 2>&1; then
sudo install -C -b -S '_old' -m 755 "${file}" "${prefix}"
rcode="${?}"
elif [[ "${ANDROID_ROOT}" != "" ]]; then
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
rcode="${?}"
else
rcode="21"
fi
fi
else
rcode="20"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: install_file_cygwin
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
# 0, then attempt to use sudo.
# Not really 100% sure this is how to install croc in cygwin.
# PARAMETERS: $1 = file to install
# $2 = location to install file into
# RETURNS: 0 = File Installed
# 20 = Could not find install command
# 21 = Could not find sudo command
#-------------------------------------------------------------------------------
install_file_cygwin() {
local file
local prefix
local rcode
file="${1}"
prefix="${2}"
if command -v install >/dev/null 2>&1; then
if [[ "${EUID}" == "0" ]]; then
install -m 755 "${prefix}" "${file}"
rcode="${?}"
else
if command -v sudo >/dev/null 2>&1; then
sudo install -m 755 "${file}" "${prefix}"
rcode="${?}"
else
rcode="21"
fi
fi
else
rcode="20"
fi
return "${rcode}"
}
#--- FUNCTION ----------------------------------------------------------------
# NAME: main
# DESCRIPTION: Put it all together in a logical way
# ...at least that is the hope...
# PARAMETERS: 1 = prefix
# RETURNS: 0 = All good
# 1 = Something done broke
#-------------------------------------------------------------------------------
main() {
local prefix
local tmpdir
local tmpdir_rcode
local croc_arch
local croc_arch_rcode
local croc_os
local croc_os_rcode
local croc_base_url
local croc_url
local croc_file
local croc_checksum_file
local croc_bin_name
local croc_version
local croc_dl_ext
local download_file_rcode
local download_checksum_file_rcode
local checksum_check_rcode
local extract_file_rcode
local install_file_rcode
local create_prefix_rcode
local bash_autocomplete_file
local bash_autocomplete_prefix
local zsh_autocomplete_file
local zsh_autocomplete_prefix
local autocomplete_install_rcode
croc_bin_name="croc"
croc_version="10.1.1"
croc_dl_ext="tar.gz"
croc_base_url="https://github.com/schollz/croc/releases/download"
prefix="${1}"
bash_autocomplete_file="bash_autocomplete"
bash_autocomplete_prefix="/etc/bash_completion.d"
zsh_autocomplete_file="zsh_autocomplete"
zsh_autocomplete_prefix="/etc/zsh"
print_banner
print_message "== Install prefix set to ${prefix}" "info"
tmpdir="$(make_tempdir "${croc_bin_name}")"
tmpdir_rcode="${?}"
if [[ "${tmpdir_rcode}" == "0" ]]; then
print_message "== Created temp dir at ${tmpdir}" "info"
elif [[ "${tmpdir_rcode}" == "1" ]]; then
print_message "== Failed to create temp dir at ${tmpdir}" "error"
else
print_message "== 'mktemp' not found in path. Is it installed?" "error"
exit 1
fi
croc_arch="$(determine_arch)"
croc_arch_rcode="${?}"
if [[ "${croc_arch_rcode}" == "0" ]]; then
print_message "== Architecture detected as ${croc_arch}" "info"
elif [[ "${croc_arch_rcode}" == "1" ]]; then
print_message "== Architecture not detected" "error"
exit 1
else
print_message "== 'uname' not found in path. Is it installed?" "error"
exit 1
fi
croc_os="$(determine_os)"
croc_os_rcode="${?}"
if [[ "${croc_os_rcode}" == "0" ]]; then
print_message "== OS detected as ${croc_os}" "info"
elif [[ "${croc_os_rcode}" == "1" ]]; then
print_message "== OS not detected" "error"
exit 1
else
print_message "== 'uname' not found in path. Is it installed?" "error"
exit 1
fi
case "${croc_os}" in
"Darwin" ) croc_os="macOS";;
*"BusyBox"* )
croc_os="Linux"
;;
"CYGWIN"* ) croc_os="Windows";
croc_dl_ext="zip";
print_message "== Cygwin is currently unsupported." "error";
exit 1;;
esac
case "${croc_arch}" in
"x86_64" ) croc_arch="64bit";;
"amd64" ) croc_arch="64bit";;
"aarch64" ) croc_arch="ARM64";;
"arm64" ) croc_arch="ARM64";;
"armv7l" ) croc_arch="ARM";;
"armv8l" ) croc_arch="ARM";;
"armv9l" ) croc_arch="ARM";;
"i686" ) croc_arch="32bit";;
* ) croc_arch="unknown";;
esac
croc_file="${croc_bin_name}_v${croc_version}_${croc_os}-${croc_arch}.${croc_dl_ext}"
croc_checksum_file="${croc_bin_name}_v${croc_version}_checksums.txt"
croc_url="${croc_base_url}/v${croc_version}/${croc_file}"
croc_checksum_url="${croc_base_url}/v${croc_version}/${croc_checksum_file}"
echo "${croc_url}" "${tmpdir}" "${croc_file}"
download_file "${croc_url}" "${tmpdir}" "${croc_file}"
download_file_rcode="${?}"
if [[ "${download_file_rcode}" == "0" ]]; then
print_message "== Downloaded croc archive into ${tmpdir}" "info"
elif [[ "${download_file_rcode}" == "1" ]]; then
print_message "== Failed to download croc archive" "error"
exit 1
elif [[ "${download_file_rcode}" == "20" ]]; then
print_message "== Failed to locate curl or wget" "error"
exit 1
else
print_message "== Return code of download tool returned an unexpected value of ${download_file_rcode}" "error"
exit 1
fi
download_file "${croc_checksum_url}" "${tmpdir}" "${croc_checksum_file}"
download_checksum_file_rcode="${?}"
if [[ "${download_checksum_file_rcode}" == "0" ]]; then
print_message "== Downloaded croc checksums file into ${tmpdir}" "info"
elif [[ "${download_checksum_file_rcode}" == "1" ]]; then
print_message "== Failed to download croc checksums" "error"
exit 1
elif [[ "${download_checksum_file_rcode}" == "20" ]]; then
print_message "== Failed to locate curl or wget" "error"
exit 1
else
print_message "== Return code of download tool returned an unexpected value of ${download_checksum_file_rcode}" "error"
exit 1
fi
checksum_check "${tmpdir}/${croc_checksum_file}" "${tmpdir}/${croc_file}" "${tmpdir}"
checksum_check_rcode="${?}"
if [[ "${checksum_check_rcode}" == "0" ]]; then
print_message "== Checksum of ${tmpdir}/${croc_file} verified" "ok"
elif [[ "${checksum_check_rcode}" == "1" ]]; then
print_message "== Failed to verify checksum of ${tmpdir}/${croc_file}" "error"
exit 1
elif [[ "${checksum_check_rcode}" == "20" ]]; then
print_message "== Failed to find tool to verify sha256 sums" "error"
exit 1
elif [[ "${checksum_check_rcode}" == "30" ]]; then
print_message "== Failed to change into working directory ${tmpdir}" "error"
exit 1
else
print_message "== Unknown return code returned while checking checksum of ${tmpdir}/${croc_file}. Returned ${checksum_check_rcode}" "error"
exit 1
fi
extract_file "${tmpdir}/${croc_file}" "${tmpdir}/" "${croc_dl_ext}"
extract_file_rcode="${?}"
if [[ "${extract_file_rcode}" == "0" ]]; then
print_message "== Extracted ${croc_file} to ${tmpdir}/" "info"
elif [[ "${extract_file_rcode}" == "1" ]]; then
print_message "== Failed to extract ${croc_file}" "error"
exit 1
elif [[ "${extract_file_rcode}" == "20" ]]; then
print_message "== Failed to determine which extraction tool to use" "error"
exit 1
elif [[ "${extract_file_rcode}" == "30" ]]; then
print_message "== Failed to find 'unzip' in path" "error"
exit 1
elif [[ "${extract_file_rcode}" == "31" ]]; then
print_message "== Failed to find 'tar' in path" "error"
exit 1
else
print_message "== Unknown error returned from extraction attempt" "error"
exit 1
fi
if [[ ! -d "${prefix}" ]]; then
create_prefix "${prefix}"
create_prefix_rcode="${?}"
if [[ "${create_prefix_rcode}" == "0" ]]; then
print_message "== Created install prefix at ${prefix}" "info"
elif [[ "${create_prefix_rcode}" == "20" ]]; then
print_message "== Failed to find mkdir in path" "error"
exit 1
elif [[ "${create_prefix_rcode}" == "21" ]]; then
print_message "== Failed to find sudo in path" "error"
exit 1
else
print_message "== Failed to create the install prefix: ${prefix}" "error"
exit 1
fi
else
print_message "== Install prefix already exists. No need to create it." "info"
fi
[ ! -d "${bash_autocomplete_prefix}/croc" ] && mkdir -p "${bash_autocomplete_prefix}/croc" >/dev/null 2>&1
case "${croc_os}" in
"Linux" ) install_file_linux "${tmpdir}/${croc_bin_name}" "${prefix}/";
install_file_rcode="${?}";;
"FreeBSD" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
install_file_rcode="${?}";;
"macOS" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
install_file_rcode="${?}";;
"Windows" ) install_file_cygwin "${tmpdir}/${croc_bin_name}" "${prefix}/";
install_file_rcode="${?}";;
esac
if [[ "${install_file_rcode}" == "0" ]] ; then
print_message "== Installed ${croc_bin_name} to ${prefix}/" "ok"
elif [[ "${install_file_rcode}" == "1" ]]; then
print_message "== Failed to install ${croc_bin_name}" "error"
exit 1
elif [[ "${install_file_rcode}" == "20" ]]; then
print_message "== Failed to locate 'install' command" "error"
exit 1
elif [[ "${install_file_rcode}" == "21" ]]; then
print_message "== Failed to locate 'sudo' command" "error"
exit 1
else
print_message "== Install attempt returned an unexpected value of ${install_file_rcode}" "error"
exit 1
fi
# case "$(basename ${SHELL})" in
# "bash" ) install_file_linux "${tmpdir}/${bash_autocomplete_file}" "${bash_autocomplete_prefix}/croc";
# autocomplete_install_rcode="${?}";;
# "zsh" ) install_file_linux "${tmpdir}/${zsh_autocomplete_file}" "${zsh_autocomplete_prefix}/zsh_autocomplete_croc";
# autocomplete_install_rcode="${?}";
# print_message "== You will need to add the following to your ~/.zshrc to enable autocompletion" "info";
# print_message "\nPROG=croc\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/zsh_autocomplete_croc\n" "info";;
# *) autocomplete_install_rcode="1";;
# esac
# if [[ "${autocomplete_install_rcode}" == "0" ]] ; then
# print_message "== Installed autocompletions for $(basename "${SHELL}")" "ok"
# elif [[ "${autocomplete_install_rcode}" == "1" ]]; then
# print_message "== Failed to install ${bash_autocomplete_file}" "error"
# elif [[ "${autocomplete_install_rcode}" == "20" ]]; then
# print_message "== Failed to locate 'install' command" "error"
# elif [[ "${autocomplete_install_rcode}" == "21" ]]; then
# print_message "== Failed to locate 'sudo' command" "error"
# else
# print_message "== Install attempt returned an unexpected value of ${autocomplete_install_rcode}" "error"
# fi
print_message "== Installation complete" "ok"
exit 0
}
#-------------------------------------------------------------------------------
# ARGUMENT PARSING
#-------------------------------------------------------------------------------
OPTS="hp:"
while getopts "${OPTS}" optchar; do
case "${optchar}" in
'h' ) print_help
exit 0
;;
'p' ) INSTALL_PREFIX="${OPTARG}"
;;
/? ) print_message "Unknown option ${OPTARG}" "warn"
;;
esac
done
#-------------------------------------------------------------------------------
# CALL MAIN
#-------------------------------------------------------------------------------
main "${INSTALL_PREFIX}"

View File

@ -1,7 +0,0 @@
#!/bin/bash
tmp=$(mktemp -d)
echo $VERSION
git clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-${VERSION}
(cd $tmp/croc-${VERSION} && go mod tidy && go mod vendor)
(cd $tmp && tar -cvzf croc_${VERSION}_src.tar.gz croc-${VERSION})
mv $tmp/croc_${VERSION}_src.tar.gz dist/

View File

@ -1,77 +0,0 @@
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
err := run()
if err != nil {
fmt.Println(err)
}
}
func run() (err error) {
versionNew := "v" + os.Getenv("VERSION")
versionHash, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
if err != nil {
return
}
versionHashNew := strings.TrimSpace(string(versionHash))
fmt.Println(versionNew)
fmt.Println(versionHashNew)
err = replaceInFile("src/cli/cli.go", `Version = "`, `"`, versionNew+"-"+versionHashNew)
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])
}
err = replaceInFile("src/install/default.txt", `croc_version="`, `"`, strings.Split(versionNew, "-")[0][1:])
if err == nil {
fmt.Printf("updated default.txt to version %s\n", strings.Split(versionNew, "-")[0][1:])
}
return
}
func replaceInFile(fname, start, end, replacement string) (err error) {
b, err := os.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 = os.WriteFile(fname, []byte(newF), 0o644)
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]
}

View File

@ -1,51 +0,0 @@
#!/bin/bash
VERSION=$(cat ./src/cli/cli.go | grep 'Version = "v' | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
echo $VERSION
# Check dependencies.
set -e
xargs=$(which gxargs || which xargs)
# Validate settings.
[ "$TRACE" ] && set -x
CONFIG=$@
for line in $CONFIG; do
eval "$line"
done
owner="schollz"
repo="croc"
tag="v${VERSION}"
filename="dist/croc_${VERSION}_src.tar.gz"
# Define variables.
GH_API="https://api.github.com"
GH_REPO="$GH_API/repos/$owner/$repo"
GH_TAGS="$GH_REPO/releases/tags/$tag"
AUTH="Authorization: token $GITHUB_TOKEN"
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
CURL_ARGS="-LJO#"
if [[ "$tag" == 'LATEST' ]]; then
GH_TAGS="$GH_REPO/releases/latest"
fi
# Validate token.
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
# Read asset tags.
response=$(curl -sH "$AUTH" $GH_TAGS)
# Get ID of the asset based on given filename.
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
# Upload asset
echo "Uploading asset... "
# Construct url
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" $GH_ASSET

View File

@ -1,23 +0,0 @@
#compdef $PROG
_cli_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
return
}
compdef _cli_zsh_autocomplete $PROG

43
src/logger/logger.go Normal file
View File

@ -0,0 +1,43 @@
package logger
import (
log "github.com/cihub/seelog"
)
// SetLogLevel determines the log level
func SetLogLevel(level string) (err error) {
// https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
// https://github.com/cihub/seelog/wiki/Log-levels
appConfig := `
<seelog minlevel="` + level + `">
<outputs formatid="stdout">
<filter levels="debug,trace">
<console formatid="debug"/>
</filter>
<filter levels="info">
<console formatid="info"/>
</filter>
<filter levels="critical,error">
<console formatid="error"/>
</filter>
<filter levels="warn">
<console formatid="warn"/>
</filter>
</outputs>
<formats>
<format id="stdout" format="%Date %Time [%LEVEL] %File %FuncShort:%Line %Msg %n" />
<format id="debug" format="%Date %Time %EscM(37)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
<format id="info" format="%Date %Time %EscM(36)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
<format id="warn" format="%Date %Time %EscM(33)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
<format id="error" format="%Date %Time %EscM(31)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
</formats>
</seelog>
`
logger, err := log.LoggerFromConfigAsBytes([]byte(appConfig))
if err != nil {
return
}
log.ReplaceLogger(logger)
return
}

View File

@ -1,84 +0,0 @@
package message
import (
"encoding/json"
"github.com/schollz/croc/v10/src/comm"
"github.com/schollz/croc/v10/src/compress"
"github.com/schollz/croc/v10/src/crypt"
log "github.com/schollz/logger"
)
// Type is a message type
type Type string
const (
TypePAKE Type = "pake"
TypeExternalIP Type = "externalip"
TypeFinished Type = "finished"
TypeError Type = "error"
TypeCloseRecipient Type = "close-recipient"
TypeCloseSender Type = "close-sender"
TypeRecipientReady Type = "recipientready"
TypeFileInfo Type = "fileinfo"
)
// Message is the possible payload for messaging
type Message struct {
Type Type `json:"t,omitempty"`
Message string `json:"m,omitempty"`
Bytes []byte `json:"b,omitempty"`
Bytes2 []byte `json:"b2,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 []byte, m Message) (err error) {
mSend, err := Encode(key, m)
if err != nil {
return
}
err = c.Send(mSend)
return
}
// Encode will convert to bytes
func Encode(key []byte, m Message) (b []byte, err error) {
b, err = json.Marshal(m)
if err != nil {
return
}
b = compress.Compress(b)
if key != nil {
log.Debugf("writing %s message (encrypted)", m.Type)
b, err = crypt.Encrypt(b, key)
} else {
log.Debugf("writing %s message (unencrypted)", m.Type)
}
return
}
// Decode will convert from bytes
func Decode(key []byte, b []byte) (m Message, err error) {
if key != nil {
b, err = crypt.Decrypt(b, key)
if err != nil {
return
}
}
b = compress.Decompress(b)
err = json.Unmarshal(b, &m)
if err == nil {
if key != nil {
log.Debugf("read %s message (encrypted)", m.Type)
} else {
log.Debugf("read %s message (unencrypted)", m.Type)
}
}
return
}

View File

@ -1,96 +0,0 @@
package message
import (
"crypto/rand"
"fmt"
"net"
"testing"
"time"
"github.com/schollz/croc/v10/src/comm"
"github.com/schollz/croc/v10/src/crypt"
log "github.com/schollz/logger"
"github.com/stretchr/testify/assert"
)
var TypeMessage Type = "message"
func TestMessage(t *testing.T) {
log.SetLevel("debug")
m := Message{Type: TypeMessage, Message: "hello, world"}
e, salt, err := crypt.New([]byte("pass"), nil)
assert.Nil(t, err)
fmt.Println(string(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)
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
_, err = Decode([]byte("not pass"), b)
assert.NotNil(t, err)
_, err = Encode([]byte("0"), m)
assert.NotNil(t, err)
}
func TestMessageNoPass(t *testing.T) {
log.SetLevel("debug")
m := Message{Type: TypeMessage, Message: "hello, world"}
b, err := Encode(nil, m)
assert.Nil(t, err)
fmt.Printf("%x\n", b)
m2, err := Decode(nil, b)
assert.Nil(t, err)
assert.Equal(t, m, m2)
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
}
func TestSend(t *testing.T) {
token := make([]byte, 40000000)
rand.Read(token)
port := "8801"
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(_ string, connection net.Conn) {
c := comm.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(800 * time.Millisecond)
a, err := comm.NewConnection("127.0.0.1:"+port, 10*time.Minute)
assert.Nil(t, err)
m := Message{Type: TypeMessage, Message: "hello, world"}
e, salt, err := crypt.New([]byte("pass"), nil)
log.Debug(salt)
assert.Nil(t, err)
assert.Nil(t, Send(a, e, m))
}

View File

@ -1,88 +0,0 @@
// From GitHub version/fork maintained by Stephen Paul Weber available at:
// https://github.com/singpolyma/mnemonicode
//
// Originally from:
// http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/
/*
Copyright (c) 2000 Oren Tirosh <oren@hishome.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package mnemonicode
const base = 1626
// WordsRequired returns the number of words required to encode input
// data of length bytes using mnomonic encoding.
//
// Every four bytes of input is encoded into three words. If there
// is an extra one or two bytes they get an extra one or two words
// respectively. If there is an extra three bytes, they will be encoded
// into three words with the last word being one of a small set of very
// short words (only needed to encode the last 3 bits).
func WordsRequired(length int) int {
return ((length + 1) * 3) / 4
}
// EncodeWordList encodes src into mnemomic words which are appended to dst.
// The final wordlist is returned.
// There will be WordsRequired(len(src)) words appeneded.
func EncodeWordList(dst []string, src []byte) (result []string) {
if n := len(dst) + WordsRequired(len(src)); cap(dst) < n {
result = make([]string, len(dst), n)
copy(result, dst)
} else {
result = dst
}
var x uint32
for len(src) >= 4 {
x = uint32(src[0])
x |= uint32(src[1]) << 8
x |= uint32(src[2]) << 16
x |= uint32(src[3]) << 24
src = src[4:]
i0 := int(x % base)
i1 := int(x/base) % base
i2 := int(x/base/base) % base
result = append(result, WordList[i0], WordList[i1], WordList[i2])
}
if len(src) > 0 {
x = 0
for i := len(src) - 1; i >= 0; i-- {
x <<= 8
x |= uint32(src[i])
}
i := int(x % base)
result = append(result, WordList[i])
if len(src) >= 2 {
i = int(x/base) % base
result = append(result, WordList[i])
}
if len(src) == 3 {
i = base + int(x/base/base)%7
result = append(result, WordList[i])
}
}
return result
}

View File

@ -1,318 +0,0 @@
// From GitHub version/fork maintained by Stephen Paul Weber available at:
// https://github.com/singpolyma/mnemonicode
//
// Originally from:
// http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/
/*
Copyright (c) 2000 Oren Tirosh <oren@hishome.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package mnemonicode
// WordListVersion is the version of compiled in word list.
const WordListVersion = "0.7"
var wordMap = make(map[string]int, len(WordList))
func init() {
for i, w := range WordList {
wordMap[w] = i
}
}
const longestWord = 7
var WordList = []string{
"academy", "acrobat", "active", "actor", "adam", "admiral",
"adrian", "africa", "agenda", "agent", "airline", "airport",
"aladdin", "alarm", "alaska", "albert", "albino", "album",
"alcohol", "alex", "algebra", "alibi", "alice", "alien",
"alpha", "alpine", "amadeus", "amanda", "amazon", "amber",
"america", "amigo", "analog", "anatomy", "angel", "animal",
"antenna", "antonio", "apollo", "april", "archive", "arctic",
"arizona", "arnold", "aroma", "arthur", "artist", "asia",
"aspect", "aspirin", "athena", "athlete", "atlas", "audio",
"august", "austria", "axiom", "aztec", "balance", "ballad",
"banana", "bandit", "banjo", "barcode", "baron", "basic",
"battery", "belgium", "berlin", "bermuda", "bernard", "bikini",
"binary", "bingo", "biology", "block", "blonde", "bonus",
"boris", "boston", "boxer", "brandy", "bravo", "brazil",
"bronze", "brown", "bruce", "bruno", "burger", "burma",
"cabinet", "cactus", "cafe", "cairo", "cake", "calypso",
"camel", "camera", "campus", "canada", "canal", "cannon",
"canoe", "cantina", "canvas", "canyon", "capital", "caramel",
"caravan", "carbon", "cargo", "carlo", "carol", "carpet",
"cartel", "casino", "castle", "castro", "catalog", "caviar",
"cecilia", "cement", "center", "century", "ceramic", "chamber",
"chance", "change", "chaos", "charlie", "charm", "charter",
"chef", "chemist", "cherry", "chess", "chicago", "chicken",
"chief", "china", "cigar", "cinema", "circus", "citizen",
"city", "clara", "classic", "claudia", "clean", "client",
"climax", "clinic", "clock", "club", "cobra", "coconut",
"cola", "collect", "colombo", "colony", "color", "combat",
"comedy", "comet", "command", "compact", "company", "complex",
"concept", "concert", "connect", "consul", "contact", "context",
"contour", "control", "convert", "copy", "corner", "corona",
"correct", "cosmos", "couple", "courage", "cowboy", "craft",
"crash", "credit", "cricket", "critic", "crown", "crystal",
"cuba", "culture", "dallas", "dance", "daniel", "david",
"decade", "decimal", "deliver", "delta", "deluxe", "demand",
"demo", "denmark", "derby", "design", "detect", "develop",
"diagram", "dialog", "diamond", "diana", "diego", "diesel",
"diet", "digital", "dilemma", "diploma", "direct", "disco",
"disney", "distant", "doctor", "dollar", "dominic", "domino",
"donald", "dragon", "drama", "dublin", "duet", "dynamic",
"east", "ecology", "economy", "edgar", "egypt", "elastic",
"elegant", "element", "elite", "elvis", "email", "energy",
"engine", "english", "episode", "equator", "escort", "ethnic",
"europe", "everest", "evident", "exact", "example", "exit",
"exotic", "export", "express", "extra", "fabric", "factor",
"falcon", "family", "fantasy", "fashion", "fiber", "fiction",
"fidel", "fiesta", "figure", "film", "filter", "final",
"finance", "finish", "finland", "flash", "florida", "flower",
"fluid", "flute", "focus", "ford", "forest", "formal",
"format", "formula", "fortune", "forum", "fragile", "france",
"frank", "friend", "frozen", "future", "gabriel", "galaxy",
"gallery", "gamma", "garage", "garden", "garlic", "gemini",
"general", "genetic", "genius", "germany", "global", "gloria",
"golf", "gondola", "gong", "good", "gordon", "gorilla",
"grand", "granite", "graph", "green", "group", "guide",
"guitar", "guru", "hand", "happy", "harbor", "harmony",
"harvard", "havana", "hawaii", "helena", "hello", "henry",
"hilton", "history", "horizon", "hotel", "human", "humor",
"icon", "idea", "igloo", "igor", "image", "impact",
"import", "index", "india", "indigo", "input", "insect",
"instant", "iris", "italian", "jacket", "jacob", "jaguar",
"janet", "japan", "jargon", "jazz", "jeep", "john",
"joker", "jordan", "jumbo", "june", "jungle", "junior",
"jupiter", "karate", "karma", "kayak", "kermit", "kilo",
"king", "koala", "korea", "labor", "lady", "lagoon",
"laptop", "laser", "latin", "lava", "lecture", "left",
"legal", "lemon", "level", "lexicon", "liberal", "libra",
"limbo", "limit", "linda", "linear", "lion", "liquid",
"liter", "little", "llama", "lobby", "lobster", "local",
"logic", "logo", "lola", "london", "lotus", "lucas",
"lunar", "machine", "macro", "madam", "madonna", "madrid",
"maestro", "magic", "magnet", "magnum", "major", "mama",
"mambo", "manager", "mango", "manila", "marco", "marina",
"market", "mars", "martin", "marvin", "master", "matrix",
"maximum", "media", "medical", "mega", "melody", "melon",
"memo", "mental", "mentor", "menu", "mercury", "message",
"metal", "meteor", "meter", "method", "metro", "mexico",
"miami", "micro", "million", "mineral", "minimum", "minus",
"minute", "miracle", "mirage", "miranda", "mister", "mixer",
"mobile", "model", "modem", "modern", "modular", "moment",
"monaco", "monica", "monitor", "mono", "monster", "montana",
"morgan", "motel", "motif", "motor", "mozart", "multi",
"museum", "music", "mustang", "natural", "neon", "nepal",
"neptune", "nerve", "neutral", "nevada", "news", "ninja",
"nirvana", "normal", "nova", "novel", "nuclear", "numeric",
"nylon", "oasis", "object", "observe", "ocean", "octopus",
"olivia", "olympic", "omega", "opera", "optic", "optimal",
"orange", "orbit", "organic", "orient", "origin", "orlando",
"oscar", "oxford", "oxygen", "ozone", "pablo", "pacific",
"pagoda", "palace", "pamela", "panama", "panda", "panel",
"panic", "paradox", "pardon", "paris", "parker", "parking",
"parody", "partner", "passage", "passive", "pasta", "pastel",
"patent", "patriot", "patrol", "patron", "pegasus", "pelican",
"penguin", "pepper", "percent", "perfect", "perfume", "period",
"permit", "person", "peru", "phone", "photo", "piano",
"picasso", "picnic", "picture", "pigment", "pilgrim", "pilot",
"pirate", "pixel", "pizza", "planet", "plasma", "plaster",
"plastic", "plaza", "pocket", "poem", "poetic", "poker",
"polaris", "police", "politic", "polo", "polygon", "pony",
"popcorn", "popular", "postage", "postal", "precise", "prefix",
"premium", "present", "price", "prince", "printer", "prism",
"private", "product", "profile", "program", "project", "protect",
"proton", "public", "pulse", "puma", "pyramid", "queen",
"radar", "radio", "random", "rapid", "rebel", "record",
"recycle", "reflex", "reform", "regard", "regular", "relax",
"report", "reptile", "reverse", "ricardo", "ringo", "ritual",
"robert", "robot", "rocket", "rodeo", "romeo", "royal",
"russian", "safari", "salad", "salami", "salmon", "salon",
"salute", "samba", "sandra", "santana", "sardine", "school",
"screen", "script", "second", "secret", "section", "segment",
"select", "seminar", "senator", "senior", "sensor", "serial",
"service", "sheriff", "shock", "sierra", "signal", "silicon",
"silver", "similar", "simon", "single", "siren", "slogan",
"social", "soda", "solar", "solid", "solo", "sonic",
"soviet", "special", "speed", "spiral", "spirit", "sport",
"static", "station", "status", "stereo", "stone", "stop",
"street", "strong", "student", "studio", "style", "subject",
"sultan", "super", "susan", "sushi", "suzuki", "switch",
"symbol", "system", "tactic", "tahiti", "talent", "tango",
"tarzan", "taxi", "telex", "tempo", "tennis", "texas",
"textile", "theory", "thermos", "tiger", "titanic", "tokyo",
"tomato", "topic", "tornado", "toronto", "torpedo", "total",
"totem", "tourist", "tractor", "traffic", "transit", "trapeze",
"travel", "tribal", "trick", "trident", "trilogy", "tripod",
"tropic", "trumpet", "tulip", "tuna", "turbo", "twist",
"ultra", "uniform", "union", "uranium", "vacuum", "valid",
"vampire", "vanilla", "vatican", "velvet", "ventura", "venus",
"vertigo", "veteran", "victor", "video", "vienna", "viking",
"village", "vincent", "violet", "violin", "virtual", "virus",
"visa", "vision", "visitor", "visual", "vitamin", "viva",
"vocal", "vodka", "volcano", "voltage", "volume", "voyage",
"water", "weekend", "welcome", "western", "window", "winter",
"wizard", "wolf", "world", "xray", "yankee", "yoga",
"yogurt", "yoyo", "zebra", "zero", "zigzag", "zipper",
"zodiac", "zoom", "abraham", "action", "address", "alabama",
"alfred", "almond", "ammonia", "analyze", "annual", "answer",
"apple", "arena", "armada", "arsenal", "atlanta", "atomic",
"avenue", "average", "bagel", "baker", "ballet", "bambino",
"bamboo", "barbara", "basket", "bazaar", "benefit", "bicycle",
"bishop", "blitz", "bonjour", "bottle", "bridge", "british",
"brother", "brush", "budget", "cabaret", "cadet", "candle",
"capitan", "capsule", "career", "cartoon", "channel", "chapter",
"cheese", "circle", "cobalt", "cockpit", "college", "compass",
"comrade", "condor", "crimson", "cyclone", "darwin", "declare",
"degree", "delete", "delphi", "denver", "desert", "divide",
"dolby", "domain", "domingo", "double", "drink", "driver",
"eagle", "earth", "echo", "eclipse", "editor", "educate",
"edward", "effect", "electra", "emerald", "emotion", "empire",
"empty", "escape", "eternal", "evening", "exhibit", "expand",
"explore", "extreme", "ferrari", "first", "flag", "folio",
"forget", "forward", "freedom", "fresh", "friday", "fuji",
"galileo", "garcia", "genesis", "gold", "gravity", "habitat",
"hamlet", "harlem", "helium", "holiday", "house", "hunter",
"ibiza", "iceberg", "imagine", "infant", "isotope", "jackson",
"jamaica", "jasmine", "java", "jessica", "judo", "kitchen",
"lazarus", "letter", "license", "lithium", "loyal", "lucky",
"magenta", "mailbox", "manual", "marble", "mary", "maxwell",
"mayor", "milk", "monarch", "monday", "money", "morning",
"mother", "mystery", "native", "nectar", "nelson", "network",
"next", "nikita", "nobel", "nobody", "nominal", "norway",
"nothing", "number", "october", "office", "oliver", "opinion",
"option", "order", "outside", "package", "pancake", "pandora",
"panther", "papa", "patient", "pattern", "pedro", "pencil",
"people", "phantom", "philips", "pioneer", "pluto", "podium",
"portal", "potato", "prize", "process", "protein", "proxy",
"pump", "pupil", "python", "quality", "quarter", "quiet",
"rabbit", "radical", "radius", "rainbow", "ralph", "ramirez",
"ravioli", "raymond", "respect", "respond", "result", "resume",
"retro", "richard", "right", "risk", "river", "roger",
"roman", "rondo", "sabrina", "salary", "salsa", "sample",
"samuel", "saturn", "savage", "scarlet", "scoop", "scorpio",
"scratch", "scroll", "sector", "serpent", "shadow", "shampoo",
"sharon", "sharp", "short", "shrink", "silence", "silk",
"simple", "slang", "smart", "smoke", "snake", "society",
"sonar", "sonata", "soprano", "source", "sparta", "sphere",
"spider", "sponsor", "spring", "acid", "adios", "agatha",
"alamo", "alert", "almanac", "aloha", "andrea", "anita",
"arcade", "aurora", "avalon", "baby", "baggage", "balloon",
"bank", "basil", "begin", "biscuit", "blue", "bombay",
"brain", "brenda", "brigade", "cable", "carmen", "cello",
"celtic", "chariot", "chrome", "citrus", "civil", "cloud",
"common", "compare", "cool", "copper", "coral", "crater",
"cubic", "cupid", "cycle", "depend", "door", "dream",
"dynasty", "edison", "edition", "enigma", "equal", "eric",
"event", "evita", "exodus", "extend", "famous", "farmer",
"food", "fossil", "frog", "fruit", "geneva", "gentle",
"george", "giant", "gilbert", "gossip", "gram", "greek",
"grille", "hammer", "harvest", "hazard", "heaven", "herbert",
"heroic", "hexagon", "husband", "immune", "inca", "inch",
"initial", "isabel", "ivory", "jason", "jerome", "joel",
"joshua", "journal", "judge", "juliet", "jump", "justice",
"kimono", "kinetic", "leonid", "lima", "maze", "medusa",
"member", "memphis", "michael", "miguel", "milan", "mile",
"miller", "mimic", "mimosa", "mission", "monkey", "moral",
"moses", "mouse", "nancy", "natasha", "nebula", "nickel",
"nina", "noise", "orchid", "oregano", "origami", "orinoco",
"orion", "othello", "paper", "paprika", "prelude", "prepare",
"pretend", "profit", "promise", "provide", "puzzle", "remote",
"repair", "reply", "rival", "riviera", "robin", "rose",
"rover", "rudolf", "saga", "sahara", "scholar", "shelter",
"ship", "shoe", "sigma", "sister", "sleep", "smile",
"spain", "spark", "split", "spray", "square", "stadium",
"star", "storm", "story", "strange", "stretch", "stuart",
"subway", "sugar", "sulfur", "summer", "survive", "sweet",
"swim", "table", "taboo", "target", "teacher", "telecom",
"temple", "tibet", "ticket", "tina", "today", "toga",
"tommy", "tower", "trivial", "tunnel", "turtle", "twin",
"uncle", "unicorn", "unique", "update", "valery", "vega",
"version", "voodoo", "warning", "william", "wonder", "year",
"yellow", "young", "absent", "absorb", "accent", "alfonso",
"alias", "ambient", "andy", "anvil", "appear", "apropos",
"archer", "ariel", "armor", "arrow", "austin", "avatar",
"axis", "baboon", "bahama", "bali", "balsa", "bazooka",
"beach", "beast", "beatles", "beauty", "before", "benny",
"betty", "between", "beyond", "billy", "bison", "blast",
"bless", "bogart", "bonanza", "book", "border", "brave",
"bread", "break", "broken", "bucket", "buenos", "buffalo",
"bundle", "button", "buzzer", "byte", "caesar", "camilla",
"canary", "candid", "carrot", "cave", "chant", "child",
"choice", "chris", "cipher", "clarion", "clark", "clever",
"cliff", "clone", "conan", "conduct", "congo", "content",
"costume", "cotton", "cover", "crack", "current", "danube",
"data", "decide", "desire", "detail", "dexter", "dinner",
"dispute", "donor", "druid", "drum", "easy", "eddie",
"enjoy", "enrico", "epoxy", "erosion", "except", "exile",
"explain", "fame", "fast", "father", "felix", "field",
"fiona", "fire", "fish", "flame", "flex", "flipper",
"float", "flood", "floor", "forbid", "forever", "fractal",
"frame", "freddie", "front", "fuel", "gallop", "game",
"garbo", "gate", "gibson", "ginger", "giraffe", "gizmo",
"glass", "goblin", "gopher", "grace", "gray", "gregory",
"grid", "griffin", "ground", "guest", "gustav", "gyro",
"hair", "halt", "harris", "heart", "heavy", "herman",
"hippie", "hobby", "honey", "hope", "horse", "hostel",
"hydro", "imitate", "info", "ingrid", "inside", "invent",
"invest", "invite", "iron", "ivan", "james", "jester",
"jimmy", "join", "joseph", "juice", "julius", "july",
"justin", "kansas", "karl", "kevin", "kiwi", "ladder",
"lake", "laura", "learn", "legacy", "legend", "lesson",
"life", "light", "list", "locate", "lopez", "lorenzo",
"love", "lunch", "malta", "mammal", "margo", "marion",
"mask", "match", "mayday", "meaning", "mercy", "middle",
"mike", "mirror", "modest", "morph", "morris", "nadia",
"nato", "navy", "needle", "neuron", "never", "newton",
"nice", "night", "nissan", "nitro", "nixon", "north",
"oberon", "octavia", "ohio", "olga", "open", "opus",
"orca", "oval", "owner", "page", "paint", "palma",
"parade", "parent", "parole", "paul", "peace", "pearl",
"perform", "phoenix", "phrase", "pierre", "pinball", "place",
"plate", "plato", "plume", "pogo", "point", "polite",
"polka", "poncho", "powder", "prague", "press", "presto",
"pretty", "prime", "promo", "quasi", "quest", "quick",
"quiz", "quota", "race", "rachel", "raja", "ranger",
"region", "remark", "rent", "reward", "rhino", "ribbon",
"rider", "road", "rodent", "round", "rubber", "ruby",
"rufus", "sabine", "saddle", "sailor", "saint", "salt",
"satire", "scale", "scuba", "season", "secure", "shake",
"shallow", "shannon", "shave", "shelf", "sherman", "shine",
"shirt", "side", "sinatra", "sincere", "size", "slalom",
"slow", "small", "snow", "sofia", "song", "sound",
"south", "speech", "spell", "spend", "spoon", "stage",
"stamp", "stand", "state", "stella", "stick", "sting",
"stock", "store", "sunday", "sunset", "support", "sweden",
"swing", "tape", "think", "thomas", "tictac", "time",
"toast", "tobacco", "tonight", "torch", "torso", "touch",
"toyota", "trade", "tribune", "trinity", "triton", "truck",
"trust", "type", "under", "unit", "urban", "urgent",
"user", "value", "vendor", "venice", "verona", "vibrate",
"virgo", "visible", "vista", "vital", "voice", "vortex",
"waiter", "watch", "wave", "weather", "wedding", "wheel",
"whiskey", "wisdom", "deal", "null", "nurse", "quebec",
"reserve", "reunion", "roof", "singer", "verbal", "amen",
"ego", "fax", "jet", "job", "rio", "ski",
"yes",
}

View File

@ -1,169 +1,4 @@
package models
import (
"context"
"fmt"
"net"
"os"
"path"
"time"
"github.com/schollz/croc/v10/src/utils"
log "github.com/schollz/logger"
)
// TCP_BUFFER_SIZE is the maximum packet size
const WEBSOCKET_BUFFER_SIZE = 1024 * 1024 * 32
const TCP_BUFFER_SIZE = 1024 * 64
// DEFAULT_RELAY is the default relay used (can be set using --relay)
var (
DEFAULT_RELAY = "croc.schollz.com"
DEFAULT_RELAY6 = "croc6.schollz.com"
DEFAULT_PORT = "9009"
DEFAULT_PASSPHRASE = "pass123"
INTERNAL_DNS = false
)
// publicDNS are servers to be queried if a local lookup fails
var publicDNS = []string{
"1.0.0.1", // Cloudflare
"1.1.1.1", // Cloudflare
"[2606:4700:4700::1111]", // Cloudflare
"[2606:4700:4700::1001]", // Cloudflare
"8.8.4.4", // Google
"8.8.8.8", // Google
"[2001:4860:4860::8844]", // Google
"[2001:4860:4860::8888]", // Google
"9.9.9.9", // Quad9
"149.112.112.112", // Quad9
"[2620:fe::fe]", // Quad9
"[2620:fe::fe:9]", // Quad9
"8.26.56.26", // Comodo
"8.20.247.20", // Comodo
"208.67.220.220", // Cisco OpenDNS
"208.67.222.222", // Cisco OpenDNS
"[2620:119:35::35]", // Cisco OpenDNS
"[2620:119:53::53]", // Cisco OpenDNS
}
func getConfigFile(requireValidPath bool) (fname string, err error) {
configFile, err := utils.GetConfigDir(requireValidPath)
if err != nil {
return
}
fname = path.Join(configFile, "internal-dns")
return
}
func init() {
log.SetLevel("info")
log.SetOutput(os.Stderr)
doRemember := false
for _, flag := range os.Args {
if flag == "--internal-dns" {
INTERNAL_DNS = true
break
}
if flag == "--remember" {
doRemember = true
}
}
if doRemember {
// save in config file
fname, err := getConfigFile(true)
if err == nil {
f, _ := os.Create(fname)
f.Close()
}
}
if !INTERNAL_DNS {
fname, err := getConfigFile(false)
if err == nil {
INTERNAL_DNS = utils.Exists(fname)
}
}
log.Trace("Using internal DNS: ", INTERNAL_DNS)
var err error
var addr string
addr, err = lookup(DEFAULT_RELAY)
if err == nil {
DEFAULT_RELAY = net.JoinHostPort(addr, DEFAULT_PORT)
} else {
DEFAULT_RELAY = ""
}
log.Tracef("Default ipv4 relay: %s", addr)
addr, err = lookup(DEFAULT_RELAY6)
if err == nil {
DEFAULT_RELAY6 = net.JoinHostPort(addr, DEFAULT_PORT)
} else {
DEFAULT_RELAY6 = ""
}
log.Tracef("Default ipv6 relay: %s", addr)
}
// Resolve a hostname to an IP address using DNS.
func lookup(address string) (ipaddress string, err error) {
if !INTERNAL_DNS {
log.Tracef("Using local DNS to resolve %s", address)
return localLookupIP(address)
}
type Result struct {
s string
err error
}
result := make(chan Result, len(publicDNS))
for _, dns := range publicDNS {
go func(dns string) {
var r Result
r.s, r.err = remoteLookupIP(address, dns)
log.Tracef("Resolved %s to %s using %s", address, r.s, dns)
result <- r
}(dns)
}
for i := 0; i < len(publicDNS); i++ {
ipaddress = (<-result).s
log.Tracef("Resolved %s to %s", address, ipaddress)
if ipaddress != "" {
return
}
}
err = fmt.Errorf("failed to resolve %s: all DNS servers exhausted", address)
return
}
// localLookupIP returns a host's IP address using the local DNS configuration.
func localLookupIP(address string) (ipaddress string, err error) {
// Create a context with a 500 millisecond timeout
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
r := &net.Resolver{}
// Use the context with timeout in the LookupHost function
ip, err := r.LookupHost(ctx, address)
if err != nil {
return
}
ipaddress = ip[0]
return
}
// remoteLookupIP returns a host's IP address based on a remote DNS server.
func remoteLookupIP(address, dns string) (ipaddress string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := new(net.Dialer)
return d.DialContext(ctx, network, dns+":53")
},
}
ip, err := r.LookupHost(ctx, address)
if err != nil {
return
}
ipaddress = ip[0]
return
}

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

@ -0,0 +1,14 @@
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
}

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

@ -0,0 +1,527 @@
package recipient
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
humanize "github.com/dustin/go-humanize"
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"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"
"github.com/tscholl2/siec"
)
var DebugLevel string
// Receive is the async operation to receive a file
func Receive(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, done chan struct{}, c *websocket.Conn, codephrase string, noPrompt bool, useStdout bool) {
logger.SetLogLevel(DebugLevel)
err := receive(forceSend, serverAddress, tcpPorts, isLocal, c, codephrase, noPrompt, useStdout)
if err != nil {
if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error())
}
}
done <- struct{}{}
}
func receive(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, c *websocket.Conn, codephrase string, noPrompt bool, useStdout bool) (err error) {
var fstats models.FileStats
var sessionKey []byte
var transferTime time.Duration
var hash256 []byte
var otherIP string
var progressFile string
var resumeFile bool
var tcpConnections []comm.Comm
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 = " performing PAKE..."
spin.Start()
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte(codephrase)
// initialize recipient Q ("1" indicates recipient)
Q, err := pake.Init(pw, 1, curve, 1*time.Millisecond)
if err != nil {
return
}
step := 0
for {
messageType, message, err := c.ReadMessage()
if err != nil {
return err
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
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:
// sender has initiated, sends their ip address
otherIP = string(message)
log.Debugf("sender IP: %s", otherIP)
// recipient begins by sending address
ip := ""
if isLocal {
ip = utils.LocalIP()
} else {
ip, _ = utils.PublicIP()
}
c.WriteMessage(websocket.BinaryMessage, []byte(ip))
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 err
}
// Q has the session key now, but we will still check if its valid
sessionKey, err = Q.SessionKey()
if err != nil {
return err
}
log.Debugf("%x\n", sessionKey)
// initialize TCP connections if using (possible, but unlikely, race condition)
go func() {
if !useWebsockets {
log.Debugf("connecting to server")
tcpConnections = make([]comm.Comm, len(tcpPorts))
for i, tcpPort := range tcpPorts {
log.Debugf("connecting to %d", i)
tcpConnections[i], err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
if err != nil {
log.Error(err)
}
}
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 {
return err
}
c.WriteMessage(websocket.BinaryMessage, []byte("ready"))
case 3:
spin.Stop()
// 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, &fstats)
if err != nil {
return err
}
log.Debugf("got file stats: %+v", fstats)
// determine if the file is resuming or not
progressFile = fmt.Sprintf("%s.progress", fstats.SentName)
overwritingOrReceiving := "Receiving"
if utils.Exists(fstats.Name) || utils.Exists(fstats.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)
c.WriteMessage(websocket.BinaryMessage, encblockBytes.Bytes())
// prompt user about the file
fileOrFolder := "file"
if fstats.IsDir {
fileOrFolder = "folder"
}
fmt.Fprintf(os.Stderr, "\r%s %s (%s) into: %s\n",
overwritingOrReceiving,
fileOrFolder,
humanize.Bytes(uint64(fstats.Size)),
fstats.Name,
)
if !noPrompt {
if "y" != utils.GetInput("ok? (y/N): ") {
fmt.Fprintf(os.Stderr, "cancelling request")
c.WriteMessage(websocket.BinaryMessage, []byte("no"))
return nil
}
}
// await file
// erase file if overwriting
if overwritingOrReceiving == "Overwriting" {
os.Remove(fstats.SentName)
}
var f *os.File
if utils.Exists(fstats.SentName) && resumeFile {
if !useWebsockets {
f, err = os.OpenFile(fstats.SentName, os.O_WRONLY, 0644)
} else {
f, err = os.OpenFile(fstats.SentName, os.O_APPEND|os.O_WRONLY, 0644)
}
if err != nil {
log.Error(err)
return err
}
} else {
f, err = os.Create(fstats.SentName)
if err != nil {
log.Error(err)
return err
}
if !useWebsockets {
if err = f.Truncate(fstats.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
bytesWritten := 0
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)...\n", otherIP)
bar := progressbar.NewOptions(
int(fstats.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(fstats.Size)),
progressbar.OptionSetWriter(os.Stderr),
)
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(fstats.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, !fstats.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 fstats.IsCompressed && !fstats.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
bar.Add(n)
if int64(bytesWritten) == fstats.Size || blocksWritten >= blocksToWrite {
log.Debug("finished", int64(bytesWritten), fstats.Size, blocksWritten, blocksToWrite)
break
}
}
finished <- true
return
}(finished, dataChan)
log.Debug("telling sender i'm ready")
c.WriteMessage(websocket.BinaryMessage, append([]byte("ready"), blocksBytes...))
startTime := time.Now()
if useWebsockets {
for {
var messageType int
// read from websockets
messageType, message, err = c.ReadMessage()
if messageType != websocket.BinaryMessage {
continue
}
if err != nil {
log.Error(err)
return err
}
if bytes.Equal(message, []byte("magic")) {
log.Debug("got magic")
break
}
dataChan <- message
// select {
// case dataChan <- message:
// default:
// log.Debug("blocked")
// // no message sent
// // block
// dataChan <- message
// }
}
} else {
_ = <-isConnectedIfUsingTCP
log.Debugf("starting listening with tcp with %d connections", len(tcpConnections))
// using TCP
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 {
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)
}
wg.Wait()
}
_ = <-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
bar.Finish()
// check hash
hash256, err = utils.HashFile(fstats.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 fstats.IsDir {
err = zipper.UnzipFile(fstats.SentName, ".")
if DebugLevel != "debug" {
os.Remove(fstats.SentName)
}
} else {
err = nil
}
if err == nil {
if useStdout && !fstats.IsDir {
var bFile []byte
bFile, err = ioutil.ReadFile(fstats.SentName)
if err != nil {
return err
}
os.Stdout.Write(bFile)
os.Remove(fstats.SentName)
}
transferRate := float64(fstats.Size) / 1000000.0 / transferTime.Seconds()
transferType := "MB/s"
if transferRate < 1 {
transferRate = float64(fstats.Size) / 1000.0 / transferTime.Seconds()
transferType = "kB/s"
}
folderOrFile := "file"
if fstats.IsDir {
folderOrFile = "folder"
}
if useStdout {
fstats.Name = "stdout"
}
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s (%2.1f %s)\n", folderOrFile, fstats.Name, transferRate, transferType)
os.Remove(progressFile)
}
return err
} else {
if DebugLevel != "debug" {
log.Debug("removing corrupted file")
os.Remove(fstats.SentName)
}
return errors.New("file corrupted")
}
default:
return fmt.Errorf("unknown step")
}
step++
}
}
func connectToTCPServer(room string, address string) (com comm.Comm, err error) {
log.Debugf("recipient connecting to %s", address)
connection, err := net.Dial("tcp", address)
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)
log.Debug("waiting for server contact")
ok, err := com.Receive()
if err != nil {
return
}
log.Debugf("[%s] server says: %s", address, ok)
err = com.Send(room)
if err != nil {
return
}
ok, err = com.Receive()
log.Debugf("[%s] server says: %s", address, ok)
if err != nil {
return
}
if ok != "recipient" {
err = errors.New(ok)
}
return
}

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

@ -0,0 +1,123 @@
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()
}

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

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

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

@ -0,0 +1,34 @@
package relay
import (
"fmt"
"net/http"
log "github.com/cihub/seelog"
"github.com/schollz/croc/src/logger"
"github.com/schollz/croc/src/tcp"
)
var DebugLevel string
// 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)
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(w, r)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
})
err = http.ListenAndServe(":"+port, nil)
return
}

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

@ -0,0 +1,491 @@
package sender
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"
"github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
"github.com/tscholl2/siec"
)
var DebugLevel string
// Send is the async call to send data
func Send(forceSend int, serverAddress string, tcpPorts []string, isLocal bool, done chan struct{}, c *websocket.Conn, fname string, codephrase string, useCompression bool, useEncryption bool) {
logger.SetLogLevel(DebugLevel)
log.Debugf("sending %s", fname)
err := send(forceSend, serverAddress, tcpPorts, isLocal, c, fname, codephrase, useCompression, useEncryption)
if err != nil {
if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error())
}
}
done <- struct{}{}
}
func 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 fstats models.FileStats
var fileHash []byte
var otherIP string
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
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte(codephrase)
// initialize sender P ("0" indicates sender)
P, err := pake.Init(pw, 0, curve, 1*time.Millisecond)
if err != nil {
return
}
step := 0
for {
messageType, message, errRead := c.ReadMessage()
if errRead != nil {
return errRead
}
if messageType == websocket.PongMessage || messageType == websocket.PingMessage {
continue
}
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:
// sender initiates communication
ip := ""
if isLocal {
ip = utils.LocalIP()
} else {
ip, _ = utils.PublicIP()
}
// send my IP address
c.WriteMessage(websocket.BinaryMessage, []byte(ip))
case 1:
// first receive the IP address from the sender
otherIP = string(message)
log.Debugf("recipient IP: %s", otherIP)
go func() {
// recipient might want file! start gathering information about file
fstat, err := os.Stat(fname)
if err != nil {
fileReady <- err
return
}
fstats = models.FileStats{
Name: filename,
Size: fstat.Size(),
ModTime: fstat.ModTime(),
IsDir: fstat.IsDir(),
SentName: fstat.Name(),
IsCompressed: useCompression,
IsEncrypted: useEncryption,
}
if fstats.IsDir {
// zip the directory
fstats.SentName, err = zipper.ZipFile(fname, true)
if err != nil {
log.Error(err)
fileReady <- err
return
}
fname = fstats.SentName
fstat, err := os.Stat(fname)
if err != nil {
fileReady <- err
return
}
// get new size
fstats.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..."
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)
if err := P.Update(message); err != nil {
return err
}
c.WriteMessage(websocket.BinaryMessage, P.Bytes())
sessionKey, _ = P.SessionKey()
// check(err)
log.Debugf("%x\n", sessionKey)
// wait for readiness
spin.Stop()
spin.Suffix = " 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")
}
// connect to TCP in background
tcpConnections = make([]comm.Comm, len(tcpPorts))
go func() {
if !useWebsockets {
log.Debugf("connecting to server")
for i, tcpPort := range tcpPorts {
log.Debugf("connecting to %s on connection %d", tcpPort, i)
tcpConnections[i], err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
if err != nil {
log.Error(err)
}
}
}
isConnectedIfUsingTCP <- true
}()
err = <-fileReady // block until file is ready
if err != nil {
return err
}
fstatsBytes, err := json.Marshal(fstats)
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 declares 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)
// start loading the file into memory
// start streaming encryption/compression
if fstats.IsDir {
// remove file if zipped
defer os.Remove(fstats.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 && !fstats.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")
}
fmt.Fprintf(os.Stderr, "\rSending (->%s)...\n", 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
}
bar := progressbar.NewOptions(
int(fstats.Size),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetBytes(int(fstats.Size)),
progressbar.OptionSetWriter(os.Stderr),
)
bar.Add(blockSize * len(blocksToSkip))
if useWebsockets {
for {
data := <-dataChan
if data.err != nil {
return data.err
}
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")
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 {
if data.err != nil {
log.Error(data.err)
return
}
bar.Add(data.bytesRead)
// write data to tcp connection
_, err = tcpConnections[i].Write(data.b)
if err != nil {
err = errors.Wrap(err, "problem writing message")
log.Error(err)
return
}
if bytes.Equal(data.b, []byte("magic")) {
log.Debugf("%d got magic", i)
return
}
}
}(i, &wg, dataChan)
}
wg.Wait()
}
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(fstats.Size) / 1000000.0 / transferTime.Seconds()
transferType := "MB/s"
if transferRate < 1 {
transferRate = float64(fstats.Size) / 1000.0 / transferTime.Seconds()
transferType = "kB/s"
}
fmt.Fprintf(os.Stderr, "\nTransfer 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, 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
}
ok, err = com.Receive()
log.Debugf("server says: %s", ok)
if err != nil {
return
}
if ok != "sender" {
err = errors.New(ok)
}
return
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,9 +0,0 @@
package tcp
import "time"
const (
DEFAULT_LOG_LEVEL = "debug"
DEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute
DEFAULT_ROOM_TTL = 3 * time.Hour
)

View File

@ -1,53 +0,0 @@
package tcp
import (
"fmt"
"time"
)
// TODO: maybe export from logger library?
var availableLogLevels = []string{"info", "error", "warn", "debug", "trace"}
type serverOptsFunc func(s *server) error
func WithBanner(banner ...string) serverOptsFunc {
return func(s *server) error {
if len(banner) > 0 {
s.banner = banner[0]
}
return nil
}
}
func WithLogLevel(level string) serverOptsFunc {
return func(s *server) error {
if !containsSlice(availableLogLevels, level) {
return fmt.Errorf("invalid log level specified: %s", level)
}
s.debugLevel = level
return nil
}
}
func WithRoomCleanupInterval(interval time.Duration) serverOptsFunc {
return func(s *server) error {
s.roomCleanupInterval = interval
return nil
}
}
func WithRoomTTL(ttl time.Duration) serverOptsFunc {
return func(s *server) error {
s.roomTTL = ttl
return nil
}
}
func containsSlice(s []string, e string) bool {
for _, ss := range s {
if e == ss {
return true
}
}
return false
}

View File

@ -1,40 +1,20 @@
package tcp
import (
"bytes"
"fmt"
"net"
"strings"
"sync"
"time"
log "github.com/schollz/logger"
"github.com/schollz/pake/v3"
"github.com/schollz/croc/v10/src/comm"
"github.com/schollz/croc/v10/src/crypt"
"github.com/schollz/croc/v10/src/models"
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"
)
type server struct {
host string
port string
debugLevel string
banner string
password string
rooms roomMap
roomCleanupInterval time.Duration
roomTTL time.Duration
stopRoomCleanup chan struct{}
}
type roomInfo struct {
first *comm.Comm
second *comm.Comm
opened time.Time
full bool
receiver comm.Comm
opened time.Time
}
type roomMap struct {
@ -42,375 +22,129 @@ type roomMap struct {
sync.Mutex
}
const pingRoom = "pinglkasjdlfjsaldjf"
// newDefaultServer initializes a new server, with some default configuration options
func newDefaultServer() *server {
s := new(server)
s.roomCleanupInterval = DEFAULT_ROOM_CLEANUP_INTERVAL
s.roomTTL = DEFAULT_ROOM_TTL
s.debugLevel = DEFAULT_LOG_LEVEL
s.stopRoomCleanup = make(chan struct{})
return s
}
// RunWithOptionsAsync asynchronously starts a TCP listener.
func RunWithOptionsAsync(host, port, password string, opts ...serverOptsFunc) error {
s := newDefaultServer()
s.host = host
s.port = port
s.password = password
for _, opt := range opts {
err := opt(s)
if err != nil {
return fmt.Errorf("could not apply optional configurations: %w", err)
}
}
return s.start()
}
var rooms roomMap
// Run starts a tcp listener, run async
func Run(debugLevel, host, port, password string, banner ...string) (err error) {
return RunWithOptionsAsync(host, port, password, WithBanner(banner...), WithLogLevel(debugLevel))
}
func Run(debugLevel, port string) {
logger.SetLogLevel(debugLevel)
rooms.Lock()
rooms.rooms = make(map[string]roomInfo)
rooms.Unlock()
func (s *server) start() (err error) {
log.SetLevel(s.debugLevel)
// 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)
}
}
rooms.Unlock()
}
}()
// Mask our password in logs
maskedPassword := ""
if len(s.password) > 2 {
maskedPassword = fmt.Sprintf("%c***%c", s.password[0], s.password[len(s.password)-1])
} else {
maskedPassword = s.password
}
log.Debugf("starting with password '%s'", maskedPassword)
s.rooms.Lock()
s.rooms.rooms = make(map[string]roomInfo)
s.rooms.Unlock()
go s.deleteOldRooms()
defer s.stopRoomDeletion()
err = s.run()
err := run(port)
if err != nil {
log.Error(err)
}
return
}
func (s *server) run() (err error) {
network := "tcp"
addr := net.JoinHostPort(s.host, s.port)
if s.host != "" {
ip := net.ParseIP(s.host)
if ip == nil {
var tcpIP *net.IPAddr
tcpIP, err = net.ResolveIPAddr("ip", s.host)
if err != nil {
return err
}
ip = tcpIP.IP
}
addr = net.JoinHostPort(ip.String(), s.port)
if s.host != "" {
if ip.To4() != nil {
network = "tcp4"
} else {
network = "tcp6"
}
}
}
addr = strings.Replace(addr, "127.0.0.1", "0.0.0.0", 1)
log.Infof("starting TCP server on " + addr)
server, err := net.Listen(network, addr)
func run(port string) (err error) {
log.Debugf("starting TCP server on " + port)
server, err := net.Listen("tcp", "0.0.0.0:"+port)
if err != nil {
return fmt.Errorf("error listening on %s: %w", addr, err)
return errors.Wrap(err, "Error listening on :"+port)
}
defer server.Close()
// spawn a new goroutine whenever a client connects
for {
connection, err := server.Accept()
if err != nil {
return fmt.Errorf("problem accepting connection: %w", err)
return errors.Wrap(err, "problem accepting connection")
}
log.Debugf("client %s connected", connection.RemoteAddr().String())
go func(port string, connection net.Conn) {
c := comm.New(connection)
room, errCommunication := s.clientCommunication(port, c)
log.Debugf("room: %+v", room)
log.Debugf("err: %+v", errCommunication)
errCommunication := clientCommuncation(port, comm.New(connection))
if errCommunication != nil {
log.Debugf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
connection.Close()
return
log.Warnf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
}
if room == pingRoom {
log.Debugf("got ping")
connection.Close()
return
}
for {
// check connection
log.Debugf("checking connection of room %s for %+v", room, c)
deleteIt := false
s.rooms.Lock()
if _, ok := s.rooms.rooms[room]; !ok {
log.Debug("room is gone")
s.rooms.Unlock()
return
}
log.Debugf("room: %+v", s.rooms.rooms[room])
if s.rooms.rooms[room].first != nil && s.rooms.rooms[room].second != nil {
log.Debug("rooms ready")
s.rooms.Unlock()
break
} else {
if s.rooms.rooms[room].first != nil {
errSend := s.rooms.rooms[room].first.Send([]byte{1})
if errSend != nil {
log.Debug(errSend)
deleteIt = true
}
}
}
s.rooms.Unlock()
if deleteIt {
s.deleteRoom(room)
break
}
time.Sleep(1 * time.Second)
}
}(s.port, connection)
}(port, connection)
}
}
// deleteOldRooms checks for rooms at a regular interval and removes those that
// have exceeded their allocated TTL.
func (s *server) deleteOldRooms() {
ticker := time.NewTicker(s.roomCleanupInterval)
for {
select {
case <-ticker.C:
var roomsToDelete []string
s.rooms.Lock()
for room := range s.rooms.rooms {
if time.Since(s.rooms.rooms[room].opened) > s.roomTTL {
roomsToDelete = append(roomsToDelete, room)
}
}
s.rooms.Unlock()
for _, room := range roomsToDelete {
s.deleteRoom(room)
log.Debugf("room cleaned up: %s", room)
}
case <-s.stopRoomCleanup:
ticker.Stop()
log.Debug("room cleanup stopped")
return
}
}
}
func (s *server) stopRoomDeletion() {
log.Debug("stop room cleanup fired")
s.stopRoomCleanup <- struct{}{}
}
var weakKey = []byte{1, 2, 3}
func (s *server) clientCommunication(port string, c *comm.Comm) (room string, err error) {
// establish secure password with PAKE for communication with relay
B, err := pake.InitCurve(weakKey, 1, "siec")
if err != nil {
return
}
Abytes, err := c.Receive()
if err != nil {
return
}
log.Debugf("Abytes: %s", Abytes)
if bytes.Equal(Abytes, []byte("ping")) {
room = pingRoom
log.Debug("sending back pong")
c.Send([]byte("pong"))
return
}
err = B.Update(Abytes)
if err != nil {
return
}
err = c.Send(B.Bytes())
if err != nil {
return
}
strongKey, err := B.SessionKey()
if err != nil {
return
}
log.Debugf("strongkey: %x", strongKey)
// receive salt
salt, err := c.Receive()
if err != nil {
return
}
strongKeyForEncryption, _, err := crypt.New(strongKey, salt)
if err != nil {
return
}
log.Debugf("waiting for password")
passwordBytesEnc, err := c.Receive()
if err != nil {
return
}
passwordBytes, err := crypt.Decrypt(passwordBytesEnc, strongKeyForEncryption)
if err != nil {
return
}
if strings.TrimSpace(string(passwordBytes)) != s.password {
err = fmt.Errorf("bad password")
enc, _ := crypt.Encrypt([]byte(err.Error()), strongKeyForEncryption)
if err = c.Send(enc); err != nil {
return "", fmt.Errorf("send error: %w", err)
}
return
}
func clientCommuncation(port string, c comm.Comm) (err error) {
// send ok to tell client they are connected
banner := s.banner
if len(banner) == 0 {
banner = "ok"
}
log.Debugf("sending '%s'", banner)
bSend, err := crypt.Encrypt([]byte(banner+"|||"+c.Connection().RemoteAddr().String()), strongKeyForEncryption)
if err != nil {
return
}
err = c.Send(bSend)
log.Debug("sending ok")
err = c.Send("ok")
if err != nil {
return
}
// wait for client to tell me which room they want
log.Debug("waiting for answer")
enc, err := c.Receive()
room, err := c.Receive()
if err != nil {
return
}
roomBytes, err := crypt.Decrypt(enc, strongKeyForEncryption)
if err != nil {
return
}
room = string(roomBytes)
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.Lock()
// first connection is always the receiver
if _, ok := rooms.rooms[room]; !ok {
rooms.rooms[room] = roomInfo{
receiver: c,
opened: time.Now(),
}
s.rooms.Unlock()
rooms.Unlock()
// tell the client that they got the room
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
if err != nil {
return
}
err = c.Send(bSend)
if err != nil {
log.Error(err)
s.deleteRoom(room)
return
}
log.Debugf("room %s has 1", room)
return
}
if s.rooms.rooms[room].full {
s.rooms.Unlock()
bSend, err = crypt.Encrypt([]byte("room full"), strongKeyForEncryption)
if err != nil {
return
}
err = c.Send(bSend)
err = c.Send("recipient")
if err != nil {
log.Error(err)
return
}
return
log.Debug("recipient connected")
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()
log.Debug("sender connected")
receiver := rooms.rooms[room].receiver
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")
}(otherConnection, c, &wg)
}(c, receiver, &wg)
// tell the sender everything is ready
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
err = c.Send("sender")
if err != nil {
return
}
err = c.Send(bSend)
if err != nil {
s.deleteRoom(room)
return
}
wg.Wait()
// delete room
s.deleteRoom(room)
return
}
func (s *server) deleteRoom(room string) {
s.rooms.Lock()
defer s.rooms.Unlock()
if _, ok := s.rooms.rooms[room]; !ok {
return
}
rooms.Lock()
log.Debugf("deleting room: %s", room)
if s.rooms.rooms[room].first != nil {
s.rooms.rooms[room].first.Close()
}
if s.rooms.rooms[room].second != nil {
s.rooms.rooms[room].second.Close()
}
s.rooms.rooms[room] = roomInfo{first: nil, second: nil}
delete(s.rooms.rooms, room)
delete(rooms.rooms, room)
rooms.Unlock()
return nil
}
// chanFromConn creates a channel from a Conn object, and sends everything it
//
// Read()s from the socket to the channel.
// Read()s from the socket to the channel.
func chanFromConn(conn net.Conn) chan []byte {
c := make(chan []byte, 1)
if err := conn.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
log.Warnf("can't set read deadline: %v", err)
}
c := make(chan []byte)
go func() {
b := make([]byte, models.TCP_BUFFER_SIZE)
for {
n, err := conn.Read(b)
if n > 0 {
@ -425,7 +159,6 @@ func chanFromConn(conn net.Conn) chan []byte {
break
}
}
log.Debug("exiting")
}()
return c
@ -443,153 +176,13 @@ func pipe(conn1 net.Conn, conn2 net.Conn) {
if b1 == nil {
return
}
if _, err := conn2.Write(b1); err != nil {
log.Errorf("write error on channel 1: %v", err)
}
conn2.Write(b1)
case b2 := <-chan2:
if b2 == nil {
return
}
if _, err := conn1.Write(b2); err != nil {
log.Errorf("write error on channel 2: %v", err)
}
conn1.Write(b2)
}
}
}
func PingServer(address string) (err error) {
log.Debugf("pinging %s", address)
c, err := comm.NewConnection(address, 300*time.Millisecond)
if err != nil {
log.Debug(err)
return
}
err = c.Send([]byte("ping"))
if err != nil {
log.Debug(err)
return
}
b, err := c.Receive()
if err != nil {
log.Debug(err)
return
}
if bytes.Equal(b, []byte("pong")) {
return nil
}
return fmt.Errorf("no pong")
}
// ConnectToTCPServer will initiate a new connection
// to the specified address, room with optional time limit
func ConnectToTCPServer(address, password, room string, timelimit ...time.Duration) (c *comm.Comm, banner string, ipaddr string, err error) {
if len(timelimit) > 0 {
c, err = comm.NewConnection(address, timelimit[0])
} else {
c, err = comm.NewConnection(address)
}
if err != nil {
log.Debug(err)
return
}
// get PAKE connection with server to establish strong key to transfer info
A, err := pake.InitCurve(weakKey, 0, "siec")
if err != nil {
log.Debug(err)
return
}
err = c.Send(A.Bytes())
if err != nil {
log.Debug(err)
return
}
Bbytes, err := c.Receive()
if err != nil {
log.Debug(err)
return
}
err = A.Update(Bbytes)
if err != nil {
log.Debug(err)
return
}
strongKey, err := A.SessionKey()
if err != nil {
log.Debug(err)
return
}
log.Debugf("strong key: %x", strongKey)
strongKeyForEncryption, salt, err := crypt.New(strongKey, nil)
if err != nil {
log.Debug(err)
return
}
// send salt
err = c.Send(salt)
if err != nil {
log.Debug(err)
return
}
log.Debug("sending password")
bSend, err := crypt.Encrypt([]byte(password), strongKeyForEncryption)
if err != nil {
log.Debug(err)
return
}
err = c.Send(bSend)
if err != nil {
log.Debug(err)
return
}
log.Debug("waiting for first ok")
enc, err := c.Receive()
if err != nil {
log.Debug(err)
return
}
data, err := crypt.Decrypt(enc, strongKeyForEncryption)
if err != nil {
log.Debug(err)
return
}
if !strings.Contains(string(data), "|||") {
err = fmt.Errorf("bad response: %s", string(data))
log.Debug(err)
return
}
banner = strings.Split(string(data), "|||")[0]
ipaddr = strings.Split(string(data), "|||")[1]
log.Debugf("sending room; %s", room)
bSend, err = crypt.Encrypt([]byte(room), strongKeyForEncryption)
if err != nil {
log.Debug(err)
return
}
err = c.Send(bSend)
if err != nil {
log.Debug(err)
return
}
log.Debug("waiting for room confirmation")
enc, err = c.Receive()
if err != nil {
log.Debug(err)
return
}
data, err = crypt.Decrypt(enc, strongKeyForEncryption)
if err != nil {
log.Debug(err)
return
}
if !bytes.Equal(data, []byte("ok")) {
err = fmt.Errorf("got bad response: %s", data)
log.Debug(err)
return
}
log.Debug("all set")
return
}

View File

@ -1,71 +0,0 @@
package tcp
import (
"bytes"
"fmt"
"testing"
"time"
log "github.com/schollz/logger"
"github.com/stretchr/testify/assert"
)
func BenchmarkConnection(b *testing.B) {
log.SetLevel("trace")
go Run("debug", "127.0.0.1", "8283", "pass123", "8284")
time.Sleep(100 * time.Millisecond)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c, _, _, _ := ConnectToTCPServer("127.0.0.1:8283", "pass123", fmt.Sprintf("testroom%d", i), 1*time.Minute)
c.Close()
}
}
func TestTCP(t *testing.T) {
log.SetLevel("error")
timeToRoomDeletion := 100 * time.Millisecond
go RunWithOptionsAsync("127.0.0.1", "8381", "pass123", WithBanner("8382"), WithLogLevel("debug"), WithRoomTTL(timeToRoomDeletion))
time.Sleep(timeToRoomDeletion)
err := PingServer("127.0.0.1:8381")
assert.Nil(t, err)
err = PingServer("127.0.0.1:8333")
assert.NotNil(t, err)
time.Sleep(timeToRoomDeletion)
c1, banner, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Minute)
assert.Equal(t, banner, "8382")
assert.Nil(t, err)
c2, _, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
assert.Nil(t, err)
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
assert.NotNil(t, err)
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Nanosecond)
assert.NotNil(t, err)
// try sending data
assert.Nil(t, c1.Send([]byte("hello, c2")))
var data []byte
for {
data, err = c2.Receive()
if bytes.Equal(data, []byte{1}) {
continue
}
break
}
assert.Nil(t, err)
assert.Equal(t, []byte("hello, c2"), data)
assert.Nil(t, c2.Send([]byte("hello, c1")))
for {
data, err = c1.Receive()
if bytes.Equal(data, []byte{1}) {
continue
}
break
}
assert.Nil(t, err)
assert.Equal(t, []byte("hello, c1"), data)
c1.Close()
time.Sleep(300 * time.Millisecond)
}

13
src/utils/exists.go Normal file
View File

@ -0,0 +1,13 @@
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
}

15
src/utils/getinput.go Normal file
View File

@ -0,0 +1,15 @@
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)
}

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

@ -0,0 +1,33 @@
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))
}

39
src/utils/ip.go Normal file
View File

@ -0,0 +1,39 @@
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()
}

11
src/utils/ip_test.go Normal file
View File

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

16
src/utils/randomname.go Normal file
View File

@ -0,0 +1,16 @@
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, "-")
}

View File

@ -1,637 +0,0 @@
package utils
import (
"archive/zip"
"bufio"
"bytes"
"compress/flate"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math"
"math/big"
"net"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"unicode"
"github.com/cespare/xxhash"
"github.com/kalafut/imohash"
"github.com/minio/highwayhash"
"github.com/schollz/croc/v10/src/mnemonicode"
log "github.com/schollz/logger"
"github.com/schollz/progressbar/v3"
)
const NbPinNumbers = 4
const NbBytesWords = 4
// Get or create home directory
func GetConfigDir(requireValidPath bool) (homedir string, err error) {
if envHomedir, isSet := os.LookupEnv("CROC_CONFIG_DIR"); isSet {
homedir = envHomedir
} else if xdgConfigHome, isSet := os.LookupEnv("XDG_CONFIG_HOME"); isSet {
homedir = path.Join(xdgConfigHome, "croc")
} else {
homedir, err = os.UserHomeDir()
if err != nil {
if !requireValidPath {
err = nil
homedir = ""
}
return
}
homedir = path.Join(homedir, ".config", "croc")
}
if requireValidPath {
if _, err = os.Stat(homedir); os.IsNotExist(err) {
err = os.MkdirAll(homedir, 0o700)
}
}
return
}
// 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 hash of a file or, in case of a symlink, the
// SHA256 hash of its target. Takes an argument to specify the algorithm to use.
func HashFile(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {
doShowProgress := false
if len(showProgress) > 0 {
doShowProgress = showProgress[0]
}
var fstats os.FileInfo
fstats, err = os.Lstat(fname)
if err != nil {
return nil, err
}
if fstats.Mode()&os.ModeSymlink != 0 {
var target string
target, err = os.Readlink(fname)
if err != nil {
return nil, err
}
return []byte(SHA256(target)), nil
}
switch algorithm {
case "imohash":
return IMOHashFile(fname)
case "md5":
return MD5HashFile(fname, doShowProgress)
case "xxhash":
return XXHashFile(fname, doShowProgress)
case "highway":
return HighwayHashFile(fname, doShowProgress)
}
err = fmt.Errorf("unspecified algorithm")
return
}
// HighwayHashFile returns highwayhash of a file
func HighwayHashFile(fname string, doShowProgress bool) (hashHighway []byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
key, err := hex.DecodeString("1553c5383fb0b86578c3310da665b4f6e0521acf22eb58a99532ffed02a6b115")
if err != nil {
return
}
h, err := highwayhash.New(key)
if err != nil {
err = fmt.Errorf("could not create highwayhash: %s", err.Error())
return
}
if doShowProgress {
stat, _ := f.Stat()
fnameShort := path.Base(fname)
if len(fnameShort) > 20 {
fnameShort = fnameShort[:20] + "..."
}
bar := progressbar.NewOptions64(stat.Size(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowBytes(true),
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
progressbar.OptionClearOnFinish(),
)
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
return
}
} else {
if _, err = io.Copy(h, f); err != nil {
return
}
}
hashHighway = h.Sum(nil)
return
}
// MD5HashFile returns MD5 hash
func MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := md5.New()
if doShowProgress {
stat, _ := f.Stat()
fnameShort := path.Base(fname)
if len(fnameShort) > 20 {
fnameShort = fnameShort[:20] + "..."
}
bar := progressbar.NewOptions64(stat.Size(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowBytes(true),
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
progressbar.OptionClearOnFinish(),
)
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
return
}
} else {
if _, err = io.Copy(h, f); err != nil {
return
}
}
hash256 = h.Sum(nil)
return
}
var imofull = imohash.NewCustom(0, 0)
var imopartial = imohash.NewCustom(16*16*8*1024, 128*1024)
// IMOHashFile returns imohash
func IMOHashFile(fname string) (hash []byte, err error) {
b, err := imopartial.SumFile(fname)
hash = b[:]
return
}
// IMOHashFileFull returns imohash of full file
func IMOHashFileFull(fname string) (hash []byte, err error) {
b, err := imofull.SumFile(fname)
hash = b[:]
return
}
// XXHashFile returns the xxhash of a file
func XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := xxhash.New()
if doShowProgress {
stat, _ := f.Stat()
fnameShort := path.Base(fname)
if len(fnameShort) > 20 {
fnameShort = fnameShort[:20] + "..."
}
bar := progressbar.NewOptions64(stat.Size(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowBytes(true),
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
progressbar.OptionClearOnFinish(),
)
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
return
}
} else {
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 hex.EncodeToString(sha.Sum(nil))
}
// PublicIP returns public ip address
func PublicIP() (ip string, err error) {
// ask ipv4.icanhazip.com for the public ip
// by making http request
// if the request fails, return nothing
resp, err := http.Get("http://ipv4.icanhazip.com")
if err != nil {
return
}
defer resp.Body.Close()
// read the body of the response
// and return the ip address
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
ip = strings.TrimSpace(buf.String())
return
}
// LocalIP returns local ip address
func LocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Error(err)
return ""
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
func GenerateRandomPin() string {
s := ""
max := new(big.Int)
max.SetInt64(9)
for i := 0; i < NbPinNumbers; i++ {
v, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
s += fmt.Sprintf("%d", v)
}
return s
}
// GetRandomName returns mnemonicoded random name
func GetRandomName() string {
var result []string
bs := make([]byte, NbBytesWords)
rand.Read(bs)
result = mnemonicode.EncodeWordList(result, bs)
return GenerateRandomPin() + "-" + strings.Join(result, "-")
}
// ByteCountDecimal converts bytes to human readable byte string
func ByteCountDecimal(b int64) string {
const unit = 1024
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) (chunkRanges []int64) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
fstat, err := os.Stat(fname)
if err != nil || fstat.Size() != fsize {
return
}
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 {
chunkRanges = []int64{}
} else {
chunks = chunks[:chunkNum]
chunkRanges = []int64{int64(chunkSize), chunks[0]}
curCount := 0
for i, chunk := range chunks {
if i == 0 {
continue
}
curCount++
if chunk-chunks[i-1] > int64(chunkSize) {
chunkRanges = append(chunkRanges, int64(curCount))
chunkRanges = append(chunkRanges, chunk)
curCount = 0
}
}
chunkRanges = append(chunkRanges, int64(curCount+1))
}
return
}
// ChunkRangesToChunks converts chunk ranges to list
func ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {
if len(chunkRanges) == 0 {
return
}
chunkSize := chunkRanges[0]
chunks = []int64{}
for i := 1; i < len(chunkRanges); i += 2 {
for j := int64(0); j < (chunkRanges[i+1]); j++ {
chunks = append(chunks, chunkRanges[i]+j*chunkSize)
}
}
return
}
// GetLocalIPs returns all local ips
func GetLocalIPs() (ips []string, err error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return
}
ips = []string{}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ips = append(ips, ipnet.IP.String())
}
}
}
return
}
func RandomFileName() (fname string, err error) {
f, err := os.CreateTemp(".", "croc-stdin-")
if err != nil {
return
}
fname = f.Name()
_ = f.Close()
return
}
func FindOpenPorts(host string, portNumStart, numPorts int) (openPorts []int) {
openPorts = []int{}
for port := portNumStart; port-portNumStart < 200; port++ {
timeout := 100 * time.Millisecond
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, fmt.Sprint(port)), timeout)
if conn != nil {
conn.Close()
} else if err != nil {
openPorts = append(openPorts, port)
}
if len(openPorts) >= numPorts {
return
}
}
return
}
// local ip determination
// https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space
var privateIPBlocks []*net.IPNet
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
} {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
}
privateIPBlocks = append(privateIPBlocks, block)
}
}
func IsLocalIP(ipaddress string) bool {
if strings.Contains(ipaddress, "127.0.0.1") {
return true
}
host, _, _ := net.SplitHostPort(ipaddress)
ip := net.ParseIP(host)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}
func ZipDirectory(destination string, source string) (err error) {
if _, err = os.Stat(destination); err == nil {
log.Errorf("%s file already exists!\n", destination)
}
fmt.Fprintf(os.Stderr, "Zipping %s to %s\n", source, destination)
file, err := os.Create(destination)
if err != nil {
log.Error(err)
}
defer file.Close()
writer := zip.NewWriter(file)
// no compression because croc does its compression on the fly
writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, flate.NoCompression)
})
defer writer.Close()
err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Error(err)
}
if info.Mode().IsRegular() {
f1, err := os.Open(path)
if err != nil {
log.Error(err)
}
defer f1.Close()
zipPath := strings.ReplaceAll(path, source, strings.TrimSuffix(destination, ".zip"))
zipPath = filepath.ToSlash(zipPath)
w1, err := writer.Create(zipPath)
if err != nil {
log.Error(err)
}
if _, err := io.Copy(w1, f1); err != nil {
log.Error(err)
}
fmt.Fprintf(os.Stderr, "\r\033[2K")
fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath)
}
return nil
})
if err != nil {
log.Error(err)
}
fmt.Fprintf(os.Stderr, "\n")
return nil
}
func UnzipDirectory(destination string, source string) error {
archive, err := zip.OpenReader(source)
if err != nil {
log.Error(err)
}
defer archive.Close()
for _, f := range archive.File {
filePath := filepath.Join(destination, f.Name)
fmt.Fprintf(os.Stderr, "\r\033[2K")
fmt.Fprintf(os.Stderr, "\rUnzipping file %s", filePath)
// Issue #593 conceal path traversal vulnerability
// make sure the filepath does not have ".."
filePath = filepath.Clean(filePath)
if strings.Contains(filePath, "..") {
log.Errorf("Invalid file path %s\n", filePath)
}
if f.FileInfo().IsDir() {
os.MkdirAll(filePath, os.ModePerm)
continue
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
log.Error(err)
}
// check if file exists
if _, err := os.Stat(filePath); err == nil {
prompt := fmt.Sprintf("\nOverwrite '%s'? (y/N) ", filePath)
choice := strings.ToLower(GetInput(prompt))
if choice != "y" && choice != "yes" {
fmt.Fprintf(os.Stderr, "Skipping '%s'\n", filePath)
continue
}
}
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
log.Error(err)
}
fileInArchive, err := f.Open()
if err != nil {
log.Error(err)
}
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
log.Error(err)
}
dstFile.Close()
fileInArchive.Close()
}
fmt.Fprintf(os.Stderr, "\n")
return nil
}
// ValidFileName checks if a filename is valid
// by making sure it has no invisible characters
func ValidFileName(fname string) (err error) {
// make sure it doesn't contain unicode or invisible characters
for _, r := range fname {
if !unicode.IsGraphic(r) {
err = fmt.Errorf("non-graphical unicode: %x U+%d in '%x'", string(r), r, fname)
return
}
if !unicode.IsPrint(r) {
err = fmt.Errorf("non-printable unicode: %x U+%d in '%x'", string(r), r, fname)
return
}
}
// make sure basename does not include ".." or path separators
_, basename := filepath.Split(fname)
if strings.Contains(basename, "..") {
err = fmt.Errorf("basename cannot contain '..': '%s'", basename)
return
}
if strings.Contains(basename, string(os.PathSeparator)) {
err = fmt.Errorf("basename cannot contain path separators: '%s'", basename)
return
}
// make sure the filename is not an absolute path
if filepath.IsAbs(fname) {
err = fmt.Errorf("filename cannot be an absolute path: '%s'", fname)
return
}
return
}
const crocRemovalFile = "croc-marked-files.txt"
func MarkFileForRemoval(fname string) {
// append the fname to the list of files to remove
f, err := os.OpenFile(crocRemovalFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
log.Debug(err)
return
}
defer f.Close()
_, err = f.WriteString(fname + "\n")
}
func RemoveMarkedFiles() (err error) {
// read the file and remove all the files
f, err := os.Open(crocRemovalFile)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fname := scanner.Text()
err = os.Remove(fname)
if err == nil {
log.Tracef("Removed %s", fname)
}
}
os.Remove(crocRemovalFile)
return
}

View File

@ -1,267 +0,0 @@
package utils
import (
"bytes"
"fmt"
"log"
"math/rand"
"os"
"path"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const TCP_BUFFER_SIZE = 1024 * 64
var bigFileSize = 75000000
func bigFile() {
os.WriteFile("bigfile.test", bytes.Repeat([]byte("z"), bigFileSize), 0o666)
}
func BenchmarkMD5(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
MD5HashFile("bigfile.test", false)
}
}
func BenchmarkXXHash(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFile("bigfile.test", false)
}
}
func BenchmarkImoHash(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
IMOHashFile("bigfile.test")
}
}
func BenchmarkHighwayHash(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
HighwayHashFile("bigfile.test", false)
}
}
func BenchmarkImoHashFull(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
IMOHashFileFull("bigfile.test")
}
}
func BenchmarkSha256(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
SHA256("hello,world")
}
}
func BenchmarkMissingChunks(b *testing.B) {
bigFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
MissingChunks("bigfile.test", int64(bigFileSize), TCP_BUFFER_SIZE/2)
}
}
func TestExists(t *testing.T) {
bigFile()
defer os.Remove("bigfile.test")
fmt.Println(GetLocalIPs())
assert.True(t, Exists("bigfile.test"))
assert.False(t, Exists("doesnotexist"))
}
func TestMD5HashFile(t *testing.T) {
bigFile()
defer os.Remove("bigfile.test")
b, err := MD5HashFile("bigfile.test", false)
assert.Nil(t, err)
assert.Equal(t, "8304ff018e02baad0e3555bade29a405", fmt.Sprintf("%x", b))
_, err = MD5HashFile("bigfile.test.nofile", false)
assert.NotNil(t, err)
}
func TestHighwayHashFile(t *testing.T) {
bigFile()
defer os.Remove("bigfile.test")
b, err := HighwayHashFile("bigfile.test", false)
assert.Nil(t, err)
assert.Equal(t, "3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329", fmt.Sprintf("%x", b))
_, err = HighwayHashFile("bigfile.test.nofile", false)
assert.NotNil(t, err)
}
func TestIMOHashFile(t *testing.T) {
bigFile()
defer os.Remove("bigfile.test")
b, err := IMOHashFile("bigfile.test")
assert.Nil(t, err)
assert.Equal(t, "c0d1e12301e6c635f6d4a8ea5c897437", fmt.Sprintf("%x", b))
}
func TestXXHashFile(t *testing.T) {
bigFile()
defer os.Remove("bigfile.test")
b, err := XXHashFile("bigfile.test", false)
assert.Nil(t, err)
assert.Equal(t, "4918740eb5ccb6f7", fmt.Sprintf("%x", b))
_, err = XXHashFile("nofile", false)
assert.NotNil(t, err)
}
func TestSHA256(t *testing.T) {
assert.Equal(t, "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b", SHA256("hello, world"))
}
func TestByteCountDecimal(t *testing.T) {
assert.Equal(t, "10.0 kB", ByteCountDecimal(10240))
assert.Equal(t, "50 B", ByteCountDecimal(50))
assert.Equal(t, "12.4 MB", ByteCountDecimal(13002343))
}
func TestMissingChunks(t *testing.T) {
fileSize := 100
chunkSize := 10
rand.Seed(1)
bigBuff := make([]byte, fileSize)
rand.Read(bigBuff)
os.WriteFile("missing.test", bigBuff, 0o644)
empty := make([]byte, chunkSize)
f, err := os.OpenFile("missing.test", os.O_RDWR, 0o644)
assert.Nil(t, err)
for block := 0; block < fileSize/chunkSize; block++ {
if block == 0 || block == 4 || block == 5 || block >= 7 {
f.WriteAt(empty, int64(block*chunkSize))
}
}
f.Close()
chunkRanges := MissingChunks("missing.test", int64(fileSize), chunkSize)
assert.Equal(t, []int64{10, 0, 1, 40, 2, 70, 3}, chunkRanges)
chunks := ChunkRangesToChunks(chunkRanges)
assert.Equal(t, []int64{0, 40, 50, 70, 80, 90}, chunks)
os.Remove("missing.test")
content := []byte("temporary file's content")
tmpfile, err := os.CreateTemp("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
panic(err)
}
if err := tmpfile.Close(); err != nil {
panic(err)
}
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)), chunkSize)
assert.Empty(t, chunkRanges)
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)+10), chunkSize)
assert.Empty(t, chunkRanges)
chunkRanges = MissingChunks(tmpfile.Name()+"ok", int64(len(content)), chunkSize)
assert.Empty(t, chunkRanges)
chunks = ChunkRangesToChunks(chunkRanges)
assert.Empty(t, chunks)
}
// func Test1(t *testing.T) {
// chunkRanges := MissingChunks("../../m/bigfile.test", int64(75000000), 1024*64/2)
// fmt.Println(chunkRanges)
// fmt.Println(ChunkRangesToChunks((chunkRanges)))
// assert.Nil(t, nil)
// }
func TestHashFile(t *testing.T) {
content := []byte("temporary file's content")
tmpfile, err := os.CreateTemp("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err = tmpfile.Write(content); err != nil {
panic(err)
}
if err = tmpfile.Close(); err != nil {
panic(err)
}
hashed, err := HashFile(tmpfile.Name(), "xxhash")
assert.Nil(t, err)
assert.Equal(t, "e66c561610ad51e2", fmt.Sprintf("%x", hashed))
}
func TestPublicIP(t *testing.T) {
ip, err := PublicIP()
fmt.Println(ip)
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
assert.Nil(t, err)
}
func TestLocalIP(t *testing.T) {
ip := LocalIP()
fmt.Println(ip)
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
}
func TestGetRandomName(t *testing.T) {
name := GetRandomName()
fmt.Println(name)
assert.NotEmpty(t, name)
}
func intSliceSame(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
func TestFindOpenPorts(t *testing.T) {
openPorts := FindOpenPorts("127.0.0.1", 9009, 4)
if !intSliceSame(openPorts, []int{9009, 9010, 9011, 9012}) && !intSliceSame(openPorts, []int{9014, 9015, 9016, 9017}) {
t.Errorf("openPorts: %v", openPorts)
}
}
func TestIsLocalIP(t *testing.T) {
assert.True(t, IsLocalIP("192.168.0.14:9009"))
}
func TestValidFileName(t *testing.T) {
// contains regular characters
assert.Nil(t, ValidFileName("中文.csl"))
// contains regular characters
assert.Nil(t, ValidFileName("[something].csl"))
// contains regular characters
assert.Nil(t, ValidFileName("[(something)].csl"))
// contains invisible character
err := ValidFileName("D中文.cslouglas")
assert.NotNil(t, err)
assert.Equal(t, "non-graphical unicode: e2808b U+8203 in '44e4b8ade696872e63736c6f75676c6173e2808b'", err.Error())
assert.NotNil(t, ValidFileName("hi..txt"))
assert.NotNil(t, ValidFileName(path.Join(string(os.PathSeparator), "abs", string(os.PathSeparator), "hi.txt")))
}

196
src/zipper/zip.go Normal file
View File

@ -0,0 +1,196 @@
package zipper
import (
"archive/zip"
"compress/flate"
"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
}
// ZipFile will zip the folder
func ZipFile(fname string, compress bool) (writtenFilename string, err error) {
logger.SetLogLevel(DebugLevel)
log.Debugf("zipping %s with compression? %v", fname, compress)
// get absolute filename
fname, err = filepath.Abs(fname)
if err != nil {
log.Error(err)
return
}
// get path to file and the filename
fpath, fname := filepath.Split(fname)
writtenFilename = fname + ".croc.zip"
log.Debugf("creating file: %s", writtenFilename)
f, err := os.Create(writtenFilename)
if err != nil {
log.Error(err)
return
}
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()
// Get the file information for the target
log.Debugf("checking %s", path.Join(fpath, fname))
ftarget, err := os.Open(path.Join(fpath, fname))
if err != nil {
log.Error(err)
return
}
defer ftarget.Close()
info, err := ftarget.Stat()
if err != nil {
log.Error(err)
return
}
// write header informaiton
header, err := zip.FileInfoHeader(info)
if err != nil {
log.Error(err)
return
}
var writer io.Writer
if info.IsDir() {
baseDir := path.Join(fpath, fname)
log.Debugf("walking base dir: %s", baseDir)
filepath.Walk(baseDir, func(curpath string, info os.FileInfo, err error) error {
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))
}
log.Debug(header.Name)
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
header.Name = filepath.ToSlash(header.Name)
writer, err = zipWriter.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(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 = io.Copy(writer, ftarget)
if err != nil {
log.Error(err)
return
}
}
log.Debugf("wrote zip file to %s", writtenFilename)
return
}

29
src/zipper/zip_test.go Normal file
View File

@ -0,0 +1,29 @@
package zipper
import (
"os"
"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"
writtenFilename, err := ZipFile("../croc", true)
assert.Nil(t, err)
defer os.Remove(writtenFilename)
err = UnzipFile(writtenFilename, ".")
assert.Nil(t, err)
assert.True(t, utils.Exists("croc"))
writtenFilename, err = ZipFile("../../README.md", false)
assert.Nil(t, err)
defer os.Remove(writtenFilename)
err = UnzipFile(writtenFilename, ".")
assert.Nil(t, err)
assert.True(t, utils.Exists("README.md"))
}