Showing preview only (353K chars total). Download the full file or copy to clipboard to get everything.
Repository: schollz/croc
Branch: main
Commit: 9a3d42ae7d4e
Files: 55
Total size: 335.9 KB
Directory structure:
gitextract_w4v75_0t/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ ├── release.yml
│ ├── stale.yml
│ └── winget.yml
├── .gitignore
├── .goreleaser.yml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── croc-entrypoint.sh
├── croc.service
├── go.mod
├── go.sum
├── main.go
└── src/
├── cli/
│ └── cli.go
├── comm/
│ ├── comm.go
│ └── comm_test.go
├── compress/
│ ├── compress.go
│ └── compress_test.go
├── croc/
│ ├── croc.go
│ ├── croc_test.go
│ └── ctx.go
├── crypt/
│ ├── crypt.go
│ └── crypt_test.go
├── diskusage/
│ ├── diskusage.go
│ ├── diskusage_test.go
│ └── diskusage_windows.go
├── install/
│ ├── Makefile
│ ├── bash_autocomplete
│ ├── default.txt
│ ├── prepare-sources-tarball.sh
│ ├── updateversion.go
│ ├── upload-src-tarball.sh
│ └── zsh_autocomplete
├── message/
│ ├── message.go
│ └── message_test.go
├── mnemonicode/
│ ├── mnemonicode.go
│ ├── mnemonicode_test.go
│ └── wordlist.go
├── models/
│ ├── constants.go
│ └── models_test.go
├── tcp/
│ ├── ctx.go
│ ├── defaults.go
│ ├── options.go
│ ├── tcp.go
│ └── tcp_test.go
└── utils/
├── ctx.go
├── utils.go
└── utils_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: schollz
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report to help us improve croc
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide as much detail as possible to help us reproduce and fix the issue.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: What did you expect to happen?
description: A clear and concise description of what you expected to happen.
placeholder: Tell us what you expected!
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: input
id: version
attributes:
label: croc version
description: What version of croc are you running?
placeholder: Run `croc --version` and paste the output here
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you using?
options:
- Linux
- macOS
- Windows
- Other (please specify in additional context)
validations:
required: true
- type: input
id: os-version
attributes:
label: OS Version
description: What version of your operating system are you using?
placeholder: e.g., Ubuntu 22.04, macOS 14.0, Windows 11
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output. You can enable debug logging with `croc --debug`
This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here, such as screenshots, configuration files, or anything else that might help us understand the issue.
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
unit-tests:
name: Go unit tests
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '^1.26.0'
- name: Display Go version
run: go version
- name: Run unit tests
run: go test -v ./...
- name: Build files
run: |
go version
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc-windows-amd64.exe
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc-windows-386.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-windows-arm.exe
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc-windows-arm64.exe
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc-linux-amd64
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc-linux-386
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-linux-arm
GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-linux-arm5
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc-linux-arm64
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags '-extldflags "-static"' -o croc-linux-riscv64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc-darwin-amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc-darwin-arm64
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc-freebsd-amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc-freebsd-arm64
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc-openbsd-amd64
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc-openbsd-arm64
- name: Check static build of the linux version
run: |
if ldd croc-linux-amd64 2>&1 | grep -q "not a dynamic executable"; then
echo "Static build confirmed."
else
echo "Error: croc-linux-amd64 is a dynamic executable."
exit 1
fi
================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy Docker
on:
release:
types: [created]
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: schollz/croc
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: Setup QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
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.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
release:
types: [created]
workflow_dispatch:
permissions:
contents: write
jobs:
prepare-source:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '^1.24.0'
- 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: Upload source artifact
uses: actions/upload-artifact@v7
with:
name: source-tarball
path: "*.tar.gz"
build:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
# Windows builds
- goos: windows
goarch: amd64
name: Windows-64bit
ext: .exe
archive: zip
- goos: windows
goarch: "386"
name: Windows-32bit
ext: .exe
archive: zip
- goos: windows
goarch: arm
name: Windows-ARM
ext: .exe
archive: zip
- goos: windows
goarch: arm64
name: Windows-ARM64
ext: .exe
archive: zip
# Linux builds
- goos: linux
goarch: amd64
name: Linux-64bit
ext: ""
archive: tar.gz
- goos: linux
goarch: "386"
name: Linux-32bit
ext: ""
archive: tar.gz
- goos: linux
goarch: arm
name: Linux-ARM
ext: ""
archive: tar.gz
- goos: linux
goarch: arm
goarm: "5"
name: Linux-ARMv5
ext: ""
archive: tar.gz
- goos: linux
goarch: arm64
name: Linux-ARM64
ext: ""
archive: tar.gz
- goos: linux
goarch: riscv64
name: Linux-RISCV64
ext: ""
archive: tar.gz
# macOS builds
- goos: darwin
goarch: amd64
name: macOS-64bit
ext: ""
archive: tar.gz
- goos: darwin
goarch: arm64
name: macOS-ARM64
ext: ""
archive: tar.gz
# BSD builds
- goos: dragonfly
goarch: amd64
name: DragonFlyBSD-64bit
ext: ""
archive: tar.gz
- goos: freebsd
goarch: amd64
name: FreeBSD-64bit
ext: ""
archive: tar.gz
- goos: freebsd
goarch: arm64
name: FreeBSD-ARM64
ext: ""
archive: tar.gz
- goos: netbsd
goarch: "386"
name: NetBSD-32bit
ext: ""
archive: tar.gz
- goos: netbsd
goarch: amd64
name: NetBSD-64bit
ext: ""
archive: tar.gz
- goos: netbsd
goarch: arm64
name: NetBSD-ARM64
ext: ""
archive: tar.gz
- goos: openbsd
goarch: amd64
name: OpenBSD-64bit
ext: ""
archive: tar.gz
- goos: openbsd
goarch: arm64
name: OpenBSD-ARM64
ext: ""
archive: tar.gz
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '^1.24.0'
- name: Build binary
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
run: |
# Set LDFLAGS based on platform
case "${{ matrix.goos }}" in
"darwin")
LDFLAGS='-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"'
;;
"dragonfly"|"freebsd"|"netbsd"|"openbsd")
LDFLAGS=""
;;
*)
LDFLAGS='-extldflags "-static"'
;;
esac
echo "Building for ${{ matrix.goos }}/${{ matrix.goarch }} with LDFLAGS: $LDFLAGS"
go build -ldflags "$LDFLAGS" -o croc${{ matrix.ext }}
- name: Create archive
run: |
if [ "${{ matrix.archive }}" = "zip" ]; then
zip croc_${{ github.event.release.name }}_${{ matrix.name }}.zip croc${{ matrix.ext }} LICENSE
else
tar -czvf croc_${{ github.event.release.name }}_${{ matrix.name }}.tar.gz croc${{ matrix.ext }} LICENSE
fi
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: build-${{ matrix.name }}
path: |
*.zip
*.tar.gz
release:
needs: [prepare-source, build]
runs-on: ubuntu-24.04
if: github.event_name == 'release'
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
merge-multiple: true
- name: Generate checksums
run: |
# Generate SHA256 checksums for all archives
sha256sum *.zip *.tar.gz > croc_${{ github.event.release.name }}_checksums.txt
# Display the checksums file for verification
echo "Generated checksums:"
cat croc_${{ github.event.release.name }}_checksums.txt
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
files: |
*.zip
*.tar.gz
*_checksums.txt
================================================
FILE: .github/workflows/stale.yml
================================================
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 0 * * *'
jobs:
stale:
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- name: Stale
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked stale because it has been open for 60 days with no activity.'
stale-pr-message: 'This pull request has been marked stale because it has been open for 60 days with no activity.'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
================================================
FILE: .github/workflows/winget.yml
================================================
name: Publish to Winget
on:
release:
types: [released]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-24.04
steps:
- name: Publish to Winget
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: schollz.croc
installers-regex: '.*Windows.*\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
================================================
FILE: .gitignore
================================================
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# Environment variables file
.env
# Croc builds
/croc
croc_v*
================================================
FILE: .goreleaser.yml
================================================
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:
enabled: false
================================================
FILE: .travis.yml
================================================
language: go
go:
- tip
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
================================================
FILE: Dockerfile
================================================
FROM golang:1.24-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
# Simple TCP health check with nc!
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD nc -z localhost 9009 || exit 1
ENTRYPOINT ["/croc-entrypoint.sh"]
CMD ["relay"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017-2025 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
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.
================================================
FILE: README.md
================================================
<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/github/v/release/schollz/croc" 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>
<a href="https://github.com/sponsors/schollz"><img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/schollz"></a>
</p>
<p align="center">
<strong>This project’s future depends on community support. <a href="https://github.com/sponsors/schollz">Become a sponsor today</a>.</strong>
</p>
## About
`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
- No need for local server or port-forwarding
- **IPv6-first** with IPv4 fallback
- Can **use a proxy**, like Tor
For more information about `croc`, see [my blog post](https://schollz.com/tinker/croc6/) or read a [recent interview I did](https://console.substack.com/p/console-91).

## Install
You can download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:
```bash
curl https://getcroc.schollz.com | bash
```
### On macOS
Using [Homebrew](https://brew.sh/):
```bash
brew install croc
```
Using [MacPorts](https://www.macports.org/):
```bash
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/windows/package-manager/):
```bash
scoop install croc
```
```bash
choco install croc
```
```bash
winget install schollz.croc
```
### Using nix-env
You can install the latest release with [Nix](https://nixos.org/):
```bash
nix-env -i croc
```
### On NixOS
You can add this to your [configuration.nix](https://nixos.org/manual/nixos/stable/#ch-configuration):
```nix
environment.systemPackages = [
pkgs.croc
];
```
### On Alpine Linux
First, install dependencies:
```bash
apk add bash coreutils
wget -qO- https://getcroc.schollz.com | bash
```
### On Arch Linux
Install with `pacman`:
```bash
pacman -S croc
```
### On Fedora
Install with `dnf`:
```bash
dnf install croc
```
### On Gentoo
Install with `portage`:
```bash
emerge net-misc/croc
```
### On Termux
Install with `pkg`:
```bash
pkg install croc
```
### On FreeBSD
Install with `pkg`:
```bash
pkg install croc
```
### On Linux, macOS, and Windows via Conda
You can install from [conda-forge](https://github.com/conda-forge/croc-feedstock) globally with [`pixi`](https://pixi.sh/):
```bash
pixi global install croc
```
Or install into a particular environment with [`conda`](https://docs.conda.io/projects/conda/):
```bash
conda install --channel conda-forge croc
```
### On Linux, macOS via Docker
Add the following one-liner function to your ~/.profile (works with any POSIX-compliant shell):
```bash
croc() { [ $# -eq 0 ] && set -- ""; mkdir -p "$HOME/.config/croc"; docker run --rm -it --user "$(id -u):$(id -g)" -v "$(pwd):/c" -v "$HOME/.config/croc:/.config/croc" -w /c -e CROC_SECRET docker.io/schollz/croc "$@"; }
```
You can also just paste it in the terminal for current session. On first run Docker will pull the image. `croc` via Docker will only work within the current directory and its subdirectories.
### Build from Source
If you prefer, you can [install Go](https://go.dev/dl/) and build from source (requires Go 1.22+):
```bash
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/packages/com.github.howeyc.crocgui/).
## Usage
To send a file, simply do:
```bash
$ 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, run:
```bash
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.
### Customizations & Options
#### Using `croc` on Linux or macOS
On Linux and macOS, the sending and receiving process is slightly different to avoid [leaking the secret via the process name](https://nvd.nist.gov/vuln/detail/CVE-2023-43621). You will need to run `croc` with the secret as an environment variable. For example, to receive with the secret `***`:
```bash
CROC_SECRET=*** croc
```
For single-user systems, the default behavior can be permanently enabled by running:
```bash
croc --classic
```
#### Custom Code Phrase
You can send with your own code phrase (must be more than 6 characters):
```bash
croc send --code [code-phrase] [file(s)-or-folder]
```
#### Allow Overwriting Without Prompt
To automatically overwrite files without prompting, use the `--overwrite` flag:
```bash
croc --yes --overwrite <code>
```
#### Excluding Folders
To exclude folders from being sent, use the `--exclude` flag with comma-delimited exclusions:
```bash
croc send --exclude "node_modules,.venv" [folder]
```
#### Use Pipes - stdin and stdout
You can pipe to `croc`:
```bash
cat [filename] | croc send
```
To receive the file to `stdout`, you can use:
```bash
croc --yes [code-phrase] > out
```
#### Send Text
To send URLs or short text, use:
```bash
croc send --text "hello world"
```
#### Send Multiple Files
You can send multiple files directly by listing the files and/or folders:
```bash
croc send [file1] [file2] [file3] [folder1] [folder2]
```
#### Show QR Code
To show QR code (for mobile devices), use:
```bash
croc send --qr [file(s)-or-folder]
```
#### Use a Proxy
You can send files via a proxy by adding `--socks5`:
```bash
croc --socks5 "127.0.0.1:9050" send SOMEFILE
```
#### Change Encryption Curve
To choose a different elliptic curve for encryption, use the `--curve` flag:
```bash
croc --curve p521 <codephrase>
```
#### Change Hash Algorithm
For faster hashing, use the `imohash` algorithm:
```bash
croc send --hash imohash SOMEFILE
```
#### Clipboard Options
By default, the code phrase is copied to your clipboard. To disable this:
```bash
croc --disable-clipboard send [filename]
```
To copy the full command with the secret as an environment variable (useful on Linux/macOS):
```bash
croc --extended-clipboard send [filename]
```
This copies the full command like `CROC_SECRET="code-phrase" croc` (including any relay/pass flags).
#### Quiet Mode
To suppress all output (useful for scripts and automation):
```bash
croc --quiet send [filename]
```
#### Self-host Relay
You can run your own relay:
```bash
croc relay
```
By default, it uses TCP ports 9009-9013. You can customize the ports (e.g., `croc relay --ports 1111,1112`), but at least **2** ports are required.
To send files using your relay:
```bash
croc --relay "myrelay.example.com:9009" send [filename]
```
#### Self-host Relay with Docker
You can also run a relay with Docker:
```bash
docker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' docker.io/schollz/croc
```
To send files using your custom relay:
```bash
croc --pass YOURPASSWORD --relay "myreal.example.com:9009" send [filename]
```
## Acknowledgements
`croc` has evolved through many iterations, and I am thankful for the contributions! Special thanks to:
- [@warner](https://github.com/warner) for the [idea](https://github.com/magic-wormhole/magic-wormhole)
- [@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28)
- [@skorokithakis](https://github.com/skorokithakis) for [proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/)
And many more!
================================================
FILE: croc-entrypoint.sh
================================================
#!/bin/sh
set -e
if [ -n "$CROC_PASS" ]; then
set -- --pass "$CROC_PASS" "$@"
fi
exec /croc "$@"
================================================
FILE: croc.service
================================================
[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
================================================
FILE: go.mod
================================================
module github.com/schollz/croc/v10
go 1.25.0
require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/chzyer/readline v1.5.1
github.com/denisbrodbeck/machineid v1.0.1
github.com/kalafut/imohash v1.1.1
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.1.1
github.com/schollz/peerdiscovery v1.7.6
github.com/schollz/progressbar/v3 v3.19.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.48.0
golang.org/x/net v0.51.0
golang.org/x/sys v0.41.0
golang.org/x/term v0.40.0
golang.org/x/time v0.14.0
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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
)
================================================
FILE: go.sum
================================================
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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.1 h1:G/HYtKgteQSVU96LidSJEbUGoZOMiBcuXYxbeb2W9e4=
github.com/kalafut/imohash v1.1.1/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.1.1 h1:lyoU5uNQ3thfjEzrahgxWWBm6+pbI1F2KAZ3gs6LIV8=
github.com/schollz/pake/v3 v3.1.1/go.mod h1:420+m3AakXcS0n7Uwc7eRs2CosQ2YfE/vKcIkilvqZc=
github.com/schollz/peerdiscovery v1.7.6 h1:HJjU1cXcNGfZgenC/vbry9F6CH9B8f+QYcTipZLbtDg=
github.com/schollz/peerdiscovery v1.7.6/go.mod h1:iTa0MWSPy49jJ2HcXL5oSSnFsd6olEUorAFljxbnj2I=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.10.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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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=
================================================
FILE: main.go
================================================
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 (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/schollz/croc/v10/src/cli"
"github.com/schollz/croc/v10/src/utils"
)
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")
// }
// }()
// 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)
}
// Exit the program gracefully
utils.RemoveMarkedFiles()
os.Exit(0)
}()
// Wait for a termination signal
<-sigs
utils.RemoveMarkedFiles()
// Exit the program gracefully
os.Exit(0)
}
================================================
FILE: src/cli/cli.go
================================================
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.4.2"
}
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"},
&cli.StringFlag{Name: "exclude", Value: "", Usage: "exclude files if they contain any of the comma separated strings"},
&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"}},
},
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"},
},
},
{
Name: "generate-fish-completion",
Usage: "generate fish completion and output to stdout",
Hidden: true,
Action: func(ctx *cli.Context) error {
completion, err := ctx.App.ToFishCompletion()
if err != nil {
return err
}
fmt.Print(completion)
return nil
},
},
}
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.BoolFlag{Name: "quiet", Usage: "disable all output"},
&cli.BoolFlag{Name: "disable-clipboard", Usage: "disable copy to clipboard"},
&cli.BoolFlag{Name: "extended-clipboard", Usage: "copy full command with secret as env variable to clipboard"},
&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("quiet") {
log.SetLevel("error")
} else 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
}
excludeStrings := []string{}
for _, v := range strings.Split(c.String("exclude"), ",") {
v = strings.ToLower(strings.TrimSpace(v))
if v != "" {
excludeStrings = append(excludeStrings, v)
}
}
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"),
Exclude: excludeStrings,
Quiet: c.Bool("quiet"),
DisableClipboard: c.Bool("disable-clipboard"),
ExtendedClipboard: c.Bool("extended-clipboard"),
}
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("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("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
}
if !c.IsSet("relay") && strings.HasPrefix(rememberedOptions.RelayAddress, "non-default:") {
var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, "non-default:")
rememberedAddr = strings.TrimSpace(rememberedAddr)
crocOptions.RelayAddress = rememberedAddr
}
if !c.IsSet("relay6") && strings.HasPrefix(rememberedOptions.RelayAddress6, "non-default:") {
var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, "non-default:")
rememberedAddr = strings.TrimSpace(rememberedAddr)
crocOptions.RelayAddress6 = rememberedAddr
}
}
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, crocOptions.Exclude)
if err != nil {
return
}
if len(crocOptions.Exclude) > 0 {
minimalFileInfosInclude := []croc.FileInfo{}
emptyFoldersToTransferInclude := []croc.FileInfo{}
for _, f := range minimalFileInfos {
exclude := false
for _, exclusion := range crocOptions.Exclude {
if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {
exclude = true
break
}
}
if !exclude {
minimalFileInfosInclude = append(minimalFileInfosInclude, f)
}
}
for _, f := range emptyFoldersToTransfer {
exclude := false
for _, exclusion := range crocOptions.Exclude {
if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {
exclude = true
break
}
}
if !exclude {
emptyFoldersToTransferInclude = append(emptyFoldersToTransferInclude, f)
}
}
totalNumberFolders = 0
folderMap := make(map[string]bool)
for _, f := range minimalFileInfosInclude {
folderMap[f.FolderRemote] = true
}
for _, f := range emptyFoldersToTransferInclude {
folderMap[f.FolderRemote] = true
}
totalNumberFolders = len(folderMap)
minimalFileInfos = minimalFileInfosInclude
emptyFoldersToTransfer = emptyFoldersToTransferInclude
}
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 = ""
}
if c.String("relay") != models.DEFAULT_RELAY {
crocOptions.RelayAddress = "non-default: " + c.String("relay")
} else {
crocOptions.RelayAddress = "default"
}
if c.String("relay6") != models.DEFAULT_RELAY6 {
crocOptions.RelayAddress6 = "non-default: " + c.String("relay6")
} else {
crocOptions.RelayAddress6 = "default"
}
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"),
Quiet: c.Bool("quiet"),
DisableClipboard: c.Bool("disable-clipboard"),
ExtendedClipboard: c.Bool("extended-clipboard"),
}
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("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("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("relay") && strings.HasPrefix(rememberedOptions.RelayAddress, "non-default:") {
var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, "non-default:")
rememberedAddr = strings.TrimSpace(rememberedAddr)
crocOptions.RelayAddress = rememberedAddr
}
if !c.IsSet("relay6") && strings.HasPrefix(rememberedOptions.RelayAddress6, "non-default:") {
var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, "non-default:")
rememberedAddr = strings.TrimSpace(rememberedAddr)
crocOptions.RelayAddress6 = rememberedAddr
}
}
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
if c.String("relay") != models.DEFAULT_RELAY {
crocOptions.RelayAddress = "non-default: " + c.String("relay")
} else {
crocOptions.RelayAddress = "default"
}
if c.String("relay6") != models.DEFAULT_RELAY6 {
crocOptions.RelayAddress6 = "non-default: " + c.String("relay6")
} else {
crocOptions.RelayAddress6 = "default"
}
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)
}
}
if len(ports) < 2 {
return fmt.Errorf("relay requires at least two ports; specify --ports with two or more ports or set --transfers to 2+")
}
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)
}
================================================
FILE: src/comm/comm.go
================================================
package comm
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"strings"
"time"
"github.com/magisterquis/connectproxy"
"github.com/schollz/croc/v10/src/utils"
log "github.com/schollz/logger"
"golang.org/x/net/proxy"
)
var Socks5Proxy = ""
var HttpProxy = ""
var MAGIC_BYTES = []byte("croc")
const maxReadMessageSize = 64 * 1024 * 1024
// 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
}
// Connection returns the net.Conn connection
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) 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
}
if n != len(tmpCopy) {
err = fmt.Errorf("wanted to write %d but wrote %d", len(b), n)
return
}
return
}
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
if err := c.connection.SetDeadline(time.Time{}); err != nil {
log.Warnf("failed to clear deadline: %v", err)
}
// read until we get 4 bytes for the magic
header := make([]byte, 4)
_, err = io.ReadFull(c.connection, header)
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
}
// read until we get 4 bytes for the header
header = make([]byte, 4)
_, err = io.ReadFull(c.connection, header)
if err != nil {
log.Debugf("initial read error: %v", err)
return
}
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
}
if numBytesUint32 > uint32(maxReadMessageSize) {
err = fmt.Errorf("message too large: %d > %d", numBytesUint32, maxReadMessageSize)
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
}
return
}
// Send a message
func (c *Comm) Send(message []byte) (err error) {
_, err = c.Write(message)
return
}
// Receive a message
func (c *Comm) Receive() (b []byte, err error) {
b, _, _, err = c.Read()
return
}
================================================
FILE: src/comm/comm_test.go
================================================
package comm
import (
"bytes"
"crypto/rand"
"encoding/binary"
"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)
}
// Use dynamic port allocation to avoid conflicts
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := listener.Addr().(*net.TCPAddr).Port
portStr := listener.Addr().String()
listener.Close() // Close the listener so we can reopen it in the goroutine
go func() {
log.Debug("starting TCP server on " + portStr)
server, err := net.Listen("tcp", portStr)
if err != nil {
log.Error(err)
return
}
defer func() {
if err := server.Close(); err != nil {
log.Error(err)
}
}()
// 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(_ int, 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(portStr, 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)
}
func TestReceiveRejectsOversizedMessage(t *testing.T) {
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
defer serverConn.Close()
c := New(clientConn)
writeErr := make(chan error, 1)
go func() {
header := new(bytes.Buffer)
header.Write(MAGIC_BYTES)
if err := binary.Write(header, binary.LittleEndian, uint32(maxReadMessageSize+1)); err != nil {
writeErr <- err
return
}
_, err := serverConn.Write(header.Bytes())
writeErr <- err
}()
_, err := c.Receive()
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "message too large")
assert.Nil(t, <-writeErr)
}
================================================
FILE: src/compress/compress.go
================================================
package compress
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, flate.HuffmanOnly)
return compressedData.Bytes()
}
// Decompress returns a decompressed byte slice.
func Decompress(src []byte) []byte {
compressedData := bytes.NewBuffer(src)
deCompressedData := new(bytes.Buffer)
decompress(compressedData, deCompressedData)
return deCompressedData.Bytes()
}
// compress uses flate to compress a byte slice to a corresponding level
func compress(src []byte, dest io.Writer, level int) {
compressor, 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.Close()
}
// decompress 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)
}
decompressor.Close()
}
================================================
FILE: src/compress/compress_test.go
================================================
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: src/croc/croc.go
================================================
package croc
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math"
"net"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/denisbrodbeck/machineid"
ignore "github.com/sabhiram/go-gitignore"
log "github.com/schollz/logger"
"github.com/schollz/pake/v3"
"github.com/schollz/peerdiscovery"
"github.com/schollz/progressbar/v3"
"github.com/skip2/go-qrcode"
"golang.org/x/term"
"golang.org/x/time/rate"
"github.com/schollz/croc/v10/src/comm"
"github.com/schollz/croc/v10/src/compress"
"github.com/schollz/croc/v10/src/crypt"
"github.com/schollz/croc/v10/src/message"
"github.com/schollz/croc/v10/src/models"
"github.com/schollz/croc/v10/src/tcp"
"github.com/schollz/croc/v10/src/utils"
)
var (
ipRequest = []byte("ips?")
handshakeRequest = []byte("handshake")
)
func init() {
log.SetLevel("debug")
}
// Debug toggles debug mode
func Debug(debug bool) {
if debug {
log.SetLevel("debug")
} else {
log.SetLevel("warn")
}
}
// Options specifies user specific options
type Options struct {
IsSender bool
SharedSecret string
RoomName string
Debug bool
RelayAddress string
RelayAddress6 string
RelayPorts []string
RelayPassword string
Stdout bool
NoPrompt bool
NoMultiplexing bool
DisableLocal bool
OnlyLocal bool
IgnoreStdin bool
Ask bool
SendingText bool
NoCompress bool
IP string
Overwrite bool
Curve string
HashAlgorithm string
ThrottleUpload string
ZipFolder bool
TestFlag bool
GitIgnore bool
MulticastAddress string
ShowQrCode bool
Exclude []string
Quiet bool
DisableClipboard bool
ExtendedClipboard bool
}
type SimpleMessage struct {
Bytes []byte
Kind string
}
// Client holds the state of the croc transfer
type Client struct {
Options Options
Pake *pake.Pake
Key []byte
ExternalIP, ExternalIPConnected string
// steps involved in forming relationship
Step1ChannelSecured bool
Step2FileInfoTransferred bool
Step3RecipientRequestFile bool
Step4FileTransferred bool
Step5CloseChannels bool
SuccessfulTransfer bool
// send / receive information of all files
FilesToTransfer []FileInfo
EmptyFoldersToTransfer []FileInfo
TotalNumberOfContents int
TotalNumberFolders int
FilesToTransferCurrentNum int
FilesHasFinished map[int]struct{}
TotalFilesIgnored int
// send / receive information of current file
CurrentFile *os.File
CurrentFileChunkRanges []int64
CurrentFileChunks []int64
CurrentFileIsClosed bool
LastFolder string
TotalSent int64
TotalChunksTransferred int
chunkMap map[uint64]struct{}
limiter *rate.Limiter
// tcp connections
conn []*comm.Comm
bar *progressbar.ProgressBar
longestFilename int
firstSend bool
mutex *sync.Mutex
fread *os.File
numfinished int
quit chan bool
finishedNum int
numberOfTransferredFiles int
// ctx.go for graceful shutdown
*stop
}
// Chunk contains information about the
// needed bytes
type Chunk struct {
Bytes []byte `json:"b,omitempty"`
Location int64 `json:"l,omitempty"`
}
// FileInfo registers the information about the file
type FileInfo struct {
Name string `json:"n,omitempty"`
FolderRemote string `json:"fr,omitempty"`
FolderSource string `json:"fs,omitempty"`
Hash []byte `json:"h,omitempty"`
Size int64 `json:"s,omitempty"`
ModTime time.Time `json:"m,omitempty"`
IsCompressed bool `json:"c,omitempty"`
IsEncrypted bool `json:"e,omitempty"`
Symlink string `json:"sy,omitempty"`
Mode os.FileMode `json:"md,omitempty"`
TempFile bool `json:"tf,omitempty"`
IsIgnored bool `json:"ig,omitempty"`
}
// RemoteFileRequest requests specific bytes
type RemoteFileRequest struct {
CurrentFileChunkRanges []int64
FilesToTransferCurrentNum int
MachineID string
}
// SenderInfo lists the files to be transferred
type SenderInfo struct {
FilesToTransfer []FileInfo
EmptyFoldersToTransfer []FileInfo
TotalNumberFolders int
MachineID string
Ask bool
SendingText bool
NoCompress bool
HashAlgorithm string
}
// New establishes a new connection for transferring files between two instances.
func New(ops Options) (c *Client, err error) {
c = new(Client)
c.FilesHasFinished = make(map[int]struct{})
// setup basic info
c.Options = ops
Debug(c.Options.Debug)
// redirect stderr to null if quiet mode is enabled
if c.Options.Quiet {
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err == nil {
os.Stderr = devNull
}
}
if len(c.Options.SharedSecret) < 6 {
err = fmt.Errorf("code is too short")
return
}
// Create a hash of part of the shared secret to use as the room name
hashExtra := "croc"
roomNameBytes := sha256.Sum256([]byte(c.Options.SharedSecret[:4] + hashExtra))
c.Options.RoomName = hex.EncodeToString(roomNameBytes[:])
c.conn = make([]*comm.Comm, 16)
// initialize throttler
if len(c.Options.ThrottleUpload) > 1 && c.Options.IsSender {
upload := c.Options.ThrottleUpload[:len(c.Options.ThrottleUpload)-1]
var uploadLimit int64
uploadLimit, err = strconv.ParseInt(upload, 10, 64)
if err != nil {
panic("Could not parse given Upload Limit")
}
minBurstSize := models.TCP_BUFFER_SIZE
var rt rate.Limit
switch unit := string(c.Options.ThrottleUpload[len(c.Options.ThrottleUpload)-1:]); unit {
case "g", "G":
uploadLimit = uploadLimit * 1024 * 1024 * 1024
case "m", "M":
uploadLimit = uploadLimit * 1024 * 1024
case "k", "K":
uploadLimit = uploadLimit * 1024
default:
uploadLimit, err = strconv.ParseInt(c.Options.ThrottleUpload, 10, 64)
if err != nil {
panic("Could not parse given Upload Limit")
}
}
rt = rate.Every(time.Second / time.Duration(uploadLimit))
if int(uploadLimit) > minBurstSize {
minBurstSize = int(uploadLimit)
}
c.limiter = rate.NewLimiter(rt, minBurstSize)
log.Debugf("Throttling Upload to %#v", c.limiter.Limit())
}
// initialize pake for recipient
if !c.Options.IsSender {
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)
}
if err != nil {
return
}
c.mutex = &sync.Mutex{}
c.stop = newStop(context.Background())
return
}
// TransferOptions for sending
type TransferOptions struct {
PathToFiles []string
KeepPathInRemote bool
}
// helper function checking for an empty folder
func isEmptyFolder(folderPath string) (bool, error) {
f, err := os.Open(folderPath)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, nil
}
// helper function to walk each subfolder and parses against an ignore file.
// returns a hashmap Key: Absolute filepath, Value: boolean (true=ignore)
func gitWalk(dir string, gitObj *ignore.GitIgnore, files map[string]bool) {
var ignoredDir bool
var current string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if isChild(current, path) && ignoredDir {
files[path] = true
return nil
}
if info.IsDir() && filepath.Base(path) == filepath.Base(dir) {
ignoredDir = false // Skip applying ignore rules for root directory
return nil
}
if gitObj.MatchesPath(info.Name()) {
files[path] = true
ignoredDir = true
current = path
return nil
} else {
files[path] = false
ignoredDir = false
return nil
}
})
if err != nil {
log.Errorf("filepath error")
}
}
func isChild(parentPath, childPath string) bool {
relPath, err := filepath.Rel(parentPath, childPath)
if err != nil {
return false
}
return !strings.HasPrefix(relPath, "..")
}
// This function retrieves the important file information
// for every file that will be transferred
func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []string) (filesInfo []FileInfo, emptyFolders []FileInfo, totalNumberFolders int, err error) {
// fnames: the relative/absolute paths of files/folders that will be transferred
totalNumberFolders = 0
var paths []string
for _, fname := range fnames {
// Support wildcard
if strings.Contains(fname, "*") {
matches, errGlob := filepath.Glob(fname)
if errGlob != nil {
err = errGlob
return
}
paths = append(paths, matches...)
continue
} else {
paths = append(paths, fname)
}
}
ignoredPaths := make(map[string]bool)
if ignoreGit {
wd, wdErr := os.Stat(".gitignore")
if wdErr == nil {
gitIgnore, gitErr := ignore.CompileIgnoreFile(wd.Name())
if gitErr == nil {
for _, path := range paths {
abs, absErr := filepath.Abs(path)
if absErr != nil {
err = absErr
return
}
if gitIgnore.MatchesPath(path) {
ignoredPaths[abs] = true
}
}
}
}
for _, path := range paths {
abs, absErr := filepath.Abs(path)
if absErr != nil {
err = absErr
return
}
file, fileErr := os.Stat(path)
if fileErr == nil && file.IsDir() {
_, subErr := os.Stat(filepath.Join(path, ".gitignore"))
if subErr == nil {
gitObj, gitObjErr := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
if gitObjErr != nil {
err = gitObjErr
return
}
gitWalk(abs, gitObj, ignoredPaths)
}
}
}
}
for _, fpath := range paths {
stat, errStat := os.Lstat(fpath)
if errStat != nil {
err = errStat
return
}
absPath, errAbs := filepath.Abs(fpath)
if errAbs != nil {
err = errAbs
return
}
if stat.IsDir() && zipfolder {
if fpath[len(fpath)-1:] != "/" {
fpath += "/"
}
fpath = filepath.Dir(fpath)
dest := filepath.Base(fpath) + ".zip"
utils.ZipDirectory(dest, fpath)
utils.MarkFileForRemoval(dest)
stat, errStat = os.Lstat(dest)
if errStat != nil {
err = errStat
return
}
absPath, errAbs = filepath.Abs(dest)
if errAbs != nil {
err = errAbs
return
}
fInfo := FileInfo{
Name: stat.Name(),
FolderRemote: "./",
FolderSource: filepath.Dir(absPath),
Size: stat.Size(),
ModTime: stat.ModTime(),
Mode: stat.Mode(),
TempFile: true,
IsIgnored: ignoredPaths[absPath],
}
if fInfo.IsIgnored {
continue
}
filesInfo = append(filesInfo, fInfo)
continue
}
if stat.IsDir() {
err = filepath.Walk(absPath,
func(pathName string, info os.FileInfo, err error) error {
if err != nil {
return err
}
absPathWithSeparator := filepath.Dir(absPath)
if !strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)) {
absPathWithSeparator += string(os.PathSeparator)
}
if strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)+string(os.PathSeparator)) {
absPathWithSeparator = strings.TrimSuffix(absPathWithSeparator, string(os.PathSeparator))
}
remoteFolder := strings.TrimPrefix(filepath.Dir(pathName), absPathWithSeparator)
if !info.IsDir() {
fInfo := FileInfo{
Name: info.Name(),
FolderRemote: strings.ReplaceAll(remoteFolder, string(os.PathSeparator), "/") + "/",
FolderSource: filepath.Dir(pathName),
Size: info.Size(),
ModTime: info.ModTime(),
Mode: info.Mode(),
TempFile: false,
IsIgnored: ignoredPaths[pathName],
}
if fInfo.IsIgnored && ignoreGit {
return nil
} else {
filesInfo = append(filesInfo, fInfo)
}
} else {
if ignoredPaths[pathName] {
return filepath.SkipDir
}
isEmptyFolder, _ := isEmptyFolder(pathName)
totalNumberFolders++
if isEmptyFolder {
emptyFolders = append(emptyFolders, FileInfo{
// Name: info.Name(),
FolderRemote: strings.ReplaceAll(strings.TrimPrefix(pathName,
filepath.Dir(absPath)+string(os.PathSeparator)), string(os.PathSeparator), "/") + "/",
})
}
}
return nil
})
if err != nil {
return
}
} else {
fInfo := FileInfo{
Name: stat.Name(),
FolderRemote: "./",
FolderSource: filepath.Dir(absPath),
Size: stat.Size(),
ModTime: stat.ModTime(),
Mode: stat.Mode(),
TempFile: false,
IsIgnored: ignoredPaths[absPath],
}
if fInfo.IsIgnored && ignoreGit {
continue
} else {
filesInfo = append(filesInfo, fInfo)
}
}
}
return
}
func (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {
c.FilesToTransfer = filesInfo
totalFilesSize := int64(0)
for i, fileInfo := range c.FilesToTransfer {
var fullPath string
fullPath = fileInfo.FolderSource + string(os.PathSeparator) + fileInfo.Name
fullPath = filepath.Clean(fullPath)
if len(fileInfo.Name) > c.longestFilename {
c.longestFilename = len(fileInfo.Name)
}
if fileInfo.Mode&os.ModeSymlink != 0 {
log.Debugf("%s is symlink", fileInfo.Name)
c.FilesToTransfer[i].Symlink, err = os.Readlink(fullPath)
if err != nil {
log.Debugf("error getting symlink: %s", err.Error())
}
log.Debugf("%+v", c.FilesToTransfer[i])
}
if c.Options.HashAlgorithm == "" {
c.Options.HashAlgorithm = "xxhash"
}
c.FilesToTransfer[i].Hash, err = c.stop.hash(fullPath, c.Options.HashAlgorithm, fileInfo.Size > 1e7)
log.Debugf("hashed %s to %x using %s", fullPath, c.FilesToTransfer[i].Hash, c.Options.HashAlgorithm)
totalFilesSize += fileInfo.Size
if err != nil {
return
}
log.Debugf("file %d info: %+v", i, c.FilesToTransfer[i])
fmt.Fprintf(os.Stderr, "\r ")
fmt.Fprintf(os.Stderr, "\rSending %d files (%s)", i, utils.ByteCountDecimal(totalFilesSize))
}
log.Debugf("longestFilename: %+v", c.longestFilename)
fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
folderName := fmt.Sprintf("%d folders", c.TotalNumberFolders)
if len(c.FilesToTransfer) == 1 {
fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
}
if strings.HasPrefix(fname, "'croc-stdin-") {
fname = "'stdin'"
if c.Options.SendingText {
fname = "'text'"
}
}
fmt.Fprintf(os.Stderr, "\r ")
if c.TotalNumberFolders > 0 {
fmt.Fprintf(os.Stderr, "\rSending %s and %s (%s)\n", fname, folderName, utils.ByteCountDecimal(totalFilesSize))
} else {
fmt.Fprintf(os.Stderr, "\rSending %s (%s)\n", fname, utils.ByteCountDecimal(totalFilesSize))
}
return
}
func (c *Client) setupLocalRelay() {
// setup the relay locally
firstPort, _ := strconv.Atoi(c.Options.RelayPorts[0])
openPorts := utils.FindOpenPorts("127.0.0.1", firstPort, len(c.Options.RelayPorts))
if len(openPorts) < len(c.Options.RelayPorts) {
panic("not enough open ports to run local relay")
}
for i, port := range openPorts {
c.Options.RelayPorts[i] = fmt.Sprint(port)
}
for _, port := range c.Options.RelayPorts {
go func(portStr string) {
debugString := "warn"
if c.Options.Debug {
debugString = "debug"
}
err := c.stop.run(
debugString,
"127.0.0.1",
portStr,
c.Options.RelayPassword,
strings.Join(c.Options.RelayPorts[1:], ","))
if err != nil {
panic(err)
}
}(port)
}
}
func (c *Client) broadcastOnLocalNetwork(useipv6 bool) {
var timeLimit time.Duration
// if we don't use an external relay, the broadcast messages need to be sent continuously
if c.Options.OnlyLocal {
timeLimit = -1 * time.Second
} else {
timeLimit = 30 * time.Second
}
// look for peers first
settings := peerdiscovery.Settings{
Limit: -1,
Payload: []byte("croc" + c.Options.RelayPorts[0]),
Delay: 20 * time.Millisecond,
TimeLimit: timeLimit,
StopChan: c.stop.stopChan,
}
if useipv6 {
settings.IPVersion = peerdiscovery.IPv6
} else {
settings.MulticastAddress = c.Options.MulticastAddress
}
discoveries, err := peerdiscovery.Discover(settings)
log.Debugf("discoveries: %+v", discoveries)
if err != nil {
log.Debug(err)
}
}
func (c *Client) transferOverLocalRelay(errchan chan<- error) {
time.Sleep(500 * time.Millisecond)
log.Debug("establishing connection")
var banner string
conn, banner, ipaddr, err := tcp.ConnectToTCPServer("127.0.0.1:"+c.Options.RelayPorts[0], c.Options.RelayPassword, c.Options.RoomName)
log.Debugf("banner: %s", banner)
if err != nil {
err = fmt.Errorf("could not connect to 127.0.0.1:%s: %w", c.Options.RelayPorts[0], err)
log.Debug(err)
// not really an error because it will try to connect over the actual relay
return
}
log.Debugf("local connection established: %+v", conn)
for {
if err := c.ctxErr(); err != nil {
errchan <- err
return
}
data, _ := conn.Receive()
if bytes.Equal(data, handshakeRequest) {
break
} else if bytes.Equal(data, []byte{1}) {
log.Trace("got ping")
} else {
log.Debugf("instead of handshake got: %s", data)
}
}
c.conn[0] = conn
log.Debug("exchanged header message")
c.Options.RelayAddress = "127.0.0.1"
c.Options.RelayPorts = strings.Split(banner, ",")
if c.Options.NoMultiplexing {
log.Debug("no multiplexing")
c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
}
c.ExternalIP = ipaddr
errchan <- c.transfer()
}
// Send will send the specified file
func (c *Client) Send(filesInfo []FileInfo, emptyFoldersToTransfer []FileInfo, totalNumberFolders int) (err error) {
go c.stop.done()
defer c.stop.Cancel()
c.EmptyFoldersToTransfer = emptyFoldersToTransfer
c.TotalNumberFolders = totalNumberFolders
c.TotalNumberOfContents = len(filesInfo)
err = c.sendCollectFiles(filesInfo)
if err != nil {
return
}
flags := &strings.Builder{}
if c.Options.RelayAddress != models.DEFAULT_RELAY && !c.Options.OnlyLocal {
flags.WriteString("--relay " + c.Options.RelayAddress + " ")
}
if c.Options.RelayPassword != models.DEFAULT_PASSPHRASE {
flags.WriteString("--pass " + c.Options.RelayPassword + " ")
}
fmt.Fprintf(os.Stderr, `Code is: %[1]s
On the other computer run:
(For Windows)
croc %[2]s%[1]s
(For Linux/macOS)
CROC_SECRET=%[1]q croc %[2]s
`, c.Options.SharedSecret, flags.String())
if !c.Options.DisableClipboard {
clipboardText := c.Options.SharedSecret
if c.Options.ExtendedClipboard {
clipboardText = fmt.Sprintf("CROC_SECRET=%q croc %s", c.Options.SharedSecret, strings.TrimSpace(flags.String()))
}
copyToClipboard(clipboardText, c.Options.Quiet, c.Options.ExtendedClipboard)
}
if c.Options.ShowQrCode {
showReceiveCommandQrCode(fmt.Sprintf("%[1]s", c.Options.SharedSecret))
}
if c.Options.Ask {
machid, _ := machineid.ID()
fmt.Fprintf(os.Stderr, "\rYour machine ID is '%s'\n", machid)
}
// c.spinner.Suffix = " waiting for recipient..."
// c.spinner.Start()
// create channel for quitting
// connect to the relay for messaging
errchan := make(chan error, 1)
if !c.Options.DisableLocal {
// add two things to the error channel
errchan = make(chan error, 2)
c.setupLocalRelay()
// broadcast on ipv4
go c.broadcastOnLocalNetwork(false)
// broadcast on ipv6
go c.broadcastOnLocalNetwork(true)
go c.transferOverLocalRelay(errchan)
}
if !c.Options.OnlyLocal {
go func() {
var ipaddr, banner string
var conn *comm.Comm
durations := []time.Duration{100 * time.Millisecond, 5 * time.Second}
for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {
if address == "" {
continue
}
host, port, _ := net.SplitHostPort(address)
log.Debugf("host: '%s', port: '%s'", host, port)
// Default port to :9009
if port == "" {
host = address
port = models.DEFAULT_PORT
}
log.Debugf("got host '%v' and port '%v'", host, port)
address = net.JoinHostPort(host, port)
log.Debugf("trying connection to %s", address)
conn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])
if err == nil {
c.Options.RelayAddress = address
break
}
log.Debugf("could not establish '%s'", address)
}
if conn == nil && err == nil {
err = fmt.Errorf("could not connect")
}
if err != nil {
err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err)
log.Debug(err)
errchan <- err
return
}
log.Debugf("banner: %s", banner)
log.Debugf("connection established: %+v", conn)
var kB []byte
B, _ := pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, c.Options.Curve)
for {
if err := c.ctxErr(); err != nil {
errchan <- err
return
}
var dataMessage SimpleMessage
log.Trace("waiting for bytes")
data, errConn := conn.Receive()
if errConn != nil {
log.Tracef("[%+v] had error: %s", conn, errConn.Error())
}
json.Unmarshal(data, &dataMessage)
log.Tracef("data: %+v '%s'", data, data)
log.Tracef("dataMessage: %s", dataMessage)
log.Tracef("kB: %x", kB)
// if kB not null, then use it to decrypt
if kB != nil {
var decryptErr error
var dataDecrypt []byte
dataDecrypt, decryptErr = crypt.Decrypt(data, kB)
if decryptErr != nil {
log.Tracef("error decrypting: %v: '%s'", decryptErr, data)
// relay sent a message encrypted with an invalid key.
// consider this a security issue and abort
if strings.Contains(decryptErr.Error(), "message authentication failed") {
errchan <- decryptErr
return
}
} else {
// copy dataDecrypt to data
data = dataDecrypt
log.Tracef("decrypted: %s", data)
}
}
if bytes.Equal(data, ipRequest) {
log.Tracef("got ipRequest")
// recipient wants to try to connect to local ips
var ips []string
// only get local ips if the local is enabled
if !c.Options.DisableLocal {
// get list of local ips
ips, err = utils.GetLocalIPs()
if err != nil {
log.Tracef("error getting local ips: %v", err)
}
// prepend the port that is being listened to
ips = append([]string{c.Options.RelayPorts[0]}, ips...)
}
log.Tracef("sending ips: %+v", ips)
bips, errIps := json.Marshal(ips)
if errIps != nil {
log.Tracef("error marshalling ips: %v", errIps)
}
bips, errIps = crypt.Encrypt(bips, kB)
if errIps != nil {
log.Tracef("error encrypting ips: %v", errIps)
}
if err = conn.Send(bips); err != nil {
log.Errorf("error sending: %v", err)
}
} else if dataMessage.Kind == "pake1" {
log.Trace("got pake1")
var pakeError error
pakeError = B.Update(dataMessage.Bytes)
if pakeError == nil {
kB, pakeError = B.SessionKey()
if pakeError == nil {
log.Tracef("dataMessage kB: %x", kB)
dataMessage.Bytes = B.Bytes()
dataMessage.Kind = "pake2"
data, _ = json.Marshal(dataMessage)
if pakeError = conn.Send(data); err != nil {
log.Errorf("dataMessage error sending: %v", err)
}
}
}
} else if bytes.Equal(data, handshakeRequest) {
log.Trace("got handshake")
break
} else if bytes.Equal(data, []byte{1}) {
log.Trace("got ping")
continue
} else {
log.Tracef("[%+v] got weird bytes: %+v", conn, data)
// throttle the reading
errchan <- fmt.Errorf("gracefully refusing using the public relay")
return
}
}
c.conn[0] = conn
c.Options.RelayPorts = strings.Split(banner, ",")
if c.Options.NoMultiplexing {
log.Debug("no multiplexing")
c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
}
c.ExternalIP = ipaddr
log.Debug("exchanged header message")
errchan <- c.transfer()
}()
}
err = <-errchan
if err == nil {
return // no error
} else {
log.Debugf("error from errchan: %v", err)
if strings.Contains(err.Error(), "could not secure channel") {
return err
}
}
if !c.Options.DisableLocal {
if strings.Contains(err.Error(), "refusing files") || strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "bad password") || strings.Contains(err.Error(), "message authentication failed") {
errchan <- err
}
err = <-errchan
}
return err
}
func showReceiveCommandQrCode(command string) {
qrCode, err := qrcode.New(command, qrcode.Medium)
if err == nil {
fmt.Println(qrCode.ToSmallString(false))
}
}
// Receive will receive a file
func (c *Client) Receive() (err error) {
go c.stop.done()
defer c.stop.Cancel()
fmt.Fprintf(os.Stderr, "connecting...")
// recipient will look for peers first
// and continue if it doesn't find any within 100 ms
usingLocal := false
isIPset := false
if c.Options.OnlyLocal || c.Options.IP != "" {
c.Options.RelayAddress = ""
c.Options.RelayAddress6 = ""
}
if c.Options.IP != "" {
// check ip version
if strings.Count(c.Options.IP, ":") >= 2 {
log.Debug("assume ipv6")
c.Options.RelayAddress6 = c.Options.IP
}
if strings.Contains(c.Options.IP, ".") {
log.Debug("assume ipv4")
c.Options.RelayAddress = c.Options.IP
}
isIPset = true
}
if !c.Options.DisableLocal && !isIPset {
log.Debug("attempt to discover peers")
var discoveries []peerdiscovery.Discovered
var wgDiscovery sync.WaitGroup
var dmux sync.Mutex
wgDiscovery.Add(2)
go func() {
defer wgDiscovery.Done()
ipv4discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
Payload: []byte("ok"),
Delay: 20 * time.Millisecond,
TimeLimit: 200 * time.Millisecond,
MulticastAddress: c.Options.MulticastAddress,
StopChan: c.stop.stopChan,
})
if err1 == nil && len(ipv4discoveries) > 0 {
dmux.Lock()
err = err1
discoveries = append(discoveries, ipv4discoveries...)
dmux.Unlock()
}
}()
go func() {
defer wgDiscovery.Done()
ipv6discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{
Limit: 1,
Payload: []byte("ok"),
Delay: 20 * time.Millisecond,
TimeLimit: 200 * time.Millisecond,
IPVersion: peerdiscovery.IPv6,
StopChan: c.stop.stopChan,
})
if err1 == nil && len(ipv6discoveries) > 0 {
dmux.Lock()
err = err1
discoveries = append(discoveries, ipv6discoveries...)
dmux.Unlock()
}
}()
wgDiscovery.Wait()
if err == nil && len(discoveries) > 0 {
log.Debugf("all discoveries: %+v", discoveries)
for i := 0; i < len(discoveries); i++ {
log.Debugf("discovery %d has payload: %+v", i, discoveries[i])
if !bytes.HasPrefix(discoveries[i].Payload, []byte("croc")) {
log.Debug("skipping discovery")
continue
}
log.Debug("switching to local")
portToUse := string(bytes.TrimPrefix(discoveries[i].Payload, []byte("croc")))
if portToUse == "" {
portToUse = models.DEFAULT_PORT
}
address := net.JoinHostPort(discoveries[i].Address, portToUse)
errPing := tcp.PingServer(address)
if errPing == nil {
log.Debugf("successfully pinged '%s'", address)
c.Options.RelayAddress = address
c.ExternalIPConnected = c.Options.RelayAddress
c.Options.RelayAddress6 = ""
usingLocal = true
break
} else {
log.Debugf("could not ping: %+v", errPing)
}
}
}
log.Debugf("discoveries: %+v", discoveries)
log.Debug("establishing connection")
}
var banner string
durations := []time.Duration{200 * time.Millisecond, 5 * time.Second}
err = fmt.Errorf("found no addresses to connect")
for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {
if address == "" {
continue
}
var host, port string
host, port, _ = net.SplitHostPort(address)
// Default port to :9009
if port == "" {
host = address
port = models.DEFAULT_PORT
}
log.Debugf("got host '%v' and port '%v'", host, port)
address = net.JoinHostPort(host, port)
log.Debugf("trying connection to %s", address)
c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])
if err == nil {
c.Options.RelayAddress = address
break
}
log.Debugf("could not establish '%s'", address)
}
if err != nil {
err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err)
log.Debug(err)
return
}
log.Debugf("receiver connection established: %+v", c.conn[0])
log.Debugf("banner: %s", banner)
if c.Options.TestFlag {
log.Debugf("TEST FLAG ENABLED, TESTING LOCAL IPS")
}
if c.Options.TestFlag || (!usingLocal && !c.Options.DisableLocal && !isIPset) {
// ask the sender for their local ips and port
// and try to connect to them
var ips []string
err = func() (err error) {
var A *pake.Pake
var data []byte
A, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)
if err != nil {
return err
}
dataMessage := SimpleMessage{
Bytes: A.Bytes(),
Kind: "pake1",
}
data, _ = json.Marshal(dataMessage)
if err = c.conn[0].Send(data); err != nil {
log.Errorf("dataMessage send error: %v", err)
return
}
data, err = c.conn[0].Receive()
if err != nil {
return
}
err = json.Unmarshal(data, &dataMessage)
if err != nil || dataMessage.Kind != "pake2" {
log.Debugf("data: %s", data)
return fmt.Errorf("dataMessage %s pake failed", ipRequest)
}
err = A.Update(dataMessage.Bytes)
if err != nil {
return
}
var kA []byte
kA, err = A.SessionKey()
if err != nil {
return
}
log.Debugf("dataMessage kA: %x", kA)
// secure ipRequest
data, err = crypt.Encrypt([]byte(ipRequest), kA)
if err != nil {
return
}
log.Debug("sending ips?")
if err = c.conn[0].Send(data); err != nil {
log.Errorf("ips send error: %v", err)
}
data, err = c.conn[0].Receive()
if err != nil {
return
}
data, err = crypt.Decrypt(data, kA)
if err != nil {
return
}
log.Debugf("ips data: %s", data)
if err = json.Unmarshal(data, &ips); err != nil {
log.Debugf("ips unmarshal error: %v", err)
}
return
}()
if len(ips) > 1 {
port := ips[0]
ips = ips[1:]
for _, ip := range ips {
ipv4Addr, ipv4Net, errNet := net.ParseCIDR(fmt.Sprintf("%s/24", ip))
log.Debugf("ipv4Add4: %+v, ipv4Net: %+v, err: %+v", ipv4Addr, ipv4Net, errNet)
// For peer-to-peer connectivity within a LAN, the sender and receiver don't need to be on the same subnet.
// Even with NAT routers in their respective local networks,
// a receiver behind NAT can establish direct access to the sender without requiring internet connectivity.
// Conversely, the local networks on the sender and receiver may overlap but not be connected.
// This often occurs with 192.168.0.0/30 and 192.168.1.0/30 subnets.
// localIps, _ := utils.GetLocalIPs()
// haveLocalIP := false
// for _, localIP := range localIps {
// localIPparsed := net.ParseIP(localIP)
// log.Debugf("localIP: %+v, localIPparsed: %+v", localIP, localIPparsed)
// if ipv4Net.Contains(localIPparsed) {
// haveLocalIP = true
// log.Debugf("ip: %+v is a local IP", ip)
// break
// }
// }
// if !haveLocalIP {
// log.Debugf("%s is not a local IP, skipping", ip)
// continue
// }
serverTry := net.JoinHostPort(ip, port)
conn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.RoomName, 500*time.Millisecond)
if errConn != nil {
log.Debug(errConn)
log.Debug("could not connect to " + serverTry)
continue
}
log.Debugf("local connection established to %s", serverTry)
log.Debugf("banner: %s", banner2)
// reset to the local port
banner = banner2
c.Options.RelayAddress = serverTry
c.ExternalIP = externalIP
c.conn[0].Close()
c.conn[0] = nil
c.conn[0] = conn
break
}
}
}
if err = c.conn[0].Send(handshakeRequest); err != nil {
log.Errorf("handshake send error: %v", err)
}
c.Options.RelayPorts = strings.Split(banner, ",")
if c.Options.NoMultiplexing {
log.Debug("no multiplexing")
c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
}
log.Debug("exchanged header message")
fmt.Fprintf(os.Stderr, "\rsecuring channel...")
err = c.transfer()
if err == nil {
if c.numberOfTransferredFiles+len(c.EmptyFoldersToTransfer) == 0 {
fmt.Fprintf(os.Stderr, "\rNo files transferred.\n")
}
} else {
c.SendError()
}
return
}
func (c *Client) transfer() (err error) {
// connect to the server
// quit with c.quit <- true
c.quit = make(chan bool)
// if recipient, initialize with sending pake information
log.Debug("ready")
if !c.Options.IsSender && !c.Step1ChannelSecured {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypePAKE,
Bytes: c.Pake.Bytes(),
Bytes2: []byte(c.Options.Curve),
})
if err != nil {
return
}
}
// listen for incoming messages and process them
for {
if e := c.ctxErr(); e != nil {
log.Tracef("transfer: %v", e)
err = e
break
}
var data []byte
var done bool
data, err = c.conn[0].Receive()
if err != nil {
log.Debugf("got error receiving: %v", err)
if !c.Step1ChannelSecured {
err = fmt.Errorf("could not secure channel")
}
break
}
done, err = c.processMessage(data)
if err != nil {
log.Debugf("data: %s", data)
log.Debugf("got error processing: %v", err)
break
}
if done {
break
}
}
if err := c.ctxErr(); err != nil && c.SuccessfulTransfer {
c.SuccessfulTransfer = false
log.Tracef("SuccessfulTransfer: %v", err)
}
// purge errors that come from successful transfer
if c.SuccessfulTransfer {
if err != nil {
log.Debugf("purging error: %s", err)
}
err = nil
}
if c.Options.IsSender && c.SuccessfulTransfer {
for _, file := range c.FilesToTransfer {
if file.TempFile {
fmt.Println("Removing " + file.Name)
os.Remove(file.Name)
}
}
}
if c.SuccessfulTransfer && !c.Options.IsSender {
for _, file := range c.FilesToTransfer {
if file.TempFile {
if unzipErr := utils.UnzipDirectory(".", file.Name); unzipErr != nil {
c.SuccessfulTransfer = false
err = fmt.Errorf("failed to unzip received archive %s: %w", file.Name, unzipErr)
log.Error(err)
break
}
if removeErr := os.Remove(file.Name); removeErr != nil {
log.Warnf("error removing %s: %v", file.Name, removeErr)
} else {
log.Debugf("Removing %s\n", file.Name)
}
}
}
}
if c.Options.Stdout && !c.Options.IsSender && len(c.FilesToTransfer) > 0 && c.FilesToTransferCurrentNum < len(c.FilesToTransfer) {
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
log.Debugf("pathToFile: %s", pathToFile)
// close if not closed already
if !c.CurrentFileIsClosed {
c.CurrentFile.Close()
c.CurrentFileIsClosed = true
}
if err = os.Remove(pathToFile); err != nil {
log.Warnf("error removing %s: %v", pathToFile, err)
}
fmt.Fprint(os.Stderr, "\n")
}
if err != nil && strings.Contains(err.Error(), "pake not successful") {
log.Debugf("pake error: %s", err.Error())
err = fmt.Errorf("password mismatch")
}
if err != nil && strings.Contains(err.Error(), "unexpected end of JSON input") {
log.Debugf("error: %s", err.Error())
err = fmt.Errorf("room (secure channel) not ready, maybe peer disconnected")
}
if err != nil {
c.SendError()
}
return
}
func (c *Client) createEmptyFolder(i int) (err error) {
err = os.MkdirAll(c.EmptyFoldersToTransfer[i].FolderRemote, os.ModePerm)
if err != nil {
return
}
fmt.Fprintf(os.Stderr, "%s\n", c.EmptyFoldersToTransfer[i].FolderRemote)
c.bar = progressbar.NewOptions64(1,
progressbar.OptionOnCompletion(func() {
c.fmtPrintUpdate()
}),
progressbar.OptionSetWidth(20),
progressbar.OptionSetDescription(" "),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionShowBytes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetVisibility(!c.Options.SendingText),
)
c.bar.Finish()
return
}
func (c *Client) processMessageFileInfo(m message.Message) (done bool, err error) {
var senderInfo SenderInfo
err = json.Unmarshal(m.Bytes, &senderInfo)
if err != nil {
log.Debug(err)
return
}
c.Options.SendingText = senderInfo.SendingText
c.Options.NoCompress = senderInfo.NoCompress
c.Options.HashAlgorithm = senderInfo.HashAlgorithm
c.EmptyFoldersToTransfer = senderInfo.EmptyFoldersToTransfer
c.TotalNumberFolders = senderInfo.TotalNumberFolders
c.FilesToTransfer = senderInfo.FilesToTransfer
for i, fi := range c.FilesToTransfer {
// Issues #593 - sanitize the sender paths and prevent ".." from being used
c.FilesToTransfer[i].FolderRemote = filepath.Clean(fi.FolderRemote)
// Issues #593 - disallow specific folders like .ssh
if strings.Contains(c.FilesToTransfer[i].FolderRemote, ".ssh") {
return true, fmt.Errorf("invalid path detected: '%s'", fi.FolderRemote)
}
// Issue #595 - disallow filenames with invisible characters
errFileName := utils.ValidFileName(path.Join(c.FilesToTransfer[i].FolderRemote, fi.Name))
if errFileName != nil {
return true, errFileName
}
}
c.TotalNumberOfContents = 0
if c.FilesToTransfer != nil {
c.TotalNumberOfContents += len(c.FilesToTransfer)
}
if c.EmptyFoldersToTransfer != nil {
c.TotalNumberOfContents += len(c.EmptyFoldersToTransfer)
}
if c.Options.HashAlgorithm == "" {
c.Options.HashAlgorithm = "xxhash"
}
log.Debugf("using hash algorithm: %s", c.Options.HashAlgorithm)
if c.Options.NoCompress {
log.Debug("disabling compression")
}
if c.Options.SendingText {
c.Options.Stdout = true
}
fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
folderName := fmt.Sprintf("%d folders", c.TotalNumberFolders)
if len(c.FilesToTransfer) == 1 {
fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
}
totalSize := int64(0)
for i, fi := range c.FilesToTransfer {
totalSize += fi.Size
if len(fi.Name) > c.longestFilename {
c.longestFilename = len(fi.Name)
}
if strings.HasPrefix(fi.Name, "croc-stdin-") && c.Options.SendingText {
c.FilesToTransfer[i].Name, err = utils.RandomFileName()
if err != nil {
return
}
}
}
// check the totalSize does not exceed disk space
// usage := diskusage.NewDiskUsage(".")
// if usage.Available() < uint64(totalSize) {
// return true, fmt.Errorf("not enough disk space")
// }
// c.spinner.Stop()
action := "Accept"
if c.Options.SendingText {
action = "Display"
fname = "text message"
}
if !c.Options.NoPrompt || c.Options.Ask || senderInfo.Ask {
if c.Options.Ask || senderInfo.Ask {
machID, _ := machineid.ID()
fmt.Fprintf(os.Stderr, "\rYour machine id is '%s'.\n%s %s (%s) from '%s'? (Y/n) ", machID, action, fname, utils.ByteCountDecimal(totalSize), senderInfo.MachineID)
} else {
if c.TotalNumberFolders > 0 {
fmt.Fprintf(os.Stderr, "\r%s %s and %s (%s)? (Y/n) ", action, fname, folderName, utils.ByteCountDecimal(totalSize))
} else {
fmt.Fprintf(os.Stderr, "\r%s %s (%s)? (Y/n) ", action, fname, utils.ByteCountDecimal(totalSize))
}
}
choice := strings.ToLower(utils.GetInput(""))
if choice != "" && choice != "y" && choice != "yes" {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeError,
Message: "refusing files",
})
if err != nil {
return false, err
}
return true, fmt.Errorf("refused files")
}
} else {
fmt.Fprintf(os.Stderr, "\rReceiving %s (%s) \n", fname, utils.ByteCountDecimal(totalSize))
}
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)\n", c.ExternalIPConnected)
for i := 0; i < len(c.EmptyFoldersToTransfer); i += 1 {
_, errExists := os.Stat(c.EmptyFoldersToTransfer[i].FolderRemote)
if os.IsNotExist(errExists) {
err = c.createEmptyFolder(i)
if err != nil {
return
}
} else {
isEmpty, _ := isEmptyFolder(c.EmptyFoldersToTransfer[i].FolderRemote)
if !isEmpty {
log.Debug("asking to overwrite")
prompt := fmt.Sprintf("\n%s already has some content in it. \nDo you want"+
" to overwrite it with an empty folder? (y/N) ", c.EmptyFoldersToTransfer[i].FolderRemote)
choice := strings.ToLower(utils.GetInput(prompt))
if choice == "y" || choice == "yes" {
err = c.createEmptyFolder(i)
if err != nil {
return
}
}
}
}
}
// if no files are to be transferred, then we can end the file transfer process
if c.FilesToTransfer == nil {
c.SuccessfulTransfer = true
c.Step3RecipientRequestFile = true
c.Step4FileTransferred = true
errStopTransfer := message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeFinished,
})
if errStopTransfer != nil {
err = errStopTransfer
}
}
log.Debug(c.FilesToTransfer)
c.Step2FileInfoTransferred = true
return
}
func (c *Client) processMessagePake(m message.Message) (err error) {
defer func() {
if r := recover(); r != nil {
if c.stop.gui {
log.Errorf("panic: %v", r)
c.stop.Cancel()
} else {
panic(r)
}
}
}()
log.Debug("received pake payload")
var salt []byte
if c.Options.IsSender {
// initialize curve based on the recipient's choice
log.Debugf("using curve %s", string(m.Bytes2))
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, string(m.Bytes2))
if err != nil {
log.Error(err)
return
}
// update the pake
err = c.Pake.Update(m.Bytes)
if err != nil {
return
}
// generate salt and send it back to recipient
log.Debug("generating salt")
salt = make([]byte, 8)
if _, rerr := rand.Read(salt); err != nil {
log.Errorf("can't generate random numbers: %v", rerr)
return
}
log.Debug("sender sending pake+salt")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypePAKE,
Bytes: c.Pake.Bytes(),
Bytes2: salt,
})
} else {
err = c.Pake.Update(m.Bytes)
if err != nil {
return
}
salt = m.Bytes2
}
// generate key
key, err := c.Pake.SessionKey()
if err != nil {
return err
}
c.Key, _, err = crypt.New(key, salt)
if err != nil {
return err
}
log.Debugf("generated key = %+x with salt %x", c.Key, salt)
// connects to the other ports of the server for transfer
var wg sync.WaitGroup
wg.Add(len(c.Options.RelayPorts))
for i := 0; i < len(c.Options.RelayPorts); i++ {
log.Debugf("port: [%s]", c.Options.RelayPorts[i])
go func(j int) {
defer wg.Done()
var host string
if c.Options.RelayAddress == "127.0.0.1" {
host = c.Options.RelayAddress
} else {
host, _, err = net.SplitHostPort(c.Options.RelayAddress)
if err != nil {
log.Errorf("bad relay address %s", c.Options.RelayAddress)
return
}
}
server := net.JoinHostPort(host, c.Options.RelayPorts[j])
log.Debugf("connecting to %s", server)
c.conn[j+1], _, _, err = tcp.ConnectToTCPServer(
server,
c.Options.RelayPassword,
fmt.Sprintf("%s-%d", c.Options.RoomName, j),
)
if err != nil {
panic(err)
}
log.Debugf("connected to %s", server)
if !c.Options.IsSender {
go c.receiveData(j)
}
}(i)
}
wg.Wait()
if !c.Options.IsSender {
log.Debug("sending external IP")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeExternalIP,
Message: c.ExternalIP,
Bytes: m.Bytes,
})
}
return
}
func (c *Client) processExternalIP(m message.Message) (done bool, err error) {
log.Debugf("received external IP: %+v", m)
if c.Options.IsSender {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeExternalIP,
Message: c.ExternalIP,
})
if err != nil {
return true, err
}
}
if c.ExternalIPConnected == "" {
// it can be preset by the local relay
c.ExternalIPConnected = m.Message
}
log.Debugf("connected as %s -> %s", c.ExternalIP, c.ExternalIPConnected)
c.Step1ChannelSecured = true
return
}
func (c *Client) processMessage(payload []byte) (done bool, err error) {
m, err := message.Decode(c.Key, payload)
if err != nil {
err = fmt.Errorf("problem with decoding: %w", err)
log.Debug(err)
return
}
// only "pake" messages should be unencrypted
// if a non-"pake" message is received unencrypted something
// is weird
if m.Type != message.TypePAKE && c.Key == nil {
err = fmt.Errorf("unencrypted communication rejected")
done = true
return
}
switch m.Type {
case message.TypeFinished:
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeFinished,
})
done = true
c.SuccessfulTransfer = true
return
case message.TypePAKE:
err = c.processMessagePake(m)
if err != nil {
err = fmt.Errorf("pake not successful: %w", err)
log.Debug(err)
}
case message.TypeExternalIP:
done, err = c.processExternalIP(m)
case message.TypeError:
// c.spinner.Stop()
log.Trace("Peer initiates interruption of my loops and goroutines")
c.stop.Cancel()
fmt.Print("\r")
err = fmt.Errorf("peer error: %s", m.Message)
return true, err
case message.TypeFileInfo:
done, err = c.processMessageFileInfo(m)
case message.TypeRecipientReady:
var remoteFile RemoteFileRequest
err = json.Unmarshal(m.Bytes, &remoteFile)
if err != nil {
return
}
c.FilesToTransferCurrentNum = remoteFile.FilesToTransferCurrentNum
c.CurrentFileChunkRanges = remoteFile.CurrentFileChunkRanges
c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)
log.Debugf("current file chunks: %+v", c.CurrentFileChunks)
c.mutex.Lock()
c.chunkMap = make(map[uint64]struct{})
for _, chunk := range c.CurrentFileChunks {
c.chunkMap[uint64(chunk)] = struct{}{}
}
c.mutex.Unlock()
c.Step3RecipientRequestFile = true
if c.Options.Ask {
fmt.Fprintf(os.Stderr, "Send to machine '%s'? (Y/n) ", remoteFile.MachineID)
choice := strings.ToLower(utils.GetInput(""))
if choice != "" && choice != "y" && choice != "yes" {
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeError,
Message: "refusing files",
})
done = true
return
}
}
case message.TypeCloseSender:
c.bar.Finish()
log.Debug("close-sender received...")
c.Step4FileTransferred = false
c.Step3RecipientRequestFile = false
log.Debug("sending close-recipient")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeCloseRecipient,
})
case message.TypeCloseRecipient:
c.Step4FileTransferred = false
c.Step3RecipientRequestFile = false
}
if err != nil {
log.Debugf("got error from processing message: %v", err)
return
}
err = c.updateState()
if err != nil {
log.Debugf("got error from updating state: %v", err)
return
}
return
}
func (c *Client) updateIfSenderChannelSecured() (err error) {
if c.Options.IsSender && c.Step1ChannelSecured && !c.Step2FileInfoTransferred {
var b []byte
machID, _ := machineid.ID()
b, err = json.Marshal(SenderInfo{
FilesToTransfer: c.FilesToTransfer,
EmptyFoldersToTransfer: c.EmptyFoldersToTransfer,
MachineID: machID,
Ask: c.Options.Ask,
TotalNumberFolders: c.TotalNumberFolders,
SendingText: c.Options.SendingText,
NoCompress: c.Options.NoCompress,
HashAlgorithm: c.Options.HashAlgorithm,
})
if err != nil {
log.Error(err)
return
}
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeFileInfo,
Bytes: b,
})
if err != nil {
return
}
c.Step2FileInfoTransferred = true
}
return
}
func (c *Client) recipientInitializeFile() (err error) {
// start initiating the process to receive a new file
log.Debugf("working on file %d", c.FilesToTransferCurrentNum)
// recipient sets the file
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
folderForFile, _ := filepath.Split(pathToFile)
folderForFileBase := filepath.Base(folderForFile)
if folderForFileBase != "." && folderForFileBase != "" {
if err := os.MkdirAll(folderForFile, os.ModePerm); err != nil {
log.Errorf("can't create %s: %v", folderForFile, err)
}
}
var errOpen error
c.CurrentFile, errOpen = os.OpenFile(
pathToFile,
os.O_WRONLY, 0o666)
var truncate bool // default false
c.CurrentFileChunks = []int64{}
c.CurrentFileChunkRanges = []int64{}
if errOpen == nil {
stat, _ := c.CurrentFile.Stat()
truncate = stat.Size() != c.FilesToTransfer[c.FilesToTransferCurrentNum].Size
if !truncate {
// recipient requests the file and chunks (if empty, then should receive all chunks)
// TODO: determine the missing chunks
c.CurrentFileChunkRanges = utils.MissingChunks(
pathToFile,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
models.TCP_BUFFER_SIZE/2,
)
}
} else {
c.CurrentFile, errOpen = os.Create(pathToFile)
if errOpen != nil {
errOpen = fmt.Errorf("could not create %s: %w", pathToFile, errOpen)
log.Error(errOpen)
return errOpen
}
errChmod := os.Chmod(pathToFile, c.FilesToTransfer[c.FilesToTransferCurrentNum].Mode.Perm())
if errChmod != nil {
log.Error(errChmod)
}
truncate = true
}
if truncate {
err := c.CurrentFile.Truncate(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
if err != nil {
err = fmt.Errorf("could not truncate %s: %w", pathToFile, err)
log.Error(err)
return err
}
}
return
}
func (c *Client) recipientGetFileReady(finished bool) (err error) {
if finished {
// TODO: do the last finishing stuff
log.Debug("finished")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeFinished,
})
if err != nil {
panic(err)
}
c.SuccessfulTransfer = true
c.FilesHasFinished[c.FilesToTransferCurrentNum] = struct{}{}
return
}
err = c.recipientInitializeFile()
if err != nil {
return
}
c.TotalSent = 0
c.CurrentFileIsClosed = false
machID, _ := machineid.ID()
bRequest, _ := json.Marshal(RemoteFileRequest{
CurrentFileChunkRanges: c.CurrentFileChunkRanges,
FilesToTransferCurrentNum: c.FilesToTransferCurrentNum,
MachineID: machID,
})
log.Debug("converting to chunk range")
c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)
if !finished {
// setup the progressbar
c.setBar()
}
log.Debugf("sending recipient ready with %d chunks", len(c.CurrentFileChunks))
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeRecipientReady,
Bytes: bRequest,
})
if err != nil {
return
}
c.Step3RecipientRequestFile = true
return
}
func formatDescription(description string) string {
const (
// Reserve extra room for variable progress metadata such as [elapsed:remaining].
progressMetaWidth = 78
minDescription = 12
defaultTermWidth = 80
)
width, _, err := term.GetSize(int(os.Stderr.Fd()))
if err != nil || width <= 0 {
width, _, err = term.GetSize(int(os.Stdout.Fd()))
}
if err != nil || width <= 0 {
if envColumns, convErr := strconv.Atoi(os.Getenv("COLUMNS")); convErr == nil && envColumns > 0 {
width = envColumns
} else {
width = defaultTermWidth
}
}
maxDescription := width - progressMetaWidth
if maxDescription < minDescription {
maxDescription = minDescription
}
runes := []rune(description)
if len(runes) > maxDescription {
if maxDescription <= 3 {
return string(runes[:maxDescription])
}
return string(runes[:maxDescription-3]) + "..."
}
return description
}
func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error) {
log.Debugf("touching file with folder / name")
if !utils.Exists(fileInfo.FolderRemote) {
err = os.MkdirAll(fileInfo.FolderRemote, os.ModePerm)
if err != nil {
log.Error(err)
return
}
}
pathToFile := path.Join(fileInfo.FolderRemote, fileInfo.Name)
if fileInfo.Symlink != "" {
log.Debug("creating symlink")
// remove symlink if it exists
if _, errExists := os.Lstat(pathToFile); errExists == nil {
os.Remove(pathToFile)
}
err = os.Symlink(fileInfo.Symlink, pathToFile)
if err != nil {
return
}
} else {
emptyFile, errCreate := os.Create(pathToFile)
if errCreate != nil {
log.Error(errCreate)
err = errCreate
return
}
emptyFile.Close()
}
// setup the progressbar
description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name)
if len(c.FilesToTransfer) == 1 {
description = c.FilesToTransfer[i].Name
// description = ""
} else {
description = " " + description
}
c.bar = progressbar.NewOptions64(1,
progressbar.OptionOnCompletion(func() {
c.fmtPrintUpdate()
}),
progressbar.OptionSetWidth(20),
progressbar.OptionSetDescription(formatDescription(description)),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionShowBytes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetVisibility(!c.Options.SendingText),
)
c.bar.Finish()
return
}
func (c *Client) updateIfRecipientHasFileInfo() (err error) {
if c.Options.IsSender || !c.Step2FileInfoTransferred || c.Step3RecipientRequestFile {
return
}
// find the next file to transfer and send that number
// if the files are the same size, then look for missing chunks
finished := true
for i, fileInfo := range c.FilesToTransfer {
if _, ok := c.FilesHasFinished[i]; ok {
continue
}
if i < c.FilesToTransferCurrentNum {
continue
}
log.Debugf("checking %+v", fileInfo)
recipientFileInfo, errRecipientFile := os.Lstat(path.Join(fileInfo.FolderRemote, fileInfo.Name))
var errHash error
var fileHash []byte
if errRecipientFile == nil && recipientFileInfo.Size() == fileInfo.Size {
// the file exists, but is same size, so hash it
fileHash, errHash = utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name), c.Options.HashAlgorithm)
}
if fileInfo.Size == 0 || fileInfo.Symlink != "" {
err = c.createEmptyFileAndFinish(fileInfo, i)
if err != nil {
return
} else {
c.numberOfTransferredFiles++
}
continue
}
log.Debugf("%s %+x %+x %+v", fileInfo.Name, fileHash, fileInfo.Hash, errHash)
if !bytes.Equal(fileHash, fileInfo.Hash) {
log.Debugf("hashed %s to %x using %s", fileInfo.Name, fileHash, c.Options.HashAlgorithm)
log.Debugf("hashes are not equal %x != %x", fileHash, fileInfo.Hash)
if errHash == nil && !c.Options.Overwrite && errRecipientFile == nil && !strings.HasPrefix(fileInfo.Name, "croc-stdin-") && !c.Options.SendingText {
missingChunks := utils.ChunkRangesToChunks(utils.MissingChunks(
path.Join(fileInfo.FolderRemote, fileInfo.Name),
fileInfo.Size,
models.TCP_BUFFER_SIZE/2,
))
percentDone := 100 - float64(len(missingChunks)*models.TCP_BUFFER_SIZE/2)/float64(fileInfo.Size)*100
log.Debug("asking to overwrite")
prompt := fmt.Sprintf("\nOverwrite '%s'? (y/N) (use --overwrite to omit) ", path.Join(fileInfo.FolderRemote, fileInfo.Name))
if percentDone < 99 {
prompt = fmt.Sprintf("\nResume '%s' (%2.1f%%)? (y/N) (use --overwrite to omit) ", path.Join(fileInfo.FolderRemote, fileInfo.Name), percentDone)
}
choice := strings.ToLower(utils.GetInput(prompt))
if choice != "y" && choice != "yes" {
fmt.Fprintf(os.Stderr, "Skipping '%s'\n", path.Join(fileInfo.FolderRemote, fileInfo.Name))
continue
}
}
} else {
log.Debugf("hashes are equal %x == %x", fileHash, fileInfo.Hash)
if !fileInfo.ModTime.IsZero() {
if err := os.Chtimes(path.Join(fileInfo.FolderRemote, fileInfo.Name), fileInfo.ModTime, fileInfo.ModTime); err != nil {
log.Warnf("chtimes %v: %v", fileInfo.ModTime, err)
} else {
log.Debugf("chtimes %v", fileInfo.ModTime)
}
}
}
if errHash != nil {
// probably can't find, its okay
log.Debug(errHash)
}
if errHash != nil || !bytes.Equal(fileHash, fileInfo.Hash) {
finished = false
c.FilesToTransferCurrentNum = i
c.numberOfTransferredFiles++
newFolder, _ := filepath.Split(fileInfo.FolderRemote)
if newFolder != c.LastFolder && len(c.FilesToTransfer) > 0 && !c.Options.SendingText && newFolder != "./" {
fmt.Fprintf(os.Stderr, "\r%s\n", newFolder)
}
c.LastFolder = newFolder
break
}
}
c.recipientGetFileReady(finished)
return
}
func (c *Client) fmtPrintUpdate() {
c.finishedNum++
if c.TotalNumberOfContents > 1 {
fmt.Fprintf(os.Stderr, " %d/%d\n", c.finishedNum, c.TotalNumberOfContents)
} else {
fmt.Fprintf(os.Stderr, "\n")
}
}
func (c *Client) updateState() (err error) {
err = c.updateIfSenderChannelSecured()
if err != nil {
return
}
err = c.updateIfRecipientHasFileInfo()
if err != nil {
return
}
if c.Options.IsSender && c.Step3RecipientRequestFile && !c.Step4FileTransferred {
log.Debug("start sending data!")
if !c.firstSend {
fmt.Fprintf(os.Stderr, "\nSending (->%s)\n", c.ExternalIPConnected)
c.firstSend = true
// if there are empty files, show them as already have been transferred now
for i := range c.FilesToTransfer {
if c.FilesToTransfer[i].Size == 0 {
// setup the progressbar and takedown the progress bar for empty files
description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name)
if len(c.FilesToTransfer) == 1 {
description = c.FilesToTransfer[i].Name
// description = ""
}
c.bar = progressbar.NewOptions64(1,
progressbar.OptionOnCompletion(func() {
c.fmtPrintUpdate()
}),
progressbar.OptionSetWidth(20),
progressbar.OptionSetDescription(formatDescription(description)),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionShowBytes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetVisibility(!c.Options.SendingText),
)
c.bar.Finish()
}
}
}
c.Step4FileTransferred = true
// setup the progressbar
c.setBar()
c.TotalSent = 0
c.CurrentFileIsClosed = false
log.Debug("beginning sending comms")
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderSource,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
c.fread, err = os.Open(pathToFile)
c.numfinished = 0
if err != nil {
return
}
for i := 0; i < len(c.Options.RelayPorts); i++ {
log.Debugf("starting sending over comm %d", i)
go c.sendData(i)
}
}
return
}
func (c *Client) setBar() {
description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name)
folder, _ := filepath.Split(c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote)
if folder == "./" {
description = c.FilesToTransfer[c.FilesToTransferCurrentNum].Name
} else if !c.Options.IsSender {
description = " " + description
}
c.bar = progressbar.NewOptions64(
c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
progressbar.OptionOnCompletion(func() {
c.fmtPrintUpdate()
}),
progressbar.OptionSetWidth(20),
progressbar.OptionSetDescription(formatDescription(description)),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionShowBytes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSetVisibility(!c.Options.SendingText),
)
byteToDo := int64(len(c.CurrentFileChunks) * models.TCP_BUFFER_SIZE / 2)
if byteToDo > 0 {
bytesDone := c.FilesToTransfer[c.FilesToTransferCurrentNum].Size - byteToDo
log.Debug(byteToDo)
log.Debug(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
log.Debug(bytesDone)
if bytesDone > 0 {
c.bar.Add64(bytesDone)
}
}
}
func (c *Client) receiveData(i int) {
defer func() {
if r := recover(); r != nil {
if c.stop.gui {
log.Errorf("panic: %v", r)
c.stop.Cancel()
} else {
panic(r)
}
}
}()
log.Tracef("%d receiving data", i)
for {
data, err := c.conn[i+1].Receive()
if err != nil {
break
}
if bytes.Equal(data, []byte{1}) {
log.Trace("got ping")
continue
}
data, err = crypt.Decrypt(data, c.Key)
if err != nil {
panic(err)
}
if !c.Options.NoCompress {
data = compress.Decompress(data)
}
// get position
var position uint64
rbuf := bytes.NewReader(data[:8])
err = binary.Read(rbuf, binary.LittleEndian, &position)
if err != nil {
panic(err)
}
positionInt64 := int64(position)
c.mutex.Lock()
if c.CurrentFileIsClosed || c.CurrentFile == nil {
c.mutex.Unlock()
log.Tracef("was closed %d", i)
return
}
if err := c.ctxErr(); err != nil {
c.CurrentFileIsClosed = true
defer c.mutex.Unlock()
log.Tracef("stopping: %v", err)
if err := c.CurrentFile.Close(); err != nil {
log.Tracef("closing %s: %v", c.CurrentFile.Name(), err)
} else {
log.Tracef("Successful closing %s", c.CurrentFile.Name())
}
log.Tracef("sending close-sender")
if sendErr := message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeCloseSender,
}); sendErr != nil {
log.Tracef("sending close-sender: %v", sendErr)
}
return
}
_, err = c.CurrentFile.WriteAt(data[8:], positionInt64)
if err != nil {
panic(err)
}
c.bar.Add(len(data[8:]))
c.TotalSent += int64(len(data[8:]))
c.TotalChunksTransferred++
// log.Debug(len(c.CurrentFileChunks), c.TotalChunksTransferred, c.TotalSent, c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
if !c.CurrentFileIsClosed && (c.TotalChunksTransferred == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) {
c.CurrentFileIsClosed = true
log.Debug("finished receiving!")
if err = c.CurrentFile.Close(); err != nil {
log.Debugf("error closing %s: %v", c.CurrentFile.Name(), err)
} else {
log.Debugf("Successful closing %s", c.CurrentFile.Name())
}
if c.Options.Stdout || c.Options.SendingText {
pathToFile := path.Join(
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
)
b, _ := os.ReadFile(pathToFile)
fmt.Print(string(b))
}
log.Debug("sending close-sender")
err = message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeCloseSender,
})
if err != nil {
panic(err)
}
}
c.mutex.Unlock()
}
}
func (c *Client) sendData(i int) {
defer func() {
if r := recover(); r != nil {
if c.stop.gui {
log.Errorf("panic: %v", r)
c.stop.Cancel()
} else {
panic(r)
}
}
log.Debugf("finished with %d", i)
c.numfinished++
if c.numfinished == len(c.Options.RelayPorts) {
log.Debug("closing file")
if err := c.fread.Close(); err != nil {
log.Errorf("error closing file: %v", err)
}
}
}()
var readingPos int64
pos := uint64(0)
curi := float64(0)
for {
if err := c.ctxErr(); err != nil {
log.Tracef("stopping send %d: %v", i, err)
return
}
// Read file
var n int
var errRead error
if math.Mod(curi, float64(len(c.Options.RelayPorts))) == float64(i) {
data := make([]byte, models.TCP_BUFFER_SIZE/2)
n, errRead = c.fread.ReadAt(data, readingPos)
if c.limiter != nil {
r := c.limiter.ReserveN(time.Now(), n)
log.Debugf("Limiting Upload for %d", r.Delay())
time.Sleep(r.Delay())
}
if n > 0 {
// check to see if this is a chunk that the recipient wants
usableChunk := true
c.mutex.Lock()
if len(c.chunkMap) != 0 {
if _, ok := c.chunkMap[pos]; !ok {
usableChunk = false
} else {
delete(c.chunkMap, pos)
}
}
c.mutex.Unlock()
if usableChunk {
// log.Debugf("sending chunk %d", pos)
posByte := make([]byte, 8)
binary.LittleEndian.PutUint64(posByte, pos)
var err error
var dataToSend []byte
if c.Options.NoCompress {
dataToSend, err = crypt.Encrypt(
append(posByte, data[:n]...),
c.Key,
)
} else {
dataToSend, err = crypt.Encrypt(
compress.Compress(
append(posByte, data[:n]...),
),
c.Key,
)
}
if err != nil {
panic(err)
}
err = c.conn[i+1].Send(dataToSend)
if err != nil {
panic(err)
}
c.bar.Add(n)
c.TotalSent += int64(n)
// time.Sleep(100 * time.Millisecond)
}
}
}
if n == 0 {
n = models.TCP_BUFFER_SIZE / 2
}
readingPos += int64(n)
curi++
pos += uint64(n)
if errRead != nil {
if errRead == io.EOF {
break
}
panic(errRead)
}
}
}
// isExecutableInPath checks for the availability of an executable
func isExecutableInPath(executableName string) bool {
_, err := exec.LookPath(executableName)
return err == nil
}
// copyToClipboard tries to send the code to the operating system clipboard
func copyToClipboard(str string, quiet bool, extendedClipboard bool) {
var cmd *exec.Cmd
switch runtime.GOOS {
// Windows should always have clip.exe in PATH by default
case "windows":
cmd = exec.Command("clip")
// MacOS uses pbcopy
case "darwin":
cmd = exec.Command("pbcopy")
// These Unix-like systems are likely using Xorg(with xclip or xsel) or Wayland(with wl-copy or waycopy)
case "linux", "android", "hurd", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "illumos", "plan9":
if os.Getenv("XDG_SESSION_TYPE") == "wayland" { // Wayland running
if isExecutableInPath("wl-copy") {
cmd = exec.Command("wl-copy")
} else if isExecutableInPath("waycopy") {
cmd = exec.Command("waycopy")
}
} else if os.Getenv("XDG_SESSION_TYPE") == "x11" || os.Getenv("XDG_SESSION_TYPE") == "xorg" { // Xorg running
if isExecutableInPath("xclip") {
cmd = exec.Command("xclip", "-selection", "clipboard")
}
} else if isExecutableInPath("xsel") {
cmd = exec.Command("xsel", "-b")
} else if isExecutableInPath("termux-clipboard-set") {
cmd = exec.Command("termux-clipboard-set")
}
default:
return
}
// Nothing has been found
if cmd == nil {
return
}
// Sending stdin into the available clipboard program
cmd.Stdin = bytes.NewReader([]byte(str))
if err := cmd.Run(); err != nil {
log.Debugf("error copying to clipboard: %v", err)
return
}
if !quiet {
if extendedClipboard {
fmt.Fprintf(os.Stderr, "Command copied to clipboard!\n")
} else {
fmt.Fprintf(os.Stderr, "Code copied to clipboard!\n")
}
}
}
================================================
FILE: src/croc/croc_test.go
================================================
package croc
import (
"context"
"fmt"
"math/rand"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/schollz/croc/v10/src/tcp"
log "github.com/schollz/logger"
"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)
}
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{"../../README.md"}, false, false, []string{})
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 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, []string{})
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 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, []string{})
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, []string{})
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 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: "ed25519",
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: "ed25519",
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, []string{})
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 TestCrocError(t *testing.T) {
content := []byte("temporary file's content")
tmpfile, err := os.CreateTemp("", "example")
if err != nil {
panic(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)
}
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, []string{})
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 TestReceiverStdoutWithInvalidSecret(t *testing.T) {
// Test for issue: panic when receiving with --stdout and invalid CROC_SECRET
// This should fail gracefully without panicking
log.SetLevel("warn")
receiver, err := New(Options{
IsSender: false,
SharedSecret: "invalid-secret-12345",
Debug: true,
RelayAddress: "127.0.0.1:8281",
RelayPassword: "pass123",
Stdout: true, // This is the key flag that triggered the panic
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Errorf("failed to create receiver: %v", err)
return
}
// This should fail but not panic
err = receiver.Receive()
// We expect an error since the secret is invalid and no sender is present
assert.NotNil(t, err)
log.Debugf("Expected error occurred: %v", 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 hashed(c *Client) bool {
if len(c.FilesToTransfer) == 0 {
return false
}
for _, file := range c.FilesToTransfer {
if len(file.Hash) == 0 {
return false
}
}
return true
}
func waitHashed(sender *Client) (err error) {
err = fmt.Errorf("not hashed")
for i := 0; i < 300; i++ { // Max 3 seconds
if hashed(sender) {
time.Sleep(100 * time.Millisecond)
return nil
}
time.Sleep(10 * time.Millisecond)
}
return
}
func createTestFile(t *testing.T, size int) (string, func()) {
tempFile, err := os.CreateTemp("", "test-*.dat")
if err != nil {
t.Fatal(err)
}
data := make([]byte, size)
for i := 0; i < size; i++ {
data[i] = byte(i % 256)
}
if _, err := tempFile.Write(data); err != nil {
tempFile.Close()
os.Remove(tempFile.Name())
t.Fatal(err)
}
if err := tempFile.Close(); err != nil {
os.Remove(tempFile.Name())
t.Fatal(err)
}
return tempFile.Name(), func() {
os.Remove(tempFile.Name())
}
}
func TestBase(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
go tcp.Run("debug", "127.0.0.1", "8286", "pass123", "8287")
time.Sleep(200 * time.Millisecond)
go tcp.Run("debug", "127.0.0.1", "8287", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := New(Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8286",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := New(Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8286",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
t.Fatal(err)
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
func TestCtx(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8288", "pass123", "8289")
time.Sleep(200 * time.Millisecond)
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8289", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := NewCtx(ctx, Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8288",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := NewCtx(ctx, Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8288",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
t.Fatal(err)
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
func validErrors(err error) bool {
s := err.Error()
return strings.Contains(s, "cancel") ||
strings.Contains(s, "context") ||
strings.Contains(s, "reset") ||
strings.Contains(s, "broken") ||
strings.Contains(s, "refusing") ||
strings.Contains(s, "EOF") ||
strings.Contains(s, "closed")
}
func result(t *testing.T, err error) {
if err != nil {
if validErrors(err) {
t.Logf("Expected error during context cancellation: %v", err)
} else {
t.Errorf("Unexpected error during cancellation: %v", err)
}
return
}
t.Error("Transfer should have been interrupted by context cancellation")
}
func TestAllCtx(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8290", "pass123", "8291")
time.Sleep(200 * time.Millisecond)
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8291", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := NewCtx(ctx, Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8290",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := NewCtx(ctx, Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8290",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
cancel()
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
result(t, err)
case <-done:
t.Error("Transfer should have been interrupted by context cancellation")
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
func TestSendCtx(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
ctx2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8292", "pass123", "8293")
time.Sleep(200 * time.Millisecond)
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8293", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := NewCtx(ctx2, Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8292",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := NewCtx(ctx, Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8292",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
cancel2()
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
result(t, err)
case <-done:
t.Error("Transfer should have been interrupted by context cancellation")
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
func TestReceiveCtx(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
ctx2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8294", "pass123", "8295")
time.Sleep(200 * time.Millisecond)
go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8295", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := NewCtx(ctx, Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8294",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := NewCtx(ctx2, Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8294",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
cancel2()
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
result(t, err)
case <-done:
t.Error("Transfer should have been interrupted by context cancellation")
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
func TestRunCtx(t *testing.T) {
tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
defer cleanup()
receivedFile := filepath.Base(tempFile)
defer os.Remove(receivedFile)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
ctx2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
go tcp.RunCtx(ctx2, "debug", "127.0.0.1", "8296", "pass123", "8297")
time.Sleep(200 * time.Millisecond)
go tcp.RunCtx(ctx2, "debug", "127.0.0.1", "8297", "pass123")
time.Sleep(200 * time.Millisecond)
uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
sender, err := NewCtx(ctx, Options{
IsSender: true,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8296",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
GitIgnore: false,
})
if err != nil {
t.Fatalf("Create sender failed: %v", err)
}
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
if errGet != nil {
t.Fatalf("Get file info failed: %v", errGet)
}
receiver, err := NewCtx(ctx, Options{
IsSender: false,
SharedSecret: uniqueSecret,
Debug: true,
RelayAddress: "127.0.0.1:8296",
RelayPassword: "pass123",
Stdout: false,
NoPrompt: true,
DisableLocal: true,
Curve: "siec",
Overwrite: true,
})
if err != nil {
t.Fatalf("Create receiver failed: %v", err)
}
fatalErr := make(chan error, 1)
failTest := func(err error) {
select {
case fatalErr <- err:
default:
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Warn("Send")
if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
failTest(fmt.Errorf("Send failed: %w", err))
}
}()
go func() {
defer wg.Done()
if err := waitHashed(sender); err != nil {
failTest(fmt.Errorf("waitHashed failed: %w", err))
return
}
log.Warn("Receive")
if err := receiver.Receive(); err != nil {
failTest(fmt.Errorf("Receive failed: %w", err))
}
}()
go func() {
for i := 0; i < 3000; i++ {
if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
time.Sleep(time.Millisecond)
if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
log.Warn("Step2FileInfoTransferred reached")
cancel2()
return
}
log.Warn("Step1ChannelSecured reached")
}
time.Sleep(time.Millisecond)
}
}()
done := make(chan bool, 1)
go func() {
wg.Wait()
done <- true
}()
select {
case err := <-fatalErr:
result(t, err)
case <-done:
t.Error("Transfer should have been interrupted by context cancellation")
case <-time.After(5 * time.Second):
t.Fatal("Test timeout after 5 seconds")
}
}
================================================
FILE: src/croc/ctx.go
================================================
// ctx.go
package croc
import (
"context"
"time"
"github.com/schollz/croc/v10/src/message"
"github.com/schollz/croc/v10/src/tcp"
"github.com/schollz/croc/v10/src/utils"
log "github.com/schollz/logger"
)
// stop manages graceful shutdown
type stop struct {
ctx context.Context
cancel context.CancelFunc
stopChan chan struct{} //peerdiscovery
run func(debugLevel string, host string, port string, password string, banner ...string) (err error)
hash func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error)
gui bool
}
// newStop creates a new stop manager instance
func newStop(ctx context.Context) *stop {
s := &stop{
stopChan: make(chan struct{}),
run: tcp.Run,
hash: utils.HashFile,
}
if ctx == nil {
ctx = context.Background()
}
s.ctx, s.cancel = context.WithCancel(ctx)
return s
}
func (s *stop) done() {
<-s.ctx.Done()
time.Sleep(time.Millisecond)
close(s.stopChan)
log.Trace("croc done")
}
// NewCtx creates a client with context support
func NewCtx(ctx context.Context, ops Options) (*Client, error) {
// Create a regular c
c, err := New(ops)
if err != nil {
return nil, err
}
c.stop = newStop(ctx)
c.stop.gui = true
c.stop.run = func(debugLevel string, host string, port string, password string, banner ...string) (err error) {
return tcp.RunCtx(c.stop.ctx, debugLevel, host, port, password, banner...)
}
c.stop.hash = func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {
return utils.HashFileCtx(c.stop.ctx, fname, algorithm, showProgress...)
}
go func() {
select {
case <-ctx.Done():
log.Trace("parent context canceled")
c.SendError()
case <-c.stopChan:
// for stop goroutine
}
log.Trace("croc NewCtx done")
}()
return c, nil
}
// ctxErr checks whether it is necessary to interrupt my loops and goroutines
func (s *stop) ctxErr() error {
select {
case <-s.ctx.Done():
return s.ctx.Err()
default:
return nil
}
}
// Cancel initiates interruption of my loops and goroutines
func (s *stop) Cancel() {
log.Trace("croc Cancel")
if s.cancel != nil {
s.cancel()
s.cancel = nil
}
}
// SendError tells the peer to interrupt their loops and goroutines
func (c *Client) SendError() {
if c.Key != nil && len(c.conn) > 0 && c.conn[0] != nil {
message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypeError,
Message: "refusing files",
})
time.Sleep(time.Millisecond)
}
}
================================================
FILE: src/crypt/crypt.go
================================================
package crypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"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")
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
}
key = pbkdf2.Key(passphrase, salt, 100, 32, sha256.New)
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
// 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)
}
b, err := aes.NewCipher(key)
if err != nil {
return
}
aesgcm, err := cipher.NewGCM(b)
if err != nil {
return
}
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
encrypted = append(ivBytes, encrypted...)
return
}
// Decrypt 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 {
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
}
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 decrypt ChaCha20-Poly1305 using the pre-generated key
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []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.
plaintext, err = aead.Open(nil, nonce, ciphertext, nil)
return
}
================================================
FILE: src/crypt/crypt_test.go
================================================
package crypt
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func BenchmarkEncrypt(b *testing.B) {
bob, _, _ := New([]byte("password"), nil)
for i := 0; i < b.N; i++ {
Encrypt([]byte("hello, world"), bob)
}
}
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)
}
================================================
FILE: src/diskusage/diskusage.go
================================================
//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())
}
================================================
FILE: src/diskusage/diskusage_test.go
================================================
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, "%")
}
================================================
FILE: src/diskusage/diskusage_windows.go
================================================
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())
}
================================================
FILE: src/install/Makefile
================================================
# 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
================================================
FILE: src/install/bash_autocomplete
================================================
: ${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
================================================
FILE: src/install/default.txt
================================================
#!/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/doas (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
gitextract_w4v75_0t/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ ├── release.yml
│ ├── stale.yml
│ └── winget.yml
├── .gitignore
├── .goreleaser.yml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── croc-entrypoint.sh
├── croc.service
├── go.mod
├── go.sum
├── main.go
└── src/
├── cli/
│ └── cli.go
├── comm/
│ ├── comm.go
│ └── comm_test.go
├── compress/
│ ├── compress.go
│ └── compress_test.go
├── croc/
│ ├── croc.go
│ ├── croc_test.go
│ └── ctx.go
├── crypt/
│ ├── crypt.go
│ └── crypt_test.go
├── diskusage/
│ ├── diskusage.go
│ ├── diskusage_test.go
│ └── diskusage_windows.go
├── install/
│ ├── Makefile
│ ├── bash_autocomplete
│ ├── default.txt
│ ├── prepare-sources-tarball.sh
│ ├── updateversion.go
│ ├── upload-src-tarball.sh
│ └── zsh_autocomplete
├── message/
│ ├── message.go
│ └── message_test.go
├── mnemonicode/
│ ├── mnemonicode.go
│ ├── mnemonicode_test.go
│ └── wordlist.go
├── models/
│ ├── constants.go
│ └── models_test.go
├── tcp/
│ ├── ctx.go
│ ├── defaults.go
│ ├── options.go
│ ├── tcp.go
│ └── tcp_test.go
└── utils/
├── ctx.go
├── utils.go
└── utils_test.go
SYMBOL INDEX (299 symbols across 30 files)
FILE: main.go
function main (line 17) | func main() {
FILE: src/cli/cli.go
function Run (line 32) | func Run() (err error) {
function setDebugLevel (line 230) | func setDebugLevel(c *cli.Context) {
function getSendConfigFile (line 249) | func getSendConfigFile(requireValidPath bool) string {
function getClassicConfigFile (line 258) | func getClassicConfigFile(requireValidPath bool) string {
function getReceiveConfigFile (line 267) | func getReceiveConfigFile(requireValidPath bool) (string, error) {
function determinePass (line 276) | func determinePass(c *cli.Context) (pass string) {
function send (line 285) | func send(c *cli.Context) (err error) {
function getStdin (line 512) | func getStdin() (fnames []string, err error) {
function makeTempFileWithString (line 529) | func makeTempFileWithString(s string) (fnames []string, err error) {
function saveConfig (line 548) | func saveConfig(c *cli.Context, crocOptions croc.Options) {
type TabComplete (line 581) | type TabComplete struct
method Do (line 583) | func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
function receive (line 611) | func receive(c *cli.Context) (err error) {
function relay (line 779) | func relay(c *cli.Context) (err error) {
FILE: src/comm/comm.go
constant maxReadMessageSize (line 24) | maxReadMessageSize = 64 * 1024 * 1024
type Comm (line 27) | type Comm struct
method Connection (line 110) | func (c *Comm) Connection() net.Conn {
method Close (line 115) | func (c *Comm) Close() {
method Write (line 121) | func (c *Comm) Write(b []byte) (n int, err error) {
method Read (line 141) | func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
method Send (line 200) | func (c *Comm) Send(message []byte) (err error) {
method Receive (line 206) | func (c *Comm) Receive() (b []byte, err error) {
function NewConnection (line 32) | func NewConnection(address string, timelimit ...time.Duration) (c *Comm,...
function New (line 94) | func New(c net.Conn) *Comm {
FILE: src/comm/comm_test.go
function TestComm (line 15) | func TestComm(t *testing.T) {
function TestReceiveRejectsOversizedMessage (line 83) | func TestReceiveRejectsOversizedMessage(t *testing.T) {
FILE: src/compress/compress.go
function CompressWithOption (line 12) | func CompressWithOption(src []byte, level int) []byte {
function Compress (line 19) | func Compress(src []byte) []byte {
function Decompress (line 26) | func Decompress(src []byte) []byte {
function compress (line 34) | func compress(src []byte, dest io.Writer, level int) {
function decompress (line 47) | func decompress(src io.Reader, dest io.Writer) {
FILE: src/compress/compress_test.go
function BenchmarkCompressLevelMinusTwo (line 40) | func BenchmarkCompressLevelMinusTwo(b *testing.B) {
function BenchmarkCompressLevelNine (line 46) | func BenchmarkCompressLevelNine(b *testing.B) {
function BenchmarkCompressLevelMinusTwoBinary (line 52) | func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
function BenchmarkCompressLevelNineBinary (line 62) | func BenchmarkCompressLevelNineBinary(b *testing.B) {
function TestCompress (line 72) | func TestCompress(t *testing.T) {
FILE: src/croc/croc.go
function init (line 49) | func init() {
function Debug (line 54) | func Debug(debug bool) {
type Options (line 63) | type Options struct
type SimpleMessage (line 97) | type SimpleMessage struct
type Client (line 103) | type Client struct
method sendCollectFiles (line 512) | func (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {
method setupLocalRelay (line 570) | func (c *Client) setupLocalRelay() {
method broadcastOnLocalNetwork (line 599) | func (c *Client) broadcastOnLocalNetwork(useipv6 bool) {
method transferOverLocalRelay (line 629) | func (c *Client) transferOverLocalRelay(errchan chan<- error) {
method Send (line 669) | func (c *Client) Send(filesInfo []FileInfo, emptyFoldersToTransfer []F...
method Receive (line 893) | func (c *Client) Receive() (err error) {
method transfer (line 1162) | func (c *Client) transfer() (err error) {
method createEmptyFolder (line 1276) | func (c *Client) createEmptyFolder(i int) (err error) {
method processMessageFileInfo (line 1298) | func (c *Client) processMessageFileInfo(m message.Message) (done bool,...
method processMessagePake (line 1441) | func (c *Client) processMessagePake(m message.Message) (err error) {
method processExternalIP (line 1546) | func (c *Client) processExternalIP(m message.Message) (done bool, err ...
method processMessage (line 1566) | func (c *Client) processMessage(payload []byte) (done bool, err error) {
method updateIfSenderChannelSecured (line 1663) | func (c *Client) updateIfSenderChannelSecured() (err error) {
method recipientInitializeFile (line 1694) | func (c *Client) recipientInitializeFile() (err error) {
method recipientGetFileReady (line 1753) | func (c *Client) recipientGetFileReady(finished bool) (err error) {
method createEmptyFileAndFinish (line 1836) | func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (e...
method updateIfRecipientHasFileInfo (line 1889) | func (c *Client) updateIfRecipientHasFileInfo() (err error) {
method fmtPrintUpdate (line 1975) | func (c *Client) fmtPrintUpdate() {
method updateState (line 1984) | func (c *Client) updateState() (err error) {
method setBar (line 2050) | func (c *Client) setBar() {
method receiveData (line 2084) | func (c *Client) receiveData(i int) {
method sendData (line 2183) | func (c *Client) sendData(i int) {
type Chunk (line 158) | type Chunk struct
type FileInfo (line 164) | type FileInfo struct
type RemoteFileRequest (line 180) | type RemoteFileRequest struct
type SenderInfo (line 187) | type SenderInfo struct
function New (line 199) | func New(ops Options) (c *Client, err error) {
type TransferOptions (line 272) | type TransferOptions struct
function isEmptyFolder (line 278) | func isEmptyFolder(folderPath string) (bool, error) {
function gitWalk (line 294) | func gitWalk(dir string, gitObj *ignore.GitIgnore, files map[string]bool) {
function isChild (line 325) | func isChild(parentPath, childPath string) bool {
function GetFilesInfo (line 335) | func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclu...
function showReceiveCommandQrCode (line 885) | func showReceiveCommandQrCode(command string) {
function formatDescription (line 1801) | func formatDescription(description string) string {
function isExecutableInPath (line 2285) | func isExecutableInPath(executableName string) bool {
function copyToClipboard (line 2291) | func copyToClipboard(str string, quiet bool, extendedClipboard bool) {
FILE: src/croc/croc_test.go
function init (line 21) | func init() {
function TestCrocReadme (line 32) | func TestCrocReadme(t *testing.T) {
function TestCrocEmptyFolder (line 96) | func TestCrocEmptyFolder(t *testing.T) {
function TestCrocSymlink (line 162) | func TestCrocSymlink(t *testing.T) {
function TestCrocIgnoreGit (line 238) | func TestCrocIgnoreGit(t *testing.T) {
function TestCrocLocal (line 265) | func TestCrocLocal(t *testing.T) {
function TestCrocError (line 334) | func TestCrocError(t *testing.T) {
function TestReceiverStdoutWithInvalidSecret (line 374) | func TestReceiverStdoutWithInvalidSecret(t *testing.T) {
function TestCleanUp (line 402) | func TestCleanUp(t *testing.T) {
function hashed (line 433) | func hashed(c *Client) bool {
function waitHashed (line 445) | func waitHashed(sender *Client) (err error) {
function createTestFile (line 457) | func createTestFile(t *testing.T, size int) (string, func()) {
function TestBase (line 484) | func TestBase(t *testing.T) {
function TestCtx (line 598) | func TestCtx(t *testing.T) {
function validErrors (line 715) | func validErrors(err error) bool {
function result (line 726) | func result(t *testing.T, err error) {
function TestAllCtx (line 738) | func TestAllCtx(t *testing.T) {
function TestSendCtx (line 857) | func TestSendCtx(t *testing.T) {
function TestReceiveCtx (line 979) | func TestReceiveCtx(t *testing.T) {
function TestRunCtx (line 1101) | func TestRunCtx(t *testing.T) {
FILE: src/croc/ctx.go
type stop (line 15) | type stop struct
method done (line 39) | func (s *stop) done() {
method ctxErr (line 77) | func (s *stop) ctxErr() error {
method Cancel (line 87) | func (s *stop) Cancel() {
function newStop (line 25) | func newStop(ctx context.Context) *stop {
function NewCtx (line 47) | func NewCtx(ctx context.Context, ops Options) (*Client, error) {
method SendError (line 96) | func (c *Client) SendError() {
FILE: src/crypt/crypt.go
function New (line 17) | func New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, e...
function Encrypt (line 37) | func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {
function Decrypt (line 59) | func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {
function NewArgon2 (line 79) | func NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, sa...
function EncryptChaCha (line 100) | func EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte...
function DecryptChaCha (line 113) | func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []b...
FILE: src/crypt/crypt_test.go
function BenchmarkEncrypt (line 10) | func BenchmarkEncrypt(b *testing.B) {
function BenchmarkDecrypt (line 17) | func BenchmarkDecrypt(b *testing.B) {
function BenchmarkNewPbkdf2 (line 27) | func BenchmarkNewPbkdf2(b *testing.B) {
function BenchmarkNewArgon2 (line 34) | func BenchmarkNewArgon2(b *testing.B) {
function BenchmarkEncryptChaCha (line 41) | func BenchmarkEncryptChaCha(b *testing.B) {
function BenchmarkDecryptChaCha (line 48) | func BenchmarkDecryptChaCha(b *testing.B) {
function TestEncryption (line 58) | func TestEncryption(t *testing.T) {
function TestEncryptionChaCha (line 89) | func TestEncryptionChaCha(t *testing.T) {
FILE: src/diskusage/diskusage.go
type DiskUsage (line 11) | type DiskUsage struct
method Free (line 27) | func (du *DiskUsage) Free() uint64 {
method Available (line 32) | func (du *DiskUsage) Available() uint64 {
method Size (line 37) | func (du *DiskUsage) Size() uint64 {
method Used (line 42) | func (du *DiskUsage) Used() uint64 {
method Usage (line 47) | func (du *DiskUsage) Usage() float32 {
function NewDiskUsage (line 17) | func NewDiskUsage(volumePath string) *DiskUsage {
FILE: src/diskusage/diskusage_test.go
function TestNewDiskUsage (line 10) | func TestNewDiskUsage(t *testing.T) {
FILE: src/diskusage/diskusage_windows.go
type DiskUsage (line 9) | type DiskUsage struct
method Free (line 33) | func (du *DiskUsage) Free() uint64 {
method Available (line 38) | func (du *DiskUsage) Available() uint64 {
method Size (line 43) | func (du *DiskUsage) Size() uint64 {
method Used (line 48) | func (du *DiskUsage) Used() uint64 {
method Usage (line 53) | func (du *DiskUsage) Usage() float32 {
function NewDiskUsage (line 17) | func NewDiskUsage(volumePath string) *DiskUsage {
FILE: src/install/updateversion.go
function main (line 10) | func main() {
function run (line 17) | func run() (err error) {
function replaceInFile (line 44) | func replaceInFile(fname, start, end, replacement string) (err error) {
function getStringInBetween (line 65) | func getStringInBetween(str, start, end string) (result string) {
FILE: src/message/message.go
type Type (line 13) | type Type
constant TypePAKE (line 16) | TypePAKE Type = "pake"
constant TypeExternalIP (line 17) | TypeExternalIP Type = "externalip"
constant TypeFinished (line 18) | TypeFinished Type = "finished"
constant TypeError (line 19) | TypeError Type = "error"
constant TypeCloseRecipient (line 20) | TypeCloseRecipient Type = "close-recipient"
constant TypeCloseSender (line 21) | TypeCloseSender Type = "close-sender"
constant TypeRecipientReady (line 22) | TypeRecipientReady Type = "recipientready"
constant TypeFileInfo (line 23) | TypeFileInfo Type = "fileinfo"
type Message (line 27) | type Message struct
method String (line 35) | func (m Message) String() string {
function Send (line 41) | func Send(c *comm.Comm, key []byte, m Message) (err error) {
function Encode (line 51) | func Encode(key []byte, m Message) (b []byte, err error) {
function Decode (line 67) | func Decode(key []byte, b []byte) (m Message, err error) {
FILE: src/message/message_test.go
function TestMessage (line 18) | func TestMessage(t *testing.T) {
function TestMessageNoPass (line 38) | func TestMessageNoPass(t *testing.T) {
function TestSend (line 51) | func TestSend(t *testing.T) {
FILE: src/mnemonicode/mnemonicode.go
constant base (line 31) | base = 1626
function WordsRequired (line 41) | func WordsRequired(length int) int {
function EncodeWordList (line 48) | func EncodeWordList(dst []string, src []byte) (result []string) {
FILE: src/mnemonicode/mnemonicode_test.go
function TestWordsRequired (line 7) | func TestWordsRequired(t *testing.T) {
function TestEncodeWordList (line 32) | func TestEncodeWordList(t *testing.T) {
function TestEncodeWordListConsistency (line 100) | func TestEncodeWordListConsistency(t *testing.T) {
function TestEncodeWordListCapacityHandling (line 118) | func TestEncodeWordListCapacityHandling(t *testing.T) {
function TestEncodeWordListBoundaryValues (line 135) | func TestEncodeWordListBoundaryValues(t *testing.T) {
FILE: src/mnemonicode/wordlist.go
constant WordListVersion (line 32) | WordListVersion = "0.7"
function init (line 36) | func init() {
constant longestWord (line 42) | longestWord = 7
FILE: src/models/constants.go
constant TCP_BUFFER_SIZE (line 16) | TCP_BUFFER_SIZE = 1024 * 64
function getConfigFile (line 49) | func getConfigFile(requireValidPath bool) (fname string, err error) {
function init (line 58) | func init() {
function lookup (line 105) | func lookup(address string) (ipaddress string, err error) {
function localLookupIP (line 135) | func localLookupIP(address string) (ipaddress string, err error) {
function remoteLookupIP (line 152) | func remoteLookupIP(address, dns string) (ipaddress string, err error) {
FILE: src/models/models_test.go
function TestConstants (line 10) | func TestConstants(t *testing.T) {
function TestPublicDNSServers (line 24) | func TestPublicDNSServers(t *testing.T) {
function TestLocalLookupIP (line 71) | func TestLocalLookupIP(t *testing.T) {
function TestRemoteLookupIPTimeout (line 112) | func TestRemoteLookupIPTimeout(t *testing.T) {
function TestLookupFunction (line 128) | func TestLookupFunction(t *testing.T) {
function TestGetConfigFile (line 170) | func TestGetConfigFile(t *testing.T) {
FILE: src/tcp/ctx.go
type stop (line 14) | type stop struct
method Cancel (line 35) | func (s *stop) Cancel() {
function newStop (line 24) | func newStop(ctx context.Context) *stop {
function RunCtx (line 43) | func RunCtx(ctx context.Context, debugLevel, host, port, password string...
function WithCtx (line 47) | func WithCtx(ctx context.Context) serverOptsFunc {
function Ignore (line 59) | func Ignore(err error) error {
FILE: src/tcp/defaults.go
constant DEFAULT_LOG_LEVEL (line 6) | DEFAULT_LOG_LEVEL = "debug"
constant DEFAULT_ROOM_CLEANUP_INTERVAL (line 7) | DEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute
constant DEFAULT_ROOM_TTL (line 8) | DEFAULT_ROOM_TTL = 3 * time.Hour
FILE: src/tcp/options.go
type serverOptsFunc (line 11) | type serverOptsFunc
function WithBanner (line 13) | func WithBanner(banner ...string) serverOptsFunc {
function WithLogLevel (line 22) | func WithLogLevel(level string) serverOptsFunc {
function WithRoomCleanupInterval (line 32) | func WithRoomCleanupInterval(interval time.Duration) serverOptsFunc {
function WithRoomTTL (line 39) | func WithRoomTTL(ttl time.Duration) serverOptsFunc {
function containsSlice (line 46) | func containsSlice(s []string, e string) bool {
FILE: src/tcp/tcp.go
type server (line 20) | type server struct
method start (line 91) | func (s *server) start() (err error) {
method run (line 119) | func (s *server) run() (err error) {
method deleteOldRooms (line 232) | func (s *server) deleteOldRooms() {
method clientCommunication (line 278) | func (s *server) clientCommunication(c *comm.Comm) (room string, err e...
method deleteRoom (line 439) | func (s *server) deleteRoom(room string) {
type roomInfo (line 36) | type roomInfo struct
type roomMap (line 43) | type roomMap struct
constant pingRoom (line 48) | pingRoom = "pinglkasjdlfjsaldjf"
function newDefaultServer (line 51) | func newDefaultServer() *server {
function RunWithOptionsAsync (line 62) | func RunWithOptionsAsync(host, port, password string, opts ...serverOpts...
function Run (line 77) | func Run(debugLevel, host, port, password string, banner ...string) (err...
function maskedPassword (line 82) | func maskedPassword(password string) (s string) {
function chanFromConn (line 459) | func chanFromConn(conn net.Conn) chan []byte {
function pipe (line 489) | func pipe(conn1 net.Conn, conn2 net.Conn) {
function PingServer (line 514) | func PingServer(address string) (err error) {
function ConnectToTCPServer (line 539) | func ConnectToTCPServer(address, password, room string, timelimit ...tim...
FILE: src/tcp/tcp_test.go
function BenchmarkConnection (line 14) | func BenchmarkConnection(b *testing.B) {
function TestTCP (line 27) | func TestTCP(t *testing.T) {
function TestTCPctx (line 80) | func TestTCPctx(t *testing.T) {
function TestWrongPassword (line 162) | func TestWrongPassword(t *testing.T) {
function TestRoomIsolation (line 173) | func TestRoomIsolation(t *testing.T) {
function TestRoomRecreationAfterTTL (line 219) | func TestRoomRecreationAfterTTL(t *testing.T) {
function TestLargeDataTransfer (line 252) | func TestLargeDataTransfer(t *testing.T) {
function TestServerReleasesPort (line 288) | func TestServerReleasesPort(t *testing.T) {
FILE: src/utils/ctx.go
type ctxFile (line 20) | type ctxFile struct
method Read (line 31) | func (c *ctxFile) Read(p []byte) (n int, err error) {
method ReadAt (line 45) | func (c *ctxFile) ReadAt(p []byte, off int64) (n int, err error) {
method Seek (line 59) | func (c *ctxFile) Seek(offset int64, whence int) (n int64, err error) {
function NewCtxFile (line 26) | func NewCtxFile(ctx context.Context, f *os.File) *ctxFile {
function HashFileCtx (line 73) | func HashFileCtx(ctx context.Context, fname string, algorithm string, sh...
function IMOHashReader (line 164) | func IMOHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) (...
function IMOHashReaderFull (line 189) | func IMOHashReaderFull(sr *io.SectionReader, bar *progressbar.ProgressBa...
function MD5HashReader (line 211) | func MD5HashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) (...
function XXHashReader (line 232) | func XXHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([...
function HighwayHashReader (line 251) | func HighwayHashReader(sr *io.SectionReader, bar *progressbar.ProgressBa...
FILE: src/utils/utils.go
constant NbPinNumbers (line 33) | NbPinNumbers = 4
constant NbBytesWords (line 34) | NbBytesWords = 4
function GetConfigDir (line 37) | func GetConfigDir(requireValidPath bool) (homedir string, err error) {
function Exists (line 63) | func Exists(name string) bool {
function GetInput (line 73) | func GetInput(prompt string) string {
function HashFile (line 82) | func HashFile(fname string, algorithm string, showProgress ...bool) (has...
function HighwayHashFile (line 115) | func HighwayHashFile(fname string, doShowProgress bool) (hashHighway []b...
function MD5HashFile (line 157) | func MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err...
function IMOHashFile (line 195) | func IMOHashFile(fname string) (hash []byte, err error) {
function IMOHashFileFull (line 202) | func IMOHashFileFull(fname string) (hash []byte, err error) {
function XXHashFile (line 209) | func XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err ...
function SHA256 (line 244) | func SHA256(s string) string {
function PublicIP (line 251) | func PublicIP() (ip string, err error) {
function LocalIP (line 271) | func LocalIP() string {
function GenerateRandomPin (line 285) | func GenerateRandomPin() string {
function GetRandomName (line 300) | func GetRandomName() string {
function ByteCountDecimal (line 309) | func ByteCountDecimal(b int64) string {
function MissingChunks (line 325) | func MissingChunks(fname string, fsize int64, chunkSize int) (chunkRange...
function ChunkRangesToChunks (line 400) | func ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {
function GetLocalIPs (line 415) | func GetLocalIPs() (ips []string, err error) {
function RandomFileName (line 435) | func RandomFileName() (fname string, err error) {
function FindOpenPorts (line 445) | func FindOpenPorts(host string, portNumStart, numPorts int) (openPorts [...
function init (line 466) | func init() {
function IsLocalIP (line 485) | func IsLocalIP(ipaddress string) bool {
function ZipDirectory (line 502) | func ZipDirectory(destination string, source string) (err error) {
function UnzipDirectory (line 640) | func UnzipDirectory(destination string, source string) error {
function resolveUnzipPath (line 738) | func resolveUnzipPath(destination string, entryName string) (string, err...
function ValidFileName (line 763) | func ValidFileName(fname string) (err error) {
constant crocRemovalFile (line 793) | crocRemovalFile = "croc-marked-files.txt"
function MarkFileForRemoval (line 795) | func MarkFileForRemoval(fname string) {
function RemoveMarkedFiles (line 806) | func RemoveMarkedFiles() (err error) {
FILE: src/utils/utils_test.go
constant TCP_BUFFER_SIZE (line 20) | TCP_BUFFER_SIZE = 1024 * 64
function bigFile (line 24) | func bigFile() {
function BenchmarkMD5 (line 28) | func BenchmarkMD5(b *testing.B) {
function BenchmarkXXHash (line 36) | func BenchmarkXXHash(b *testing.B) {
function BenchmarkImoHash (line 44) | func BenchmarkImoHash(b *testing.B) {
function BenchmarkHighwayHash (line 52) | func BenchmarkHighwayHash(b *testing.B) {
function BenchmarkImoHashFull (line 60) | func BenchmarkImoHashFull(b *testing.B) {
function BenchmarkSha256 (line 68) | func BenchmarkSha256(b *testing.B) {
function BenchmarkMissingChunks (line 75) | func BenchmarkMissingChunks(b *testing.B) {
function TestExists (line 83) | func TestExists(t *testing.T) {
function TestMD5HashFile (line 91) | func TestMD5HashFile(t *testing.T) {
function TestHighwayHashFile (line 101) | func TestHighwayHashFile(t *testing.T) {
function TestIMOHashFile (line 111) | func TestIMOHashFile(t *testing.T) {
function TestXXHashFile (line 119) | func TestXXHashFile(t *testing.T) {
function TestSHA256 (line 129) | func TestSHA256(t *testing.T) {
function TestByteCountDecimal (line 133) | func TestByteCountDecimal(t *testing.T) {
function TestMissingChunks (line 139) | func TestMissingChunks(t *testing.T) {
function TestHashFile (line 195) | func TestHashFile(t *testing.T) {
function TestPublicIP (line 215) | func TestPublicIP(t *testing.T) {
function TestLocalIP (line 222) | func TestLocalIP(t *testing.T) {
function TestGetRandomName (line 228) | func TestGetRandomName(t *testing.T) {
function intSliceSame (line 234) | func intSliceSame(a, b []int) bool {
function TestFindOpenPorts (line 246) | func TestFindOpenPorts(t *testing.T) {
function TestIsLocalIP (line 254) | func TestIsLocalIP(t *testing.T) {
function TestValidFileName (line 258) | func TestValidFileName(t *testing.T) {
function TestUnzipDirectory (line 289) | func TestUnzipDirectory(t *testing.T) {
function TestUnzipToNonExistentDirectory (line 359) | func TestUnzipToNonExistentDirectory(t *testing.T) {
function TestUnzipDirectoryRejectsPathTraversal (line 420) | func TestUnzipDirectoryRejectsPathTraversal(t *testing.T) {
function TestResolveUnzipPathRejectsAbsolutePathEntry (line 445) | func TestResolveUnzipPathRejectsAbsolutePathEntry(t *testing.T) {
function TestZipAndUnzipRoundTrip (line 457) | func TestZipAndUnzipRoundTrip(t *testing.T) {
function createTestZipWithModTime (line 606) | func createTestZipWithModTime(zipPath string, modTime time.Time) error {
type zipTestEntry (line 667) | type zipTestEntry struct
function createZipWithEntries (line 672) | func createZipWithEntries(zipPath string, entries []zipTestEntry) error {
function verifyFileContent (line 698) | func verifyFileContent(t *testing.T, filePath, expectedContent string) {
function verifyFileModTime (line 712) | func verifyFileModTime(t *testing.T, filePath string, expectedTime time....
function TestHashFileCtxNoCancellation (line 730) | func TestHashFileCtxNoCancellation(t *testing.T) {
function TestHashFileCtxWithCancellation (line 856) | func TestHashFileCtxWithCancellation(t *testing.T) {
function TestHashFileCtxEquivalence (line 946) | func TestHashFileCtxEquivalence(t *testing.T) {
function TestHashFileCtxLargeFile (line 994) | func TestHashFileCtxLargeFile(t *testing.T) {
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (383K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 16,
"preview": "github: schollz\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2546,
"preview": "name: Bug Report\ndescription: File a bug report to help us improve croc\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n - type:"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/dependabot.yml",
"chars": 205,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"daily\"\n -"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2606,
"preview": "name: CI\n\non:\n push:\n pull_request:\n workflow_dispatch:\n\njobs:\n unit-tests:\n name: Go unit tests\n runs-on: ubu"
},
{
"path": ".github/workflows/deploy.yml",
"chars": 1344,
"preview": "name: Deploy Docker\n\non:\n release:\n types: [created]\n workflow_dispatch:\n\njobs:\n docker:\n runs-on: ubuntu-24.04"
},
{
"path": ".github/workflows/release.yml",
"chars": 5987,
"preview": "name: Release\n\non:\n release:\n types: [created]\n workflow_dispatch:\n\npermissions:\n contents: write\n\njobs:\n prepare"
},
{
"path": ".github/workflows/stale.yml",
"chars": 655,
"preview": "name: Mark stale issues and pull requests\n\non:\n schedule:\n - cron: '0 0 * * *'\n\njobs:\n stale:\n runs-on: ubuntu-2"
},
{
"path": ".github/workflows/winget.yml",
"chars": 380,
"preview": "name: Publish to Winget\r\n\r\non:\r\n release:\r\n types: [released]\r\n workflow_dispatch:\r\n\r\njobs:\r\n publish:\r\n runs-o"
},
{
"path": ".gitignore",
"chars": 554,
"preview": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gi"
},
{
"path": ".goreleaser.yml",
"chars": 2098,
"preview": "project_name: croc\n\nbuild:\n main: main.go\n binary: croc\n ldflags: -s -w -X main.Version=\"v{{.Version}}-{{.Date}}\"\n e"
},
{
"path": ".travis.yml",
"chars": 646,
"preview": "language: go\n\ngo:\n - tip\n\nenv:\n - \"PATH=/home/travis/gopath/bin:$PATH\"\n\ninstall: true\n\nscript:\n - env GO111MODULE=on "
},
{
"path": "Dockerfile",
"chars": 495,
"preview": "FROM golang:1.24-alpine AS builder\n\nRUN apk add --no-cache git gcc musl-dev\n\nWORKDIR /go/croc\n\nCOPY . .\n\nRUN go build -v"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2017-2025 Zack\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 8422,
"preview": "<p align=\"center\">\n <img src=\"https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20"
},
{
"path": "croc-entrypoint.sh",
"chars": 103,
"preview": "#!/bin/sh\nset -e\n\nif [ -n \"$CROC_PASS\" ]; then\n set -- --pass \"$CROC_PASS\" \"$@\"\nfi\n\nexec /croc \"$@\"\n"
},
{
"path": "croc.service",
"chars": 201,
"preview": "[Unit]\nDescription=croc relay\nAfter=network.target\n\n[Service]\nType=simple\nDynamicUser=yes\nCapabilityBoundingSet=CAP_NET_"
},
{
"path": "go.mod",
"chars": 1350,
"preview": "module github.com/schollz/croc/v10\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/chzyer/readlin"
},
{
"path": "go.sum",
"chars": 13094,
"preview": "filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:"
},
{
"path": "main.go",
"chars": 1094,
"preview": "package main\n\n//go:generate go run src/install/updateversion.go\n//go:generate git commit -am \"bump $VERSION\"\n//go:genera"
},
{
"path": "src/cli/cli.go",
"chars": 25354,
"preview": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"str"
},
{
"path": "src/comm/comm.go",
"chars": 5494,
"preview": "package comm\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/magis"
},
{
"path": "src/comm/comm_test.go",
"chars": 2528,
"preview": "package comm\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\tlog \"github.com/schollz/lo"
},
{
"path": "src/compress/compress.go",
"chars": 1416,
"preview": "package compress\n\nimport (\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"io\"\n\n\tlog \"github.com/schollz/logger\"\n)\n\n// CompressWithOption r"
},
{
"path": "src/compress/compress_test.go",
"chars": 5624,
"preview": "package compress\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar fable = []byte"
},
{
"path": "src/croc/croc.go",
"chars": 66724,
"preview": "package croc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding"
},
{
"path": "src/croc/croc_test.go",
"chars": 28967,
"preview": "package croc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"tes"
},
{
"path": "src/croc/ctx.go",
"chars": 2481,
"preview": "// ctx.go\npackage croc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/message\"\n\t\"github.com/schollz/cro"
},
{
"path": "src/crypt/crypt.go",
"chars": 3467,
"preview": "package crypt\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"golang.org/x/cry"
},
{
"path": "src/crypt/crypt_test.go",
"chars": 2701,
"preview": "package crypt\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkEncrypt(b *testing.B) "
},
{
"path": "src/diskusage/diskusage.go",
"chars": 1238,
"preview": "//go:build !windows\n// +build !windows\n\npackage diskusage\n\nimport (\n\t\"golang.org/x/sys/unix\"\n)\n\n// DiskUsage contains us"
},
{
"path": "src/diskusage/diskusage_test.go",
"chars": 373,
"preview": "package diskusage\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar KB = uint64(1024)\n\nfunc TestNewDiskUsage(t *testing.T) {\n\tusage := "
},
{
"path": "src/diskusage/diskusage_windows.go",
"chars": 1295,
"preview": "package diskusage\n\nimport (\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\ntype DiskUsage struct {\n\tfreeBytes int64\n\ttotalBy"
},
{
"path": "src/install/Makefile",
"chars": 526,
"preview": "# VERSION=8.X.Y make release\n\nrelease:\n\tcd ../../ && go run src/install/updateversion.go\n\tgit commit -am \"bump ${VERSION"
},
{
"path": "src/install/bash_autocomplete",
"chars": 558,
"preview": ": ${PROG:=$(basename ${BASH_SOURCE})}\n\n_cli_bash_autocomplete() {\n if [[ \"${COMP_WORDS[0]}\" != \"source\" ]]; then\n lo"
},
{
"path": "src/install/default.txt",
"chars": 27284,
"preview": "#!/bin/bash - \n#===============================================================================\n#\n# FILE: defau"
},
{
"path": "src/install/prepare-sources-tarball.sh",
"chars": 296,
"preview": "#!/bin/bash\ntmp=$(mktemp -d)\necho $VERSION\ngit clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-"
},
{
"path": "src/install/updateversion.go",
"chars": 1768,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc main() {\n\terr := run()\n\tif err != nil {\n\t\tfmt.Println("
},
{
"path": "src/install/upload-src-tarball.sh",
"chars": 1463,
"preview": "#!/bin/bash\nVERSION=$(cat ./src/cli/cli.go | grep 'Version = \"v' | sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/')\necho $VERSION\n\n# C"
},
{
"path": "src/install/zsh_autocomplete",
"chars": 488,
"preview": "#compdef $PROG\n\n_cli_zsh_autocomplete() {\n\n local -a opts\n local cur\n cur=${words[-1]}\n if [[ \"$cur\" == \"-\"* ]]; the"
},
{
"path": "src/message/message.go",
"chars": 1855,
"preview": "package message\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/co"
},
{
"path": "src/message/message_test.go",
"chars": 2366,
"preview": "package message\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"git"
},
{
"path": "src/mnemonicode/mnemonicode.go",
"chars": 2953,
"preview": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n"
},
{
"path": "src/mnemonicode/mnemonicode_test.go",
"chars": 3617,
"preview": "package mnemonicode\n\nimport (\n\t\"testing\"\n)\n\nfunc TestWordsRequired(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n"
},
{
"path": "src/mnemonicode/wordlist.go",
"chars": 17776,
"preview": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n"
},
{
"path": "src/models/constants.go",
"chars": 4239,
"preview": "package models\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/utils\"\n\tlog \""
},
{
"path": "src/models/models_test.go",
"chars": 3721,
"preview": "package models\n\nimport (\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConstants(t *testing.T) {\n\tif TCP_BUFFER_SIZE "
},
{
"path": "src/tcp/ctx.go",
"chars": 1457,
"preview": "// ctx.go\npackage tcp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\n\tlog \"github.com/schollz/logger\"\n)\n\n// stop manages"
},
{
"path": "src/tcp/defaults.go",
"chars": 176,
"preview": "package tcp\n\nimport \"time\"\n\nconst (\n\tDEFAULT_LOG_LEVEL = \"debug\"\n\tDEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time."
},
{
"path": "src/tcp/options.go",
"chars": 999,
"preview": "package tcp\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// TODO: maybe export from logger library?\nvar availableLogLevels = []string{\"in"
},
{
"path": "src/tcp/tcp.go",
"chars": 14163,
"preview": "package tcp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tlog \"github.com/schollz/logger\"\n\t\"g"
},
{
"path": "src/tcp/tcp_test.go",
"chars": 7935,
"preview": "package tcp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/stret"
},
{
"path": "src/utils/ctx.go",
"chars": 6803,
"preview": "// ctx.go\npackage utils\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github."
},
{
"path": "src/utils/utils.go",
"chars": 20726,
"preview": "package utils\n\nimport (\n\t\"archive/zip\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\""
},
{
"path": "src/utils/utils_test.go",
"chars": 32208,
"preview": "package utils\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"s"
}
]
About this extraction
This page contains the full source code of the schollz/croc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (335.9 KB), approximately 104.8k tokens, and a symbol index with 299 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.