Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
|
|
@ -1,3 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: schollz
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: New issue
|
||||
about: Create an issue
|
||||
title: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Read this and delete before submitting:* Thanks for starting a discussion! Please provide as much context as possible so that others can understand your thoughts. If you have a specific change in mind, consider submitting a pull request instead.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Go unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- run: go version
|
||||
- run: go test -v ./...
|
||||
- name: Build files
|
||||
run: |
|
||||
go version
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
|
||||
GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20.7' # go1.20.8+ refuses to build go1.22 code...
|
||||
- name: Build Windows 7
|
||||
run: |
|
||||
go version
|
||||
rm go.mod go.sum
|
||||
go mod init github.com/schollz/croc/v10
|
||||
go mod tidy
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CD
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the main branch
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: schollz/croc
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm,linux/arm64,linux/386
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
name: Make release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Prepare source tarball
|
||||
run: |
|
||||
git clone -b ${{ github.event.release.name }} --depth 1 https://github.com/schollz/croc croc-${{ github.event.release.name }}
|
||||
cd croc-${{ github.event.release.name }} && go mod tidy && go mod vendor
|
||||
cd .. && tar -czvf croc_${{ github.event.release.name }}_src.tar.gz croc-${{ github.event.release.name }}
|
||||
- name: Build files
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-64bit.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-32bit.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-ARM.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-ARM64.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-32bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM.tar.gz croc LICENSE
|
||||
GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_macOS-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz croc LICENSE
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20.7' # go1.20.8+ refuses to build go1.22 code...
|
||||
- name: Build Windows 7
|
||||
run: |
|
||||
go version
|
||||
rm go.mod go.sum
|
||||
go mod init github.com/schollz/croc/v10
|
||||
go mod tidy
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows7-64bit.zip croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows7-32bit.zip croc.exe
|
||||
- name: Create checksums.txt
|
||||
run: |
|
||||
touch croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_src.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-ARM.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-ARM64.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows7-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows7-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-ARM.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_macOS-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
croc_${{ github.event.release.name }}_checksums.txt
|
||||
croc_${{ github.event.release.name }}_src.tar.gz
|
||||
croc_${{ github.event.release.name }}_Windows-64bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows-32bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows-ARM.zip
|
||||
croc_${{ github.event.release.name }}_Windows-ARM64.zip
|
||||
croc_${{ github.event.release.name }}_Windows7-64bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows7-32bit.zip
|
||||
croc_${{ github.event.release.name }}_Linux-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-32bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-ARM.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-ARMv5.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_macOS-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '19 12 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
name: Publish to Winget
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: schollz.croc
|
||||
installers-regex: '_Windows-\w+\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
|
@ -1,13 +1,3 @@
|
|||
# Binaries
|
||||
/croc
|
||||
/croc.exe
|
||||
zsh_autocomplete
|
||||
bash_autocomplete
|
||||
dist
|
||||
bin
|
||||
croc-stdin*
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
src/utils/bigfile.test
|
||||
test1/
|
||||
|
||||
|
|
|
|||
100
.goreleaser.yml
100
.goreleaser.yml
|
|
@ -1,100 +0,0 @@
|
|||
project_name: croc
|
||||
build:
|
||||
main: main.go
|
||||
binary: croc
|
||||
ldflags: -s -w -X main.Version="v{{.Version}}-{{.Date}}"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
- dragonfly
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm:
|
||||
- 7
|
||||
nfpms:
|
||||
-
|
||||
formats:
|
||||
- deb
|
||||
vendor: "schollz.com"
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
maintainer: "Zack Scholl <zack.scholl@gmail.com>"
|
||||
description: "A simple, secure, and fast way to transfer data."
|
||||
license: "MIT"
|
||||
file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
archives:
|
||||
-
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- zsh_autocomplete
|
||||
- bash_autocomplete
|
||||
|
||||
brews:
|
||||
-
|
||||
tap:
|
||||
owner: schollz
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
install: |
|
||||
bin.install "croc"
|
||||
|
||||
test: |
|
||||
system "#{bin}/croc --version"
|
||||
|
||||
scoop:
|
||||
bucket:
|
||||
owner: schollz
|
||||
name: scoop-bucket
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
|
||||
license: MIT
|
||||
announce:
|
||||
twitter:
|
||||
# Wether its enabled or not.
|
||||
# Defaults to false.
|
||||
enabled: false
|
||||
23
.travis.yml
23
.travis.yml
|
|
@ -1,23 +1,16 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
- 1.9
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
install: true
|
||||
|
||||
before_install:
|
||||
- go get github.com/gosuri/uiprogress
|
||||
- go get github.com/schollz/mnemonicode
|
||||
- go get github.com/pkg/errors
|
||||
- go get github.com/sirupsen/logrus
|
||||
- go get github.com/verybluebot/tarinator-go
|
||||
script:
|
||||
- env GO111MODULE=on go build -v
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/compress
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/croc
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/crypt
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/tcp
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/utils
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/comm
|
||||
|
||||
branches:
|
||||
except:
|
||||
- dev
|
||||
- win
|
||||
- go test -v
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at zack.scholl@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
16
Dockerfile
16
Dockerfile
|
|
@ -1,16 +0,0 @@
|
|||
FROM golang:1.22-alpine as builder
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
WORKDIR /go/croc
|
||||
COPY . .
|
||||
RUN go build -v -ldflags="-s -w"
|
||||
|
||||
FROM alpine:latest
|
||||
EXPOSE 9009
|
||||
EXPOSE 9010
|
||||
EXPOSE 9011
|
||||
EXPOSE 9012
|
||||
EXPOSE 9013
|
||||
COPY --from=builder /go/croc/croc /go/croc/croc-entrypoint.sh /
|
||||
USER nobody
|
||||
ENTRYPOINT ["/croc-entrypoint.sh"]
|
||||
CMD ["relay"]
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "77ed807830b4df581417e7f89eb81d4872832b72"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gosuri/uilive"
|
||||
packages = ["."]
|
||||
revision = "ac356e6e42cd31fcef8e6aec13ae9ed6fe87713e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gosuri/uiprogress"
|
||||
packages = [".","util/strutil"]
|
||||
revision = "d0567a9d84a1c40dd7568115ea66f4887bf57b33"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mars9/crypt"
|
||||
packages = ["."]
|
||||
revision = "65899cf653ff022fe5c7fe504b439feed9e7e0fc"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
packages = ["."]
|
||||
revision = "15c9654387fad6d257aa28f9be57b9f124101955"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/verybluebot/tarinator-go"
|
||||
packages = ["."]
|
||||
revision = "f75724675c91d0c731b69c81e0985de07663f007"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["pbkdf2","scrypt","ssh/terminal"]
|
||||
revision = "541b9d50ad47e36efd8fb423e938e59ff1691f68"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "8dbc5d05d6edcc104950cc299a1ce6641235bc86"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
packages = ["transform"]
|
||||
revision = "c01e4764d870b77f8abe5096ee19ad20d80e8075"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "56157cf168219ec6f5596364497dc7fc93cb674ce0a159fd339d88d025f97e25"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/gosuri/uiprogress"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.3"
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017-2024 Zack
|
||||
Copyright (c) 2017 Zack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
394
README.md
394
README.md
|
|
@ -1,281 +1,113 @@
|
|||
|
||||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg"
|
||||
width="408px" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-v10.1.1-brightgreen.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://github.com/schollz/croc/actions/workflows/ci.yml"><img
|
||||
src="https://github.com/schollz/croc/actions/workflows/ci.yml/badge.svg" alt="Build
|
||||
Status"></a>
|
||||
<p align="center">This project is supported by <a href="https://github.com/sponsors/schollz">Github sponsors</a>.</p>
|
||||
|
||||
`croc` is a tool that allows any two computers to simply and securely transfer files and folders. AFAIK, *croc* is the only CLI file-transfer tool that does **all** of the following:
|
||||
|
||||
- allows **any two computers** to transfer data (using a relay)
|
||||
- provides **end-to-end encryption** (using PAKE)
|
||||
- enables easy **cross-platform** transfers (Windows, Linux, Mac)
|
||||
- allows **multiple file** transfers
|
||||
- allows **resuming transfers** that are interrupted
|
||||
- local server or port-forwarding **not needed**
|
||||
- **ipv6-first** with ipv4 fallback
|
||||
- can **use proxy**, like tor
|
||||
|
||||
For more information about `croc`, see [my blog post](https://schollz.com/software/croc6) or read a [recent interview I did](https://console.substack.com/p/console-91).
|
||||
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
Download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:
|
||||
|
||||
```
|
||||
curl https://getcroc.schollz.com | bash
|
||||
```
|
||||
|
||||
|
||||
On macOS you can install the latest release with [Homebrew](https://brew.sh/):
|
||||
|
||||
```
|
||||
brew install croc
|
||||
```
|
||||
|
||||
On macOS you can also install the latest release with [MacPorts](https://macports.org/):
|
||||
|
||||
```
|
||||
sudo port selfupdate
|
||||
sudo port install croc
|
||||
```
|
||||
|
||||
On Windows you can install the latest release with [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org), or [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
|
||||
|
||||
```
|
||||
scoop install croc
|
||||
```
|
||||
|
||||
```
|
||||
choco install croc
|
||||
```
|
||||
|
||||
```
|
||||
winget install schollz.croc
|
||||
```
|
||||
|
||||
On Unix you can install the latest release with [Nix](https://nixos.org/nix):
|
||||
|
||||
```
|
||||
nix-env -i croc
|
||||
```
|
||||
|
||||
|
||||
On Alpine Linux you have to install dependencies first:
|
||||
|
||||
```
|
||||
apk add bash coreutils
|
||||
wget -qO- https://getcroc.schollz.com | bash
|
||||
```
|
||||
|
||||
On Arch Linux you can install the latest release with `pacman`:
|
||||
|
||||
```
|
||||
pacman -S croc
|
||||
```
|
||||
|
||||
On Fedora you can install with `dnf`:
|
||||
|
||||
```
|
||||
dnf install croc
|
||||
```
|
||||
|
||||
On Gentoo you can install with `portage`:
|
||||
```
|
||||
emerge net-misc/croc
|
||||
```
|
||||
|
||||
On Termux you can install with `pkg`:
|
||||
|
||||
```
|
||||
pkg install croc
|
||||
```
|
||||
|
||||
On FreeBSD you can install with `pkg`:
|
||||
|
||||
```
|
||||
pkg install croc
|
||||
```
|
||||
|
||||
On Linux, macOS, and Windows you can install from [conda-forge](https://github.com/conda-forge/croc-feedstock/) globally with [`pixi`](https://pixi.sh/):
|
||||
|
||||
```
|
||||
pixi global install croc
|
||||
```
|
||||
|
||||
or into a particular environment with [`conda`](https://docs.conda.io/projects/conda/):
|
||||
|
||||
```
|
||||
conda install --channel conda-forge croc
|
||||
```
|
||||
|
||||
Or, you can [install Go](https://golang.org/dl/) and build from source (requires Go 1.17+):
|
||||
|
||||
```
|
||||
go install github.com/schollz/croc/v10@latest
|
||||
```
|
||||
|
||||
On Android there is a 3rd party F-Droid app [available to download](https://f-droid.org/en/packages/com.github.howeyc.crocgui/).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
To send a file, simply do:
|
||||
|
||||
```
|
||||
$ croc send [file(s)-or-folder]
|
||||
Sending 'file-or-folder' (X MB)
|
||||
Code is: code-phrase
|
||||
```
|
||||
|
||||
Then to receive the file (or folder) on another computer, you can just do
|
||||
|
||||
```
|
||||
croc code-phrase
|
||||
```
|
||||
|
||||
The code phrase is used to establish password-authenticated key agreement ([PAKE](https://en.wikipedia.org/wiki/Password-authenticated_key_agreement)) which generates a secret key for the sender and recipient to use for end-to-end encryption.
|
||||
|
||||
There are a number of configurable options (see `--help`). A set of options (like custom relay, ports, and code phrase) can be set using `--remember`.
|
||||
|
||||
### Using `croc` on Linux or Mac OS
|
||||
|
||||
On Linux and Mac OS, the sending & receiving is slightly different to avoid [leaking the secret via the process name](https://nvd.nist.gov/vuln/detail/CVE-2023-43621). On these systems you will need to run `croc` with the secret as an environment variable. For example, to receive with the secret `***`:
|
||||
|
||||
```
|
||||
CROC_SECRET=*** croc
|
||||
```
|
||||
|
||||
This will show only `croc` in the process list of a multi-user system and not leak the secret.
|
||||
|
||||
For a single-user system the default behavior can be permanently enabled by running
|
||||
|
||||
```
|
||||
croc --classic
|
||||
```
|
||||
|
||||
and confirming.
|
||||
Run this command again to disable classic mode.
|
||||
|
||||
### Custom code phrase
|
||||
|
||||
You can send with your own code phrase (must be more than 6 characters).
|
||||
|
||||
```
|
||||
croc send --code [code-phrase] [file(s)-or-folder]
|
||||
```
|
||||
|
||||
### Allow overwriting without prompt
|
||||
|
||||
By default, croc will prompt whether to overwrite a file. You can automatically overwrite files by using the `--overwrite` flag (recipient only). For example, receive a file to automatically overwrite:
|
||||
|
||||
```
|
||||
croc --yes --overwrite <code>
|
||||
```
|
||||
|
||||
|
||||
### Use pipes - stdin and stdout
|
||||
|
||||
You can pipe to `croc`:
|
||||
|
||||
```
|
||||
cat [filename] | croc send
|
||||
```
|
||||
|
||||
In this case `croc` will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789". To receive to `stdout` at you can always just use the `--yes` will automatically approve the transfer and pipe it out to `stdout`.
|
||||
|
||||
```
|
||||
croc --yes [code-phrase] > out
|
||||
```
|
||||
|
||||
All of the other text printed to the console is going to `stderr` so it will not interfere with the message going to `stdout`.
|
||||
|
||||
|
||||
### Send text
|
||||
|
||||
Sometimes you want to send URLs or short text. In addition to piping, you can easily send text with `croc`:
|
||||
|
||||
```
|
||||
croc send --text "hello world"
|
||||
```
|
||||
|
||||
This will automatically tell the receiver to use `stdout` when they receive the text so it will be displayed.
|
||||
|
||||
|
||||
### Use a proxy
|
||||
|
||||
You can use a proxy as your connection to the relay by adding a proxy address with `--socks5`. For example, you can send via a tor relay:
|
||||
|
||||
```
|
||||
croc --socks5 "127.0.0.1:9050" send SOMEFILE
|
||||
```
|
||||
|
||||
### Change encryption curve
|
||||
|
||||
You can choose from several different elliptic curves to use for encryption by using the `--curve` flag. Only the recipient can choose the curve. For example, receive a file using the P-521 curve:
|
||||
|
||||
```
|
||||
croc --curve p521 <codephrase>
|
||||
```
|
||||
|
||||
Available curves are P-256, P-348, P-521 and SIEC. P-256 is the default curve.
|
||||
|
||||
### Change hash algorithm
|
||||
|
||||
You can choose from several different hash algorithms. The default is the `xxhash` algorithm which is fast and thorough. If you want to optimize for speed you can use the `imohash` algorithm which is even faster, but since it samples files (versus reading the whole file) it can mistakenly determine that a file is the same on the two computers transferring - though this is only a problem if you are syncing files versus sending a new file to a computer.
|
||||
|
||||
```
|
||||
croc send --hash imohash SOMEFILE
|
||||
```
|
||||
|
||||
### Self-host relay
|
||||
|
||||
The relay is needed to staple the parallel incoming and outgoing connections. By default, `croc` uses a public relay but you can also run your own relay:
|
||||
|
||||
```
|
||||
croc relay
|
||||
```
|
||||
|
||||
By default it uses TCP ports 9009-9013. Make sure to open those up. You can customize the ports (e.g. `croc relay --ports 1111,1112`), but you must have a minimum of **2** ports for the relay. The first port is for communication and the subsequent ports are used for the multiplexed data transfer.
|
||||
|
||||
You can send files using your relay by entering `--relay` to change the relay that you are using if you want to custom host your own.
|
||||
|
||||
```
|
||||
croc --relay "myrelay.example.com:9009" send [filename]
|
||||
```
|
||||
|
||||
Note, when sending, you only need to include the first port (the communication port). The subsequent ports for data transfer will be transmitted back to the user from the relay.
|
||||
|
||||
#### Self-host relay (docker)
|
||||
|
||||
If it's easier you can also run a relay with Docker:
|
||||
|
||||
|
||||
```
|
||||
docker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' schollz/croc
|
||||
```
|
||||
|
||||
Be sure to include the password for the relay otherwise any requests will be rejected.
|
||||
|
||||
```
|
||||
croc --pass YOURPASSWORD --relay "myreal.example.com:9009" send [filename]
|
||||
```
|
||||
|
||||
Note: when including `--pass YOURPASSWORD` you can instead pass a file with the password, e.g. `--pass FILEWITHPASSWORD`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
`croc` has gone through many iterations, and I am awed by all the great contributions! If you feel like contributing, in any way, by all means you can send an Issue, a PR, or ask a question.
|
||||
|
||||
Thanks [@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole), [@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28), [@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/). Finally thanks for making pull requests [@maximbaz](https://github.com/maximbaz), [@meyermarcel](https://github.com/meyermarcel), [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu), [@nicolashardy](https://github.com/nicolashardy), [@fbartels](https://github.com/fbartels), [@rkuprov](https://github.com/rkuprov), [@hreese](https://github.com/hreese), [@xenrox](https://github.com/xenrox) and [Ipar](https://github.com/lpar)!
|
||||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/31846899-2b8a7034-b5cf-11e7-9643-afe552226c59.png"
|
||||
width="100%" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-0.1.0-green.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://gitter.im/schollz/croc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://img.shields.io/badge/chat-on%20gitter-green.svg?style=flat-square" alt="Version"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Secure transfer of stuff from one side of the internet to the other.</p>
|
||||
|
||||
This is more or less (but mostly *less*) a Golang port of [@warner's](https://github.com/warner) [*magic-wormhole*](https://github.com/warner/magic-wormhole) which allows you to directly transfer files and folders between computers. I decided to make this because I wanted to send my friend Jessie a file using *magic-wormhole* and when I told Jessie how to install the dependencies she made this face: :sob:. So, nominally, *croc* does the same thing (encrypted file transfer directly between computers) without dependencies so you can just double-click on your computer, even if you use Windows.
|
||||
|
||||
**Don't we have enough open-source peer-to-peer file-transfer utilities?**
|
||||
|
||||
[There](https://github.com/cowbell/sharedrop) [are](https://github.com/webtorrent/instant.io) [great](https://github.com/kern/filepizza) [tools](https://github.com/warner/magic-wormhole) [that](https://github.com/zerotier/toss) [already](https://github.com/ipfs/go-ipfs) [do](https://github.com/zerotier/toss) [this](https://github.com/nils-werner/zget). But, no we don't, because after review, [I found it was useful to make a new one](https://schollz.github.io/sending-a-file/).
|
||||
|
||||
# Example
|
||||
|
||||
_These two gifs should run in sync if you force-reload (Ctl+F5)_
|
||||
|
||||
**Sender:**
|
||||
|
||||

|
||||
|
||||
**Receiver:**
|
||||
|
||||

|
||||
|
||||
|
||||
**Sender:**
|
||||
|
||||
```
|
||||
$ croc -send croc.exe
|
||||
Sending 4.4 MB file named 'croc.exe'
|
||||
Code is: 4-cement-galaxy-alpha
|
||||
|
||||
Sending (->24.65.41.43:50843)..
|
||||
0s [==========================================================] 100%
|
||||
File sent.
|
||||
```
|
||||
|
||||
**Receiver:**
|
||||
|
||||
```
|
||||
$ croc
|
||||
Enter receive code: 4-cement-galaxy-alpha
|
||||
Receiving file (4.4 MB) into: croc.exe
|
||||
ok? (y/n): y
|
||||
|
||||
Receiving (<-50.32.38.188:50843)..
|
||||
0s [==========================================================] 100%
|
||||
Received file written to croc.exe
|
||||
```
|
||||
|
||||
Note, by default, you don't need any arguments for receiving! This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
[Download the latest release for your system](https://github.com/schollz/croc/releases/latest).
|
||||
|
||||
Or, you can [install Go](https://golang.org/dl/) and build from source with `go get github.com/schollz/croc`.
|
||||
|
||||
|
||||
|
||||
# How does it work?
|
||||
|
||||
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit and design. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transfering of files and folders through a intermediary relay that connects the TCP ports between the two computers.
|
||||
|
||||
In *croc*, code phrase is 16 random bits that are [menemonic encoded](http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/) plus a prepended integer to specify number of threads. This code phrase is hashed using sha256 and sent to a relay which maps that key to that connection. When the relay finds a matching key for both the receiver and the sender (i.e. they both have the same code phrase), then the sender transmits the encrypted metadata to the receiver through the relay. Then the receiver decrypts and reviews the metadata (file name, size), and chooses whether to consent to the transfer.
|
||||
|
||||
After the receiver consents to the transfer, the sender transmits encrypted data through the relay. The relay setups up [Go channels](https://golang.org/doc/effective_go.html?h=chan#channels) for each connection which pipes all the data incoming from that sender's connection out to the receiver's connection. After the transmission the channels are destroyed and all the connection and meta data information is wiped from the relay server. The encrypted file data never is stored on the relay.
|
||||
|
||||
**Encryption**
|
||||
|
||||
Encryption uses PBKDF2 (see [RFC2898](http://www.ietf.org/rfc/rfc2898.txt)) where the code phrase shared between the sender and receiver is used as the passphrase. For each of the two encrypted data blocks (metadata stored on relay server, and file data transmitted), a random 8-byte salt is used and a IV is generated according to [NIST Recommendation for Block ciphers, Section 8.2](http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf).
|
||||
|
||||
|
||||
**Decryption**
|
||||
|
||||
On the receiver's computer, each piece of received encrypted data is written to a separate file. These files are concatenated and then decrypted. The hash of the decrypted file is then checked against the hash transmitted from the sender (part of the meta data block).
|
||||
|
||||
## Run your own relay
|
||||
|
||||
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `cowyo.com`, which has no guarantees except that I guarantee to turn if off as soon as it gets abused ([click here to check the current status of the public relay](https://stats.uptimerobot.com/lOwJYIgRm)).
|
||||
|
||||
I recommend you run your own relay, it is very easy. On your server, `your-server.com`, just run
|
||||
|
||||
```
|
||||
$ croc -relay
|
||||
```
|
||||
|
||||
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server.
|
||||
|
||||
_Note:_ If you are behind a firewall, make sure to open up TCP ports 27001-27009.
|
||||
|
||||
# Contribute
|
||||
|
||||
I am awed by all the [great contributions](#acknowledgements) made! If you feel like contributing, in any way, by all means you can send an Issue, a PR, ask a question, or tweet me ([@yakczar](http://ctt.ec/Rq054)).
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
Thanks...
|
||||
|
||||
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
|
||||
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
|
||||
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
|
||||
- ...for making pull requests [@Girbons](https://github.com/ss), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia)!
|
||||
|
|
|
|||
|
|
@ -0,0 +1,589 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/verybluebot/tarinator-go"
|
||||
|
||||
"github.com/gosuri/uiprogress"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Connection struct {
|
||||
Server string
|
||||
File FileMetaData
|
||||
NumberOfConnections int
|
||||
Code string
|
||||
HashedCode string
|
||||
Path string
|
||||
IsSender bool
|
||||
AskPath bool
|
||||
Debug bool
|
||||
DontEncrypt bool
|
||||
Wait bool
|
||||
bars []*uiprogress.Bar
|
||||
rate int
|
||||
}
|
||||
|
||||
type FileMetaData struct {
|
||||
Name string
|
||||
Size int
|
||||
Hash string
|
||||
Path string
|
||||
IsDir bool
|
||||
IsEncrypted bool
|
||||
}
|
||||
|
||||
const (
|
||||
crocReceiveDir = "croc_received"
|
||||
tmpTarGzFileName = "to_send.tmp.tar.gz"
|
||||
)
|
||||
|
||||
func NewConnection(flags *Flags) (*Connection, error) {
|
||||
c := new(Connection)
|
||||
c.Debug = flags.Debug
|
||||
c.DontEncrypt = flags.DontEncrypt
|
||||
c.Wait = flags.Wait
|
||||
c.Server = flags.Server
|
||||
c.Code = flags.Code
|
||||
c.NumberOfConnections = flags.NumberOfConnections
|
||||
c.rate = flags.Rate
|
||||
if len(flags.File) > 0 {
|
||||
// check wether the file is a dir
|
||||
info, err := os.Stat(flags.File)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if info.Mode().IsDir() { // if our file is a dir
|
||||
fmt.Println("compressing directory...")
|
||||
|
||||
// we "tarify" the file
|
||||
err = tarinator.Tarinate([]string{flags.File}, path.Base(flags.File)+".tar")
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// now, we change the target file name to match the new archive created
|
||||
flags.File = path.Base(flags.File) + ".tar"
|
||||
// we set the value IsDir to true
|
||||
c.File.IsDir = true
|
||||
}
|
||||
c.File.Name = path.Base(flags.File)
|
||||
c.File.Path = path.Dir(flags.File)
|
||||
c.IsSender = true
|
||||
} else {
|
||||
c.IsSender = false
|
||||
c.AskPath = flags.PathSpec
|
||||
c.Path = flags.Path
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
if c.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Run() error {
|
||||
forceSingleThreaded := false
|
||||
if c.IsSender {
|
||||
fsize, err := FileSize(path.Join(c.File.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fsize < MAX_NUMBER_THREADS*BUFFERSIZE {
|
||||
forceSingleThreaded = true
|
||||
log.Debug("forcing single thread")
|
||||
}
|
||||
}
|
||||
log.Debug("checking code validity")
|
||||
for {
|
||||
// check code
|
||||
goodCode := true
|
||||
m := strings.Split(c.Code, "-")
|
||||
log.Debug(m)
|
||||
numThreads, errParse := strconv.Atoi(m[0])
|
||||
if len(m) < 2 {
|
||||
goodCode = false
|
||||
log.Debug("code too short")
|
||||
} else if numThreads > MAX_NUMBER_THREADS || numThreads < 1 || (forceSingleThreaded && numThreads != 1) {
|
||||
c.NumberOfConnections = MAX_NUMBER_THREADS
|
||||
goodCode = false
|
||||
log.Debug("incorrect number of threads")
|
||||
} else if errParse != nil {
|
||||
goodCode = false
|
||||
log.Debug("problem parsing threads")
|
||||
}
|
||||
log.Debug(m)
|
||||
log.Debug(goodCode)
|
||||
if !goodCode {
|
||||
if c.IsSender {
|
||||
if forceSingleThreaded {
|
||||
c.NumberOfConnections = 1
|
||||
}
|
||||
c.Code = strconv.Itoa(c.NumberOfConnections) + "-" + GetRandomName()
|
||||
} else {
|
||||
if len(c.Code) != 0 {
|
||||
fmt.Println("Code must begin with number of threads (e.g. 3-some-code)")
|
||||
}
|
||||
c.Code = getInput("Enter receive code: ")
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// assign number of connections
|
||||
c.NumberOfConnections, _ = strconv.Atoi(strings.Split(c.Code, "-")[0])
|
||||
|
||||
if c.IsSender {
|
||||
if c.DontEncrypt {
|
||||
// don't encrypt
|
||||
CopyFile(path.Join(c.File.Path, c.File.Name), c.File.Name+".enc")
|
||||
c.File.IsEncrypted = false
|
||||
} else {
|
||||
// encrypt
|
||||
log.Debug("encrypting...")
|
||||
if err := EncryptFile(path.Join(c.File.Path, c.File.Name), c.File.Name+".enc", c.Code); err != nil {
|
||||
return err
|
||||
}
|
||||
c.File.IsEncrypted = true
|
||||
}
|
||||
// split file into pieces to send
|
||||
if err := SplitFile(c.File.Name+".enc", c.NumberOfConnections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get file hash
|
||||
var err error
|
||||
c.File.Hash, err = HashFile(path.Join(c.File.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get file size
|
||||
c.File.Size, err = FileSize(c.File.Name + ".enc")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// remove the file now since we still have pieces
|
||||
if err := os.Remove(c.File.Name + ".enc"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove compressed archive
|
||||
if c.File.IsDir {
|
||||
log.Debug("removing archive: " + c.File.Name)
|
||||
if err := os.Remove(c.File.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.File.IsDir {
|
||||
fmt.Printf("Sending %s folder named '%s'\n", humanize.Bytes(uint64(c.File.Size)), c.File.Name[:len(c.File.Name)-4])
|
||||
} else {
|
||||
fmt.Printf("Sending %s file named '%s'\n", humanize.Bytes(uint64(c.File.Size)), c.File.Name)
|
||||
|
||||
}
|
||||
fmt.Printf("Code is: %s\n", c.Code)
|
||||
}
|
||||
|
||||
return c.runClient()
|
||||
}
|
||||
|
||||
// runClient spawns threads for parallel uplink/downlink via TCP
|
||||
func (c *Connection) runClient() error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"code": c.Code,
|
||||
"sender?": c.IsSender,
|
||||
})
|
||||
|
||||
c.HashedCode = Hash(c.Code)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(c.NumberOfConnections)
|
||||
|
||||
uiprogress.Start()
|
||||
if !c.Debug {
|
||||
c.bars = make([]*uiprogress.Bar, c.NumberOfConnections)
|
||||
}
|
||||
gotTimeout := false
|
||||
gotOK := false
|
||||
gotResponse := false
|
||||
gotConnectionInUse := false
|
||||
notPresent := false
|
||||
for id := 0; id < c.NumberOfConnections; id++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
port := strconv.Itoa(27001 + id)
|
||||
connection, err := net.Dial("tcp", c.Server+":"+port)
|
||||
if err != nil {
|
||||
if c.Server == "cowyo.com" {
|
||||
fmt.Println("\nCheck http://bit.ly/croc-relay to see if the public server is down or contact the webmaster: @yakczar")
|
||||
} else {
|
||||
fmt.Printf("\nCould not connect to relay %s\n", c.Server)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
message := receiveMessage(connection)
|
||||
logger.Debugf("relay says: %s", message)
|
||||
if c.IsSender {
|
||||
logger.Debugf("telling relay: %s", "s."+c.Code)
|
||||
metaData, err := json.Marshal(c.File)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
encryptedMetaData, salt, iv := Encrypt(metaData, c.Code)
|
||||
sendMessage("s."+c.HashedCode+"."+hex.EncodeToString(encryptedMetaData)+"-"+salt+"-"+iv, connection)
|
||||
} else {
|
||||
logger.Debugf("telling relay: %s", "r."+c.Code)
|
||||
if c.Wait {
|
||||
// tell server to wait for sender
|
||||
sendMessage("r."+c.HashedCode+".0.0.0", connection)
|
||||
} else {
|
||||
// tell server to cancel if sender doesn't exist
|
||||
sendMessage("c."+c.HashedCode+".0.0.0", connection)
|
||||
}
|
||||
}
|
||||
if c.IsSender { // this is a sender
|
||||
logger.Debug("waiting for ok from relay")
|
||||
message = receiveMessage(connection)
|
||||
if message == "timeout" {
|
||||
gotTimeout = true
|
||||
fmt.Println("You've just exceeded limit waiting time.")
|
||||
return
|
||||
}
|
||||
if message == "no" {
|
||||
if id == 0 {
|
||||
fmt.Println("The specifed code is already in use by a sender.")
|
||||
}
|
||||
gotConnectionInUse = true
|
||||
} else {
|
||||
logger.Debug("got ok from relay")
|
||||
if id == 0 {
|
||||
fmt.Printf("\nSending (->%s)..\n", message)
|
||||
}
|
||||
// wait for pipe to be made
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Write data from file
|
||||
logger.Debug("send file")
|
||||
if err := c.sendFile(id, connection); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
} else { // this is a receiver
|
||||
logger.Debug("waiting for meta data from sender")
|
||||
message = receiveMessage(connection)
|
||||
if message == "no" {
|
||||
if id == 0 {
|
||||
fmt.Println("The specifed code is already in use by a sender.")
|
||||
}
|
||||
gotConnectionInUse = true
|
||||
} else {
|
||||
m := strings.Split(message, "-")
|
||||
encryptedData, salt, iv, sendersAddress := m[0], m[1], m[2], m[3]
|
||||
if sendersAddress == "0.0.0.0" {
|
||||
notPresent = true
|
||||
time.Sleep(1 * time.Second)
|
||||
return
|
||||
}
|
||||
encryptedBytes, err := hex.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
decryptedBytes, _ := Decrypt(encryptedBytes, c.Code, salt, iv, c.DontEncrypt)
|
||||
err = json.Unmarshal(decryptedBytes, &c.File)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("meta data received: %v", c.File)
|
||||
// have the main thread ask for the okay
|
||||
if id == 0 {
|
||||
fType := "file"
|
||||
fName := path.Join(c.Path, c.File.Name)
|
||||
if c.File.IsDir {
|
||||
fType = "folder"
|
||||
fName = fName[:len(fName)-4]
|
||||
}
|
||||
if _, err := os.Stat(path.Join(c.Path, c.File.Name)); os.IsNotExist(err) {
|
||||
fmt.Printf("Receiving %s (%s) into: %s\n", fType, humanize.Bytes(uint64(c.File.Size)), fName)
|
||||
} else {
|
||||
fmt.Printf("Overwriting %s %s (%s)\n", fType, fName, humanize.Bytes(uint64(c.File.Size)))
|
||||
}
|
||||
var sentFileNames []string
|
||||
|
||||
if c.AskPath {
|
||||
getPath := getInput("path: ")
|
||||
if len(getPath) > 0 {
|
||||
c.Path = path.Clean(getPath)
|
||||
}
|
||||
}
|
||||
if fileAlreadyExists(sentFileNames, c.File.Name) {
|
||||
fmt.Printf("Will not overwrite file!")
|
||||
os.Exit(1)
|
||||
}
|
||||
getOK := getInput("ok? (y/n): ")
|
||||
if getOK == "y" {
|
||||
gotOK = true
|
||||
sentFileNames = append(sentFileNames, c.File.Name)
|
||||
}
|
||||
gotResponse = true
|
||||
}
|
||||
// wait for the main thread to get the okay
|
||||
for limit := 0; limit < 1000; limit++ {
|
||||
if gotResponse {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if !gotOK {
|
||||
sendMessage("not ok", connection)
|
||||
} else {
|
||||
sendMessage("ok", connection)
|
||||
logger.Debug("receive file")
|
||||
if id == 0 {
|
||||
fmt.Printf("\n\nReceiving (<-%s)..\n", sendersAddress)
|
||||
}
|
||||
if err := c.receiveFile(id, connection); err != nil {
|
||||
log.Error(errors.Wrap(err, "Problem receiving the file: "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if gotConnectionInUse {
|
||||
return nil // connection was in use, just quit cleanly
|
||||
}
|
||||
|
||||
if c.IsSender {
|
||||
if gotTimeout {
|
||||
fmt.Println("Timeout waiting for receiver")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("\nFile sent.")
|
||||
} else { // Is a Receiver
|
||||
if notPresent {
|
||||
fmt.Println("Sender is not ready. Use -wait to wait until sender connects.")
|
||||
return nil
|
||||
}
|
||||
if !gotOK {
|
||||
return errors.New("Transfer interrupted")
|
||||
}
|
||||
if err := c.catFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Code: [%s]", c.Code)
|
||||
if c.DontEncrypt {
|
||||
if err := CopyFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if c.File.IsEncrypted {
|
||||
if err := DecryptFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name), c.Code); err != nil {
|
||||
return errors.Wrap(err, "Problem decrypting file")
|
||||
}
|
||||
} else {
|
||||
if err := CopyFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name)); err != nil {
|
||||
return errors.Wrap(err, "Problem copying file")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !c.Debug {
|
||||
os.Remove(path.Join(c.Path, c.File.Name+".enc"))
|
||||
}
|
||||
|
||||
fileHash, err := HashFile(path.Join(c.Path, c.File.Name))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("\n\n\ndownloaded hash: [%s]", fileHash)
|
||||
log.Debugf("\n\n\nrelayed hash: [%s]", c.File.Hash)
|
||||
|
||||
if c.File.Hash != fileHash {
|
||||
return fmt.Errorf("\nUh oh! %s is corrupted! Sorry, try again.\n", c.File.Name)
|
||||
} else {
|
||||
if c.File.IsDir { // if the file was originally a dir
|
||||
fmt.Print("decompressing folder")
|
||||
log.Debug("untarring " + c.File.Name)
|
||||
err := tarinator.UnTarinate(c.Path, path.Join(c.Path, c.File.Name))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we remove the old tar.gz file
|
||||
err = os.Remove(path.Join(c.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\nReceived folder written to %s\n", path.Join(c.Path, c.File.Name[:len(c.File.Name)-4]))
|
||||
} else {
|
||||
fmt.Printf("\nReceived file written to %s\n", path.Join(c.Path, c.File.Name))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileAlreadyExists(s []string, f string) bool {
|
||||
for _, a := range s {
|
||||
if a == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Connection) catFile() error {
|
||||
// cat the file
|
||||
files := make([]string, c.NumberOfConnections)
|
||||
for id := range files {
|
||||
files[id] = path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id))
|
||||
}
|
||||
toRemove := true
|
||||
if c.Debug {
|
||||
toRemove = false
|
||||
}
|
||||
return CatFiles(files, path.Join(c.Path, c.File.Name+".enc"), toRemove)
|
||||
}
|
||||
|
||||
func (c *Connection) receiveFile(id int, connection net.Conn) error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "receiveFile #" + strconv.Itoa(id),
|
||||
})
|
||||
|
||||
logger.Debug("waiting for chunk size from sender")
|
||||
fileSizeBuffer := make([]byte, 10)
|
||||
connection.Read(fileSizeBuffer)
|
||||
fileDataString := strings.Trim(string(fileSizeBuffer), ":")
|
||||
fileSizeInt, _ := strconv.Atoi(fileDataString)
|
||||
chunkSize := int64(fileSizeInt)
|
||||
logger.Debugf("chunk size: %d", chunkSize)
|
||||
if chunkSize == 0 {
|
||||
logger.Debug(fileSizeBuffer)
|
||||
return errors.New("chunk size is empty!")
|
||||
}
|
||||
|
||||
os.Remove(path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id)))
|
||||
log.Debug("Making " + c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
newFile, err := os.Create(path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
if !c.Debug {
|
||||
c.bars[id] = uiprogress.AddBar(int(chunkSize)/1024 + 1).AppendCompleted().PrependElapsed()
|
||||
}
|
||||
|
||||
logger.Debug("waiting for file")
|
||||
var receivedBytes int64
|
||||
receivedFirstBytes := false
|
||||
for {
|
||||
if !c.Debug {
|
||||
c.bars[id].Incr()
|
||||
}
|
||||
if (chunkSize - receivedBytes) < BUFFERSIZE {
|
||||
logger.Debug("at the end")
|
||||
io.CopyN(newFile, connection, (chunkSize - receivedBytes))
|
||||
// Empty the remaining bytes that we don't need from the network buffer
|
||||
if (receivedBytes+BUFFERSIZE)-chunkSize < BUFFERSIZE {
|
||||
logger.Debug("empty remaining bytes from network buffer")
|
||||
connection.Read(make([]byte, (receivedBytes+BUFFERSIZE)-chunkSize))
|
||||
}
|
||||
break
|
||||
}
|
||||
io.CopyN(newFile, connection, BUFFERSIZE)
|
||||
receivedBytes += BUFFERSIZE
|
||||
if !receivedFirstBytes {
|
||||
receivedFirstBytes = true
|
||||
logger.Debug("Receieved first bytes!")
|
||||
}
|
||||
}
|
||||
logger.Debug("received file")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connection) sendFile(id int, connection net.Conn) error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "sendFile #" + strconv.Itoa(id),
|
||||
})
|
||||
defer connection.Close()
|
||||
|
||||
// open encrypted file chunk
|
||||
logger.Debug("opening encrypted file chunk: " + c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
file, err := os.Open(c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// determine and send the file size to client
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debugf("sending chunk size: %d", fi.Size())
|
||||
_, err = connection.Write([]byte(fillString(strconv.FormatInt(int64(fi.Size()), 10), 10)))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Problem sending chunk data: ")
|
||||
}
|
||||
|
||||
// show the progress
|
||||
if !c.Debug {
|
||||
logger.Debug("going to show progress")
|
||||
c.bars[id] = uiprogress.AddBar(int(fi.Size())).AppendCompleted().PrependElapsed()
|
||||
}
|
||||
|
||||
// rate limit the bandwidth
|
||||
logger.Debug("determining rate limiting")
|
||||
bufferSizeInKilobytes := BUFFERSIZE / 1024
|
||||
rate := float64(c.rate) / float64(c.NumberOfConnections*bufferSizeInKilobytes)
|
||||
throttle := time.NewTicker(time.Second / time.Duration(rate))
|
||||
defer throttle.Stop()
|
||||
|
||||
// send the file
|
||||
sendBuffer := make([]byte, BUFFERSIZE)
|
||||
totalBytesSent := 0
|
||||
for range throttle.C {
|
||||
n, err := file.Read(sendBuffer)
|
||||
connection.Write(sendBuffer)
|
||||
totalBytesSent += n
|
||||
if !c.Debug {
|
||||
c.bars[id].Set(totalBytesSent)
|
||||
}
|
||||
if err == io.EOF {
|
||||
//End of file reached, break out of for loop
|
||||
logger.Debug("EOF")
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.Debug("file is sent")
|
||||
logger.Debug("removing piece")
|
||||
if !c.Debug {
|
||||
file.Close()
|
||||
err = os.Remove(c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
if [ -n "$CROC_PASS" ]; then
|
||||
set -- --pass "$CROC_PASS" "$@"
|
||||
fi
|
||||
exec /croc "$@"
|
||||
12
croc.service
12
croc.service
|
|
@ -1,12 +0,0 @@
|
|||
[Unit]
|
||||
Description=croc relay
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=yes
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
ExecStart=/usr/bin/croc relay
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mars9/crypt"
|
||||
"github.com/schollz/mnemonicode"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mathrand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
func GetRandomName() string {
|
||||
result := []string{}
|
||||
bs := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(bs, mathrand.Uint32())
|
||||
result = mnemonicode.EncodeWordList(result, bs)
|
||||
return strings.Join(result, "-")
|
||||
}
|
||||
|
||||
func Encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypted []byte, salt string, iv string) {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return plaintext, "salt", "iv"
|
||||
}
|
||||
key, saltBytes := deriveKey(passphrase, nil)
|
||||
ivBytes := make([]byte, 12)
|
||||
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
||||
// Section 8.2
|
||||
rand.Read(ivBytes)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
|
||||
salt = hex.EncodeToString(saltBytes)
|
||||
iv = hex.EncodeToString(ivBytes)
|
||||
return
|
||||
}
|
||||
|
||||
func Decrypt(data []byte, passphrase string, salt string, iv string, dontencrypt ...bool) (plaintext []byte, err error) {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return data, nil
|
||||
}
|
||||
saltBytes, _ := hex.DecodeString(salt)
|
||||
ivBytes, _ := hex.DecodeString(iv)
|
||||
key, _ := deriveKey(passphrase, saltBytes)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
plaintext, err = aesgcm.Open(nil, ivBytes, data, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
|
||||
if salt == nil {
|
||||
salt = make([]byte, 8)
|
||||
// http://www.ietf.org/rfc/rfc2898.txt
|
||||
// Salt.
|
||||
rand.Read(salt)
|
||||
}
|
||||
return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
|
||||
}
|
||||
|
||||
func Hash(data string) string {
|
||||
return HashBytes([]byte(data))
|
||||
}
|
||||
|
||||
func HashBytes(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func EncryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, true)
|
||||
}
|
||||
|
||||
func DecryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, false)
|
||||
}
|
||||
|
||||
func cryptFile(inputFilename string, outputFilename string, password string, encrypt bool) error {
|
||||
in, err := os.Open(inputFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(outputFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := out.Sync(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
c := &crypt.Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: crypt.NewPbkdf2Key([]byte(password), 32),
|
||||
}
|
||||
if encrypt {
|
||||
if err := c.Encrypt(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := c.Decrypt(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
encrypted, salt, iv := Encrypt([]byte("hello, world"), key)
|
||||
decrypted, err := Decrypt(encrypted, key, salt, iv)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(decrypted) != "hello, world" {
|
||||
t.Error("problem decrypting")
|
||||
}
|
||||
_, err = Decrypt(encrypted, "wrong passphrase", salt, iv)
|
||||
if err == nil {
|
||||
t.Error("should not work!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptFiles(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
if err := ioutil.WriteFile("temp", []byte("hello, world!"), 0644); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := EncryptFile("temp", "temp.enc", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data, err := ioutil.ReadFile("temp.dec")
|
||||
if string(data) != "hello, world!" {
|
||||
t.Errorf("Got something weird: " + string(data))
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key+"wrong password"); err == nil {
|
||||
t.Error("should throw error!")
|
||||
}
|
||||
os.Remove("temp.dec")
|
||||
os.Remove("temp.enc")
|
||||
os.Remove("temp")
|
||||
}
|
||||
39
go.mod
39
go.mod
|
|
@ -1,39 +0,0 @@
|
|||
module github.com/schollz/croc/v10
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/kalafut/imohash v1.1.0
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b
|
||||
github.com/minio/highwayhash v1.0.3
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/schollz/cli/v2 v2.2.1
|
||||
github.com/schollz/logger v1.2.0
|
||||
github.com/schollz/pake/v3 v3.0.5
|
||||
github.com/schollz/peerdiscovery v1.7.5
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/time v0.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect
|
||||
github.com/twmb/murmur3 v1.1.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
148
go.sum
148
go.sum
|
|
@ -1,148 +0,0 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kalafut/imohash v1.1.0 h1:Lldcmx0SXgMSoABB2WBD8mTgf0OlVnISn2Dyrfg2Ep8=
|
||||
github.com/kalafut/imohash v1.1.0/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
|
||||
github.com/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU=
|
||||
github.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s=
|
||||
github.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho=
|
||||
github.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM=
|
||||
github.com/schollz/pake/v3 v3.0.5 h1:MnZVdI987lkjln9BSx/zUb724TZISa2jbO+dPj6BvgQ=
|
||||
github.com/schollz/pake/v3 v3.0.5/go.mod h1:OGbG6htRwSKo6V8R5tg61ufpFmZM1b/PrrSp6g2ZLLc=
|
||||
github.com/schollz/peerdiscovery v1.7.5 h1:0cEhO+o8i4fpeKBwl7u0UY3Kt3XVt5fSzS4rg17ZPb4=
|
||||
github.com/schollz/peerdiscovery v1.7.5/go.mod h1:Crht2FOfD1/eL3U/AIM0vvwVZDPePlBgSX3Xw+TnJoE=
|
||||
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
|
||||
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tscholl2/siec v0.0.0-20210707234609-9bdfc483d499/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U=
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
|
||||
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# .goreleaser.yml
|
||||
# Build customization
|
||||
builds:
|
||||
- binary: croc
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- 386
|
||||
goarm:
|
||||
- 6
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
archive:
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
darwin: OSX
|
||||
linux_arm: raspberry_pi
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: darwin
|
||||
format: zip
|
||||
113
main.go
113
main.go
|
|
@ -1,55 +1,78 @@
|
|||
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 (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/schollz/croc/v10/src/cli"
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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")
|
||||
// }
|
||||
// }()
|
||||
const BUFFERSIZE = 1024
|
||||
|
||||
// Create a channel to receive OS signals
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
var oneGigabytePerSecond = 1000000 // expressed as kbps
|
||||
|
||||
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)
|
||||
type Flags struct {
|
||||
Relay bool
|
||||
Debug bool
|
||||
Wait bool
|
||||
PathSpec bool
|
||||
DontEncrypt bool
|
||||
Server string
|
||||
File string
|
||||
Path string
|
||||
Code string
|
||||
Rate int
|
||||
NumberOfConnections int
|
||||
}
|
||||
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
fmt.Println(`
|
||||
,_
|
||||
>' )
|
||||
croc version ` + fmt.Sprintf("%5s", version) + ` ( ( \
|
||||
|| \
|
||||
/^^^^\ ||
|
||||
/^^\________/0 \ ||
|
||||
( ` + "`" + `~+++,,_||__,,++~^^^^^^^
|
||||
...V^V^V^V^V^V^\...............................
|
||||
|
||||
`)
|
||||
flags := new(Flags)
|
||||
flag.BoolVar(&flags.Relay, "relay", false, "run as relay")
|
||||
flag.BoolVar(&flags.Debug, "debug", false, "debug mode")
|
||||
flag.BoolVar(&flags.Wait, "wait", false, "wait for code to be sent")
|
||||
flag.BoolVar(&flags.PathSpec, "ask-save", false, "ask for path to save to")
|
||||
flag.StringVar(&flags.Server, "server", "cowyo.com", "address of relay server")
|
||||
flag.StringVar(&flags.File, "send", "", "file to send")
|
||||
flag.StringVar(&flags.Path, "save", "", "path to save to")
|
||||
flag.StringVar(&flags.Code, "code", "", "use your own code phrase")
|
||||
flag.IntVar(&flags.Rate, "rate", oneGigabytePerSecond, "throttle down to speed in kbps")
|
||||
flag.BoolVar(&flags.DontEncrypt, "no-encrypt", false, "turn off encryption")
|
||||
flag.IntVar(&flags.NumberOfConnections, "threads", 4, "number of threads to use")
|
||||
flag.Parse()
|
||||
|
||||
if flags.Relay {
|
||||
r := NewRelay(flags)
|
||||
r.Run()
|
||||
} else {
|
||||
c, err := NewConnection(flags)
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'%s'\n\n", err.Error())
|
||||
return
|
||||
}
|
||||
err = c.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'%s'\n\n", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print(prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,317 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const MAX_NUMBER_THREADS = 8
|
||||
const CONNECTION_TIMEOUT = time.Hour
|
||||
|
||||
type connectionMap struct {
|
||||
receiver map[string]net.Conn
|
||||
sender map[string]net.Conn
|
||||
metadata map[string]string
|
||||
potentialReceivers map[string]struct{}
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *connectionMap) IsSenderConnected(key string) (found bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
_, found = c.sender[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *connectionMap) IsPotentialReceiverConnected(key string) (found bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
_, found = c.potentialReceivers[key]
|
||||
return
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
connections connectionMap
|
||||
Debug bool
|
||||
NumberOfConnections int
|
||||
}
|
||||
|
||||
func NewRelay(flags *Flags) *Relay {
|
||||
r := new(Relay)
|
||||
r.Debug = flags.Debug
|
||||
r.NumberOfConnections = MAX_NUMBER_THREADS
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
if r.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Relay) Run() {
|
||||
r.connections = connectionMap{}
|
||||
r.connections.Lock()
|
||||
r.connections.receiver = make(map[string]net.Conn)
|
||||
r.connections.sender = make(map[string]net.Conn)
|
||||
r.connections.metadata = make(map[string]string)
|
||||
r.connections.potentialReceivers = make(map[string]struct{})
|
||||
r.connections.Unlock()
|
||||
r.runServer()
|
||||
}
|
||||
|
||||
func (r *Relay) runServer() {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "main",
|
||||
})
|
||||
logger.Debug("Initializing")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(r.NumberOfConnections)
|
||||
for id := 0; id < r.NumberOfConnections; id++ {
|
||||
go r.listenerThread(id, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *Relay) listenerThread(id int, wg *sync.WaitGroup) {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "listenerThread:" + strconv.Itoa(27000+id),
|
||||
})
|
||||
|
||||
defer wg.Done()
|
||||
err := r.listener(id)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) listener(id int) (err error) {
|
||||
port := strconv.Itoa(27001 + id)
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "listener" + ":" + port,
|
||||
})
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listening on "+":"+port)
|
||||
}
|
||||
defer server.Close()
|
||||
logger.Debug("waiting for connections")
|
||||
//Spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "problem accepting connection")
|
||||
}
|
||||
logger.Debugf("Client %s connected", connection.RemoteAddr().String())
|
||||
go r.clientCommuncation(id, connection)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) clientCommuncation(id int, connection net.Conn) {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"id": id,
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
sendMessage("who?", connection)
|
||||
m := strings.Split(receiveMessage(connection), ".")
|
||||
if len(m) < 3 {
|
||||
logger.Debug("exiting, not enough information")
|
||||
sendMessage("not enough information", connection)
|
||||
return
|
||||
}
|
||||
connectionType, codePhrase, metaData := m[0], m[1], m[2]
|
||||
key := codePhrase + "-" + strconv.Itoa(id)
|
||||
|
||||
if connectionType == "s" { // sender connection
|
||||
if r.connections.IsSenderConnected(key) {
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
r.connections.Lock()
|
||||
r.connections.metadata[key] = metaData
|
||||
r.connections.sender[key] = connection
|
||||
r.connections.Unlock()
|
||||
// wait for receiver
|
||||
receiversAddress := ""
|
||||
isTimeout := time.Duration(0)
|
||||
for {
|
||||
if CONNECTION_TIMEOUT <= isTimeout {
|
||||
sendMessage("timeout", connection)
|
||||
break
|
||||
}
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.receiver[key]; ok {
|
||||
receiversAddress = r.connections.receiver[key].RemoteAddr().String()
|
||||
logger.Debug("got receiver")
|
||||
r.connections.RUnlock()
|
||||
break
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
isTimeout += 100 * time.Millisecond
|
||||
}
|
||||
logger.Debug("telling sender ok")
|
||||
sendMessage(receiversAddress, connection)
|
||||
logger.Debug("preparing pipe")
|
||||
r.connections.Lock()
|
||||
con1 := r.connections.sender[key]
|
||||
con2 := r.connections.receiver[key]
|
||||
r.connections.Unlock()
|
||||
logger.Debug("piping connections")
|
||||
Pipe(con1, con2)
|
||||
logger.Debug("done piping")
|
||||
r.connections.Lock()
|
||||
// close connections
|
||||
r.connections.sender[key].Close()
|
||||
r.connections.receiver[key].Close()
|
||||
// delete connctions
|
||||
delete(r.connections.sender, key)
|
||||
delete(r.connections.receiver, key)
|
||||
delete(r.connections.metadata, key)
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
r.connections.Unlock()
|
||||
logger.Debug("deleted sender and receiver")
|
||||
} else if connectionType == "r" || connectionType == "c" {
|
||||
//receiver
|
||||
if r.connections.IsPotentialReceiverConnected(key) {
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
// add as a potential receiver
|
||||
r.connections.Lock()
|
||||
r.connections.potentialReceivers[key] = struct{}{}
|
||||
r.connections.Unlock()
|
||||
// wait for sender's metadata
|
||||
sendersAddress := ""
|
||||
for {
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.metadata[key]; ok {
|
||||
if _, ok2 := r.connections.sender[key]; ok2 {
|
||||
sendersAddress = r.connections.sender[key].RemoteAddr().String()
|
||||
logger.Debug("got sender meta data")
|
||||
r.connections.RUnlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if connectionType == "c" {
|
||||
sendMessage("0-0-0-0.0.0.0", connection)
|
||||
// sender is not ready so delete connection
|
||||
r.connections.Lock()
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
r.connections.Unlock()
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
// send meta data
|
||||
r.connections.RLock()
|
||||
sendMessage(r.connections.metadata[key]+"-"+sendersAddress, connection)
|
||||
r.connections.RUnlock()
|
||||
// check for receiver's consent
|
||||
consent := receiveMessage(connection)
|
||||
logger.Debugf("consent: %s", consent)
|
||||
if consent == "ok" {
|
||||
logger.Debug("got consent")
|
||||
r.connections.Lock()
|
||||
r.connections.receiver[key] = connection
|
||||
r.connections.Unlock()
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("Got unknown protocol: '%s'", connectionType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sendMessage(message string, connection net.Conn) {
|
||||
message = fillString(message, BUFFERSIZE)
|
||||
connection.Write([]byte(message))
|
||||
}
|
||||
|
||||
func receiveMessage(connection net.Conn) string {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"func": "receiveMessage",
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
messageByte := make([]byte, BUFFERSIZE)
|
||||
err := connection.SetDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
_, err = connection.Read(messageByte)
|
||||
if err != nil {
|
||||
logger.Warn("read deadline, no response")
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(string(messageByte), ":", "", -1)
|
||||
}
|
||||
|
||||
func fillString(retunString string, toLength int) string {
|
||||
for {
|
||||
lengthString := len(retunString)
|
||||
if lengthString < toLength {
|
||||
retunString = retunString + ":"
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return retunString
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, BUFFERSIZE)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Pipe creates a full-duplex pipe between the two sockets and transfers data from one to the other.
|
||||
func Pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
} else {
|
||||
conn2.Write(b1)
|
||||
}
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
} else {
|
||||
conn1.Write(b2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
718
src/cli/cli.go
718
src/cli/cli.go
|
|
@ -1,718 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/schollz/cli/v2"
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/croc"
|
||||
"github.com/schollz/croc/v10/src/mnemonicode"
|
||||
"github.com/schollz/croc/v10/src/models"
|
||||
"github.com/schollz/croc/v10/src/tcp"
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/schollz/pake/v3"
|
||||
)
|
||||
|
||||
// Version specifies the version
|
||||
var Version string
|
||||
|
||||
// Run will run the command line program
|
||||
func Run() (err error) {
|
||||
// use all of the processors
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "croc"
|
||||
if Version == "" {
|
||||
Version = "v10.1.0"
|
||||
}
|
||||
app.Version = Version
|
||||
app.Compiled = time.Now()
|
||||
app.Usage = "easily and securely transfer stuff from one computer to another"
|
||||
app.UsageText = `croc [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder]
|
||||
|
||||
USAGE EXAMPLES:
|
||||
Send a file:
|
||||
croc send file.txt
|
||||
|
||||
-git to respect your .gitignore
|
||||
Send multiple files:
|
||||
croc send file1.txt file2.txt file3.txt
|
||||
or
|
||||
croc send *.jpg
|
||||
|
||||
Send everything in a folder:
|
||||
croc send example-folder-name
|
||||
|
||||
Send a file with a custom code:
|
||||
croc send --code secret-code file.txt
|
||||
|
||||
Receive a file using code:
|
||||
croc secret-code`
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "send",
|
||||
Usage: "send file(s), or folder (see options with croc send -h)",
|
||||
Description: "send file(s), or folder, over the relay",
|
||||
ArgsUsage: "[filename(s) or folder]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "zip", Usage: "zip folder before sending"},
|
||||
&cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay"},
|
||||
&cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5)"},
|
||||
&cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"},
|
||||
&cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"},
|
||||
&cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"},
|
||||
&cli.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"},
|
||||
&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
|
||||
&cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"},
|
||||
&cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show receive code as a qrcode"},
|
||||
},
|
||||
HelpName: "croc send",
|
||||
Action: send,
|
||||
},
|
||||
{
|
||||
Name: "relay",
|
||||
Usage: "start your own relay (optional)",
|
||||
Description: "start relay",
|
||||
HelpName: "croc relay",
|
||||
Action: relay,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "host", Usage: "host of the relay"},
|
||||
&cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"},
|
||||
&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
|
||||
&cli.IntFlag{Name: "transfers", Value: 5, Usage: "number of ports to use for relay"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{Name: "internal-dns", Usage: "use a built-in DNS stub resolver rather than the host operating system"},
|
||||
&cli.BoolFlag{Name: "classic", Usage: "toggle between the classic mode (insecure due to local attack vector) and new mode (secure)"},
|
||||
&cli.BoolFlag{Name: "remember", Usage: "save these settings to reuse next time"},
|
||||
&cli.BoolFlag{Name: "debug", Usage: "toggle debug mode"},
|
||||
&cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
|
||||
&cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
|
||||
&cli.BoolFlag{Name: "no-compress", Usage: "disable compression"},
|
||||
&cli.BoolFlag{Name: "ask", Usage: "make sure sender and recipient are prompted"},
|
||||
&cli.BoolFlag{Name: "local", Usage: "force to use only local connections"},
|
||||
&cli.BoolFlag{Name: "ignore-stdin", Usage: "ignore piped stdin"},
|
||||
&cli.BoolFlag{Name: "overwrite", Usage: "do not prompt to overwrite or resume"},
|
||||
&cli.BoolFlag{Name: "testing", Usage: "flag for testing purposes"},
|
||||
&cli.StringFlag{Name: "multicast", Value: "239.255.255.250", Usage: "multicast address to use for local discovery"},
|
||||
&cli.StringFlag{Name: "curve", Value: "p256", Usage: "choose an encryption curve (" + strings.Join(pake.AvailableCurves(), ", ") + ")"},
|
||||
&cli.StringFlag{Name: "ip", Value: "", Usage: "set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009"},
|
||||
&cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}},
|
||||
&cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}},
|
||||
&cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
|
||||
&cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}},
|
||||
&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
|
||||
&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
|
||||
&cli.StringFlag{Name: "throttleUpload", Value: "", Usage: "Throttle the upload speed e.g. 500k"},
|
||||
}
|
||||
app.EnableBashCompletion = true
|
||||
app.HideHelp = false
|
||||
app.HideVersion = false
|
||||
app.Action = func(c *cli.Context) error {
|
||||
allStringsAreFiles := func(strs []string) bool {
|
||||
for _, str := range strs {
|
||||
if !utils.Exists(str) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// check if "classic" is set
|
||||
classicFile := getClassicConfigFile(true)
|
||||
classicInsecureMode := utils.Exists(classicFile)
|
||||
if c.Bool("classic") {
|
||||
if classicInsecureMode {
|
||||
// classic mode not enabled
|
||||
fmt.Print(`Classic mode is currently ENABLED.
|
||||
|
||||
Disabling this mode will prevent the shared secret from being visible
|
||||
on the host's process list when passed via the command line. On a
|
||||
multi-user system, this will help ensure that other local users cannot
|
||||
access the shared secret and receive the files instead of the intended
|
||||
recipient.
|
||||
|
||||
Do you wish to continue to DISABLE the classic mode? (y/N) `)
|
||||
choice := strings.ToLower(utils.GetInput(""))
|
||||
if choice == "y" || choice == "yes" {
|
||||
os.Remove(classicFile)
|
||||
fmt.Print("\nClassic mode DISABLED.\n\n")
|
||||
fmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase:
|
||||
|
||||
Send: CROC_SECRET=*** croc send file.txt
|
||||
|
||||
Receive: CROC_SECRET=*** croc` + "\n\n")
|
||||
} else {
|
||||
fmt.Print("\nClassic mode ENABLED.\n")
|
||||
|
||||
}
|
||||
} else {
|
||||
// enable classic mode
|
||||
// touch the file
|
||||
fmt.Print(`Classic mode is currently DISABLED.
|
||||
|
||||
Please note that enabling this mode will make the shared secret visible
|
||||
on the host's process list when passed via the command line. On a
|
||||
multi-user system, this could allow other local users to access the
|
||||
shared secret and receive the files instead of the intended recipient.
|
||||
|
||||
Do you wish to continue to enable the classic mode? (y/N) `)
|
||||
choice := strings.ToLower(utils.GetInput(""))
|
||||
if choice == "y" || choice == "yes" {
|
||||
fmt.Print("\nClassic mode ENABLED.\n\n")
|
||||
os.WriteFile(classicFile, []byte("enabled"), 0o644)
|
||||
fmt.Print(`To send and receive, use the code phrase:
|
||||
|
||||
Send: croc send --code *** file.txt
|
||||
|
||||
Receive: croc ***` + "\n\n")
|
||||
} else {
|
||||
fmt.Print("\nClassic mode DISABLED.\n")
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// if trying to send but forgot send, let the user know
|
||||
if c.Args().Present() && allStringsAreFiles(c.Args().Slice()) {
|
||||
fnames := []string{}
|
||||
for _, fpath := range c.Args().Slice() {
|
||||
_, basename := filepath.Split(fpath)
|
||||
fnames = append(fnames, "'"+basename+"'")
|
||||
}
|
||||
promptMessage := fmt.Sprintf("Did you mean to send %s? (Y/n) ", strings.Join(fnames, ", "))
|
||||
choice := strings.ToLower(utils.GetInput(promptMessage))
|
||||
if choice == "" || choice == "y" || choice == "yes" {
|
||||
return send(c)
|
||||
}
|
||||
}
|
||||
|
||||
return receive(c)
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
func setDebugLevel(c *cli.Context) {
|
||||
if c.Bool("debug") {
|
||||
log.SetLevel("debug")
|
||||
log.Debug("debug mode on")
|
||||
// print the public IP address
|
||||
ip, err := utils.PublicIP()
|
||||
if err == nil {
|
||||
log.Debugf("public IP address: %s", ip)
|
||||
} else {
|
||||
log.Debug(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
log.SetLevel("info")
|
||||
}
|
||||
}
|
||||
|
||||
func getSendConfigFile(requireValidPath bool) string {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
return path.Join(configFile, "send.json")
|
||||
}
|
||||
|
||||
func getClassicConfigFile(requireValidPath bool) string {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
return path.Join(configFile, "classic_enabled")
|
||||
}
|
||||
|
||||
func getReceiveConfigFile(requireValidPath bool) (string, error) {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
return path.Join(configFile, "receive.json"), nil
|
||||
}
|
||||
|
||||
func determinePass(c *cli.Context) (pass string) {
|
||||
pass = c.String("pass")
|
||||
b, err := os.ReadFile(pass)
|
||||
if err == nil {
|
||||
pass = strings.TrimSpace(string(b))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func send(c *cli.Context) (err error) {
|
||||
setDebugLevel(c)
|
||||
comm.Socks5Proxy = c.String("socks5")
|
||||
comm.HttpProxy = c.String("connect")
|
||||
|
||||
portParam := c.Int("port")
|
||||
if portParam == 0 {
|
||||
portParam = 9009
|
||||
}
|
||||
transfersParam := c.Int("transfers")
|
||||
if transfersParam == 0 {
|
||||
transfersParam = 4
|
||||
}
|
||||
|
||||
ports := make([]string, transfersParam+1)
|
||||
for i := 0; i <= transfersParam; i++ {
|
||||
ports[i] = strconv.Itoa(portParam + i)
|
||||
}
|
||||
|
||||
crocOptions := croc.Options{
|
||||
SharedSecret: c.String("code"),
|
||||
IsSender: true,
|
||||
Debug: c.Bool("debug"),
|
||||
NoPrompt: c.Bool("yes"),
|
||||
RelayAddress: c.String("relay"),
|
||||
RelayAddress6: c.String("relay6"),
|
||||
Stdout: c.Bool("stdout"),
|
||||
DisableLocal: c.Bool("no-local"),
|
||||
OnlyLocal: c.Bool("local"),
|
||||
IgnoreStdin: c.Bool("ignore-stdin"),
|
||||
RelayPorts: ports,
|
||||
Ask: c.Bool("ask"),
|
||||
NoMultiplexing: c.Bool("no-multi"),
|
||||
RelayPassword: determinePass(c),
|
||||
SendingText: c.String("text") != "",
|
||||
NoCompress: c.Bool("no-compress"),
|
||||
Overwrite: c.Bool("overwrite"),
|
||||
Curve: c.String("curve"),
|
||||
HashAlgorithm: c.String("hash"),
|
||||
ThrottleUpload: c.String("throttleUpload"),
|
||||
ZipFolder: c.Bool("zip"),
|
||||
GitIgnore: c.Bool("git"),
|
||||
ShowQrCode: c.Bool("qrcode"),
|
||||
MulticastAddress: c.String("multicast"),
|
||||
}
|
||||
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
|
||||
crocOptions.RelayAddress6 = ""
|
||||
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
|
||||
crocOptions.RelayAddress = ""
|
||||
}
|
||||
b, errOpen := os.ReadFile(getSendConfigFile(false))
|
||||
if errOpen == nil && !c.Bool("remember") {
|
||||
var rememberedOptions croc.Options
|
||||
err = json.Unmarshal(b, &rememberedOptions)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// update anything that isn't explicitly set
|
||||
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
|
||||
crocOptions.RelayAddress = rememberedOptions.RelayAddress
|
||||
}
|
||||
if !c.IsSet("no-local") {
|
||||
crocOptions.DisableLocal = rememberedOptions.DisableLocal
|
||||
}
|
||||
if !c.IsSet("ports") && len(rememberedOptions.RelayPorts) > 0 {
|
||||
crocOptions.RelayPorts = rememberedOptions.RelayPorts
|
||||
}
|
||||
if !c.IsSet("code") {
|
||||
crocOptions.SharedSecret = rememberedOptions.SharedSecret
|
||||
}
|
||||
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
|
||||
crocOptions.RelayPassword = rememberedOptions.RelayPassword
|
||||
}
|
||||
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
|
||||
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
|
||||
}
|
||||
if !c.IsSet("overwrite") {
|
||||
crocOptions.Overwrite = rememberedOptions.Overwrite
|
||||
}
|
||||
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
|
||||
crocOptions.Curve = rememberedOptions.Curve
|
||||
}
|
||||
if !c.IsSet("local") {
|
||||
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
|
||||
}
|
||||
if !c.IsSet("hash") {
|
||||
crocOptions.HashAlgorithm = rememberedOptions.HashAlgorithm
|
||||
}
|
||||
if !c.IsSet("git") {
|
||||
crocOptions.GitIgnore = rememberedOptions.GitIgnore
|
||||
}
|
||||
}
|
||||
|
||||
var fnames []string
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool("ignore-stdin") {
|
||||
fnames, err = getStdin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
utils.MarkFileForRemoval(fnames[0])
|
||||
defer func() {
|
||||
e := os.Remove(fnames[0])
|
||||
if e != nil {
|
||||
log.Error(e)
|
||||
}
|
||||
}()
|
||||
} else if c.String("text") != "" {
|
||||
fnames, err = makeTempFileWithString(c.String("text"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
utils.MarkFileForRemoval(fnames[0])
|
||||
defer func() {
|
||||
e := os.Remove(fnames[0])
|
||||
if e != nil {
|
||||
log.Error(e)
|
||||
}
|
||||
}()
|
||||
|
||||
} else {
|
||||
fnames = c.Args().Slice()
|
||||
}
|
||||
if len(fnames) == 0 {
|
||||
return errors.New("must specify file: croc send [filename(s) or folder]")
|
||||
}
|
||||
|
||||
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
|
||||
if !classicInsecureMode {
|
||||
// if operating system is UNIX, then use environmental variable to set the code
|
||||
if (!(runtime.GOOS == "windows") && c.IsSet("code")) || os.Getenv("CROC_SECRET") != "" {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
if crocOptions.SharedSecret == "" {
|
||||
fmt.Printf(`On UNIX systems, to send with a custom code phrase,
|
||||
you need to set the environmental variable CROC_SECRET:
|
||||
|
||||
CROC_SECRET=**** croc send file.txt
|
||||
|
||||
Or you can have the code phrase automatically generated:
|
||||
|
||||
croc send file.txt
|
||||
|
||||
Or you can go back to the classic croc behavior by enabling classic mode:
|
||||
|
||||
croc --classic
|
||||
|
||||
`)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(crocOptions.SharedSecret) == 0 {
|
||||
// generate code phrase
|
||||
crocOptions.SharedSecret = utils.GetRandomName()
|
||||
}
|
||||
minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cr, err := croc.New(crocOptions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// save the config
|
||||
saveConfig(c, crocOptions)
|
||||
|
||||
err = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getStdin() (fnames []string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(f, os.Stdin)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnames = []string{f.Name()}
|
||||
return
|
||||
}
|
||||
|
||||
func makeTempFileWithString(s string) (fnames []string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = f.WriteString(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnames = []string{f.Name()}
|
||||
return
|
||||
}
|
||||
|
||||
func saveConfig(c *cli.Context, crocOptions croc.Options) {
|
||||
if c.Bool("remember") {
|
||||
configFile := getSendConfigFile(true)
|
||||
log.Debug("saving config file")
|
||||
var bConfig []byte
|
||||
// if the code wasn't set, don't save it
|
||||
if c.String("code") == "" {
|
||||
crocOptions.SharedSecret = ""
|
||||
}
|
||||
bConfig, err := json.MarshalIndent(crocOptions, "", " ")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(configFile, bConfig, 0o644)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("wrote %s", configFile)
|
||||
}
|
||||
}
|
||||
|
||||
type TabComplete struct{}
|
||||
|
||||
func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
|
||||
var words = strings.SplitAfter(string(line), "-")
|
||||
var lastPartialWord = words[len(words)-1]
|
||||
var nbCharacter = len(lastPartialWord)
|
||||
if nbCharacter == 0 {
|
||||
// No completion
|
||||
return [][]rune{[]rune("")}, 0
|
||||
}
|
||||
if len(words) == 1 && nbCharacter == utils.NbPinNumbers {
|
||||
// Check if word is indeed a number
|
||||
_, err := strconv.Atoi(lastPartialWord)
|
||||
if err == nil {
|
||||
return [][]rune{[]rune("-")}, nbCharacter
|
||||
}
|
||||
}
|
||||
var strArray [][]rune
|
||||
for _, s := range mnemonicode.WordList {
|
||||
if strings.HasPrefix(s, lastPartialWord) {
|
||||
var completionCandidate = s[nbCharacter:]
|
||||
if len(words) <= mnemonicode.WordsRequired(utils.NbBytesWords) {
|
||||
completionCandidate += "-"
|
||||
}
|
||||
strArray = append(strArray, []rune(completionCandidate))
|
||||
}
|
||||
}
|
||||
return strArray, nbCharacter
|
||||
}
|
||||
|
||||
func receive(c *cli.Context) (err error) {
|
||||
comm.Socks5Proxy = c.String("socks5")
|
||||
comm.HttpProxy = c.String("connect")
|
||||
crocOptions := croc.Options{
|
||||
SharedSecret: c.String("code"),
|
||||
IsSender: false,
|
||||
Debug: c.Bool("debug"),
|
||||
NoPrompt: c.Bool("yes"),
|
||||
RelayAddress: c.String("relay"),
|
||||
RelayAddress6: c.String("relay6"),
|
||||
Stdout: c.Bool("stdout"),
|
||||
Ask: c.Bool("ask"),
|
||||
RelayPassword: determinePass(c),
|
||||
OnlyLocal: c.Bool("local"),
|
||||
IP: c.String("ip"),
|
||||
Overwrite: c.Bool("overwrite"),
|
||||
Curve: c.String("curve"),
|
||||
TestFlag: c.Bool("testing"),
|
||||
MulticastAddress: c.String("multicast"),
|
||||
}
|
||||
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
|
||||
crocOptions.RelayAddress6 = ""
|
||||
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
|
||||
crocOptions.RelayAddress = ""
|
||||
}
|
||||
|
||||
switch c.Args().Len() {
|
||||
case 1:
|
||||
crocOptions.SharedSecret = c.Args().First()
|
||||
case 3:
|
||||
fallthrough
|
||||
case 4:
|
||||
var phrase []string
|
||||
phrase = append(phrase, c.Args().First())
|
||||
phrase = append(phrase, c.Args().Tail()...)
|
||||
crocOptions.SharedSecret = strings.Join(phrase, "-")
|
||||
}
|
||||
|
||||
// load options here
|
||||
setDebugLevel(c)
|
||||
|
||||
doRemember := c.Bool("remember")
|
||||
configFile, err := getReceiveConfigFile(doRemember)
|
||||
if err != nil && doRemember {
|
||||
return
|
||||
}
|
||||
b, errOpen := os.ReadFile(configFile)
|
||||
if errOpen == nil && !doRemember {
|
||||
var rememberedOptions croc.Options
|
||||
err = json.Unmarshal(b, &rememberedOptions)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// update anything that isn't explicitly Globally set
|
||||
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
|
||||
crocOptions.RelayAddress = rememberedOptions.RelayAddress
|
||||
}
|
||||
if !c.IsSet("yes") {
|
||||
crocOptions.NoPrompt = rememberedOptions.NoPrompt
|
||||
}
|
||||
if crocOptions.SharedSecret == "" {
|
||||
crocOptions.SharedSecret = rememberedOptions.SharedSecret
|
||||
}
|
||||
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
|
||||
crocOptions.RelayPassword = rememberedOptions.RelayPassword
|
||||
}
|
||||
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
|
||||
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
|
||||
}
|
||||
if !c.IsSet("overwrite") {
|
||||
crocOptions.Overwrite = rememberedOptions.Overwrite
|
||||
}
|
||||
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
|
||||
crocOptions.Curve = rememberedOptions.Curve
|
||||
}
|
||||
if !c.IsSet("local") {
|
||||
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
|
||||
}
|
||||
}
|
||||
|
||||
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
|
||||
if crocOptions.SharedSecret == "" && os.Getenv("CROC_SECRET") != "" {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
} else if !(runtime.GOOS == "windows") && crocOptions.SharedSecret != "" && !classicInsecureMode {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
if crocOptions.SharedSecret == "" {
|
||||
fmt.Printf(`On UNIX systems, to receive with croc you either need
|
||||
to set a code phrase using your environmental variables:
|
||||
|
||||
CROC_SECRET=**** croc
|
||||
|
||||
Or you can specify the code phrase when you run croc without
|
||||
declaring the secret on the command line:
|
||||
|
||||
croc
|
||||
Enter receive code: ****
|
||||
|
||||
Or you can go back to the classic croc behavior by enabling classic mode:
|
||||
|
||||
croc --classic
|
||||
|
||||
`)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
if crocOptions.SharedSecret == "" {
|
||||
l, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "Enter receive code: ",
|
||||
AutoComplete: TabComplete{},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
crocOptions.SharedSecret, err = l.Readline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.String("out") != "" {
|
||||
if err = os.Chdir(c.String("out")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cr, err := croc.New(crocOptions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// save the config
|
||||
if doRemember {
|
||||
log.Debug("saving config file")
|
||||
var bConfig []byte
|
||||
bConfig, err = json.MarshalIndent(crocOptions, "", " ")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(configFile, bConfig, 0o644)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("wrote %s", configFile)
|
||||
}
|
||||
|
||||
err = cr.Receive()
|
||||
return
|
||||
}
|
||||
|
||||
func relay(c *cli.Context) (err error) {
|
||||
log.Infof("starting croc relay version %v", Version)
|
||||
debugString := "info"
|
||||
if c.Bool("debug") {
|
||||
debugString = "debug"
|
||||
}
|
||||
host := c.String("host")
|
||||
var ports []string
|
||||
|
||||
if c.IsSet("ports") {
|
||||
ports = strings.Split(c.String("ports"), ",")
|
||||
} else {
|
||||
portString := c.Int("port")
|
||||
if portString == 0 {
|
||||
portString = 9009
|
||||
}
|
||||
transfersString := c.Int("transfers")
|
||||
if transfersString == 0 {
|
||||
transfersString = 4
|
||||
}
|
||||
ports = make([]string, transfersString)
|
||||
for i := range ports {
|
||||
ports[i] = strconv.Itoa(portString + i)
|
||||
}
|
||||
}
|
||||
|
||||
tcpPorts := strings.Join(ports[1:], ",")
|
||||
for i, port := range ports {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
go func(portStr string) {
|
||||
err := tcp.Run(debugString, host, portStr, determinePass(c))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}(port)
|
||||
}
|
||||
return tcp.Run(debugString, host, ports[0], determinePass(c), tcpPorts)
|
||||
}
|
||||
200
src/comm/comm.go
200
src/comm/comm.go
|
|
@ -1,200 +0,0 @@
|
|||
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")
|
||||
|
||||
// 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
|
||||
defer c.connection.SetDeadline(time.Time{})
|
||||
|
||||
// read until we get 4 bytes for the magic
|
||||
header := make([]byte, 4)
|
||||
_, err = io.ReadFull(c.connection, header)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package comm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestComm(t *testing.T) {
|
||||
token := make([]byte, 3000)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
port := "8001"
|
||||
go func() {
|
||||
log.Debugf("starting TCP server on " + port)
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(_ string, connection net.Conn) {
|
||||
c := New(connection)
|
||||
err = c.Send([]byte("hello, world"))
|
||||
assert.Nil(t, err)
|
||||
data, err := c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, computer"), data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte{'\x00'}, data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token, data)
|
||||
}(port, connection)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
a, err := NewConnection("127.0.0.1:"+port, 10*time.Minute)
|
||||
assert.Nil(t, err)
|
||||
data, err := a.Receive()
|
||||
assert.Equal(t, []byte("hello, world"), data)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, a.Send([]byte("hello, computer")))
|
||||
assert.Nil(t, a.Send([]byte{'\x00'}))
|
||||
|
||||
assert.Nil(t, a.Send(token))
|
||||
_ = a.Connection()
|
||||
a.Close()
|
||||
assert.NotNil(t, a.Send(token))
|
||||
_, err = a.Write(token)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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, -2)
|
||||
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()
|
||||
}
|
||||
|
||||
// compress uses flate to decompress an io.Reader
|
||||
func decompress(src io.Reader, dest io.Writer) {
|
||||
decompressor := flate.NewReader(src)
|
||||
if _, err := io.Copy(dest, decompressor); err != nil {
|
||||
log.Debugf("error copying data: %v", err)
|
||||
}
|
||||
decompressor.Close()
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package compress
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var fable = []byte(`The Frog and the Crocodile
|
||||
Once, there was a frog who lived in the middle of a swamp. His entire family had lived in that swamp for generations, but this particular frog decided that he had had quite enough wetness to last him a lifetime. He decided that he was going to find a dry place to live instead.
|
||||
|
||||
The only thing that separated him from dry land was a swampy, muddy, swiftly flowing river. But the river was home to all sorts of slippery, slittering snakes that loved nothing better than a good, plump frog for dinner, so Frog didn't dare try to swim across.
|
||||
|
||||
So for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.
|
||||
|
||||
The snakes hissed and jeered at him, daring him to come closer, but he refused. Occasionally they would slither closer, jaws open to attack, but the frog always leaped out of the way. But no matter how far upstream he searched or how far downstream, the frog wasn't able to find a way across the water.
|
||||
|
||||
He had felt certain that there would be a bridge, or a place where the banks came together, yet all he found was more reeds and water. After a while, even the snakes stopped teasing him and went off in search of easier prey.
|
||||
|
||||
The frog sighed in frustration and sat to sulk in the rushes. Suddenly, he spotted two big eyes staring at him from the water. The giant log-shaped animal opened its mouth and asked him, "What are you doing, Frog? Surely there are enough flies right there for a meal."
|
||||
|
||||
The frog croaked in surprise and leaped away from the crocodile. That creature could swallow him whole in a moment without thinking about it! Once he was a satisfied that he was a safe distance away, he answered. "I'm tired of living in swampy waters, and I want to travel to the other side of the river. But if I swim across, the snakes will eat me."
|
||||
|
||||
The crocodile harrumphed in agreement and sat, thinking, for a while. "Well, if you're afraid of the snakes, I could give you a ride across," he suggested.
|
||||
|
||||
"Oh no, I don't think so," Frog answered quickly. "You'd eat me on the way over, or go underwater so the snakes could get me!"
|
||||
|
||||
"Now why would I let the snakes get you? I think they're a terrible nuisance with all their hissing and slithering! The river would be much better off without them altogether! Anyway, if you're so worried that I might eat you, you can ride on my tail."
|
||||
|
||||
The frog considered his offer. He did want to get to dry ground very badly, and there didn't seem to be any other way across the river. He looked at the crocodile from his short, squat buggy eyes and wondered about the crocodile's motives. But if he rode on the tail, the croc couldn't eat him anyway. And he was right about the snakes--no self-respecting crocodile would give a meal to the snakes.
|
||||
|
||||
"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail."
|
||||
|
||||
The crocodile flopped his tail into the marshy mud and let the frog climb on, then he waddled out to the river. But he couldn't stick his tail into the water as a rudder because the frog was on it -- and if he put his tail in the water, the snakes would eat the frog. They clumsily floated downstream for a ways, until the crocodile said, "Hop onto my back so I can steer straight with my tail." The frog moved, and the journey smoothed out.
|
||||
|
||||
From where he was sitting, the frog couldn't see much except the back of Crocodile's head. "Why don't you hop up on my head so you can see everything around us?" Crocodile invited. `)
|
||||
|
||||
func BenchmarkCompressLevelMinusTwo(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(fable, -2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelNine(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(fable, 9)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
|
||||
data := make([]byte, 1000000)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(data, -2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelNineBinary(b *testing.B) {
|
||||
data := make([]byte, 1000000)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(data, 9)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
compressedB := CompressWithOption(fable, 9)
|
||||
dataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level 9: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
assert.Equal(t, fable, Decompress(compressedB))
|
||||
|
||||
compressedB = CompressWithOption(fable, -2)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
|
||||
compressedB = Compress(fable)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
|
||||
data := make([]byte, 4096)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compressedB = CompressWithOption(data, -2)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
|
||||
fmt.Printf("random, Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compressedB = CompressWithOption(data, 9)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
|
||||
|
||||
fmt.Printf("random, Level 9: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
|
||||
}
|
||||
2162
src/croc/croc.go
2162
src/croc/croc.go
File diff suppressed because it is too large
Load Diff
|
|
@ -1,400 +0,0 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"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)
|
||||
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)
|
||||
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)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err = receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("receive failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
s, err := filepath.EvalSymlinks(path.Join(pathName, "README.link"))
|
||||
if s != "../../README.md" && s != "..\\..\\README.md" {
|
||||
log.Debug(s)
|
||||
t.Errorf("symlink failed to transfer in folder")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("symlink transfer failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
func TestCrocIgnoreGit(t *testing.T) {
|
||||
log.SetLevel("trace")
|
||||
defer os.Remove(".gitignore")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
file, err := os.Create(".gitignore")
|
||||
if err != nil {
|
||||
log.Errorf("error creating file")
|
||||
}
|
||||
_, err = file.WriteString("LICENSE")
|
||||
if err != nil {
|
||||
log.Errorf("error writing to file")
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
// due to how files are ignored in this function, all we have to do to test is make sure LICENSE doesn't get included in FilesInfo.
|
||||
filesInfo, _, _, errGet := GetFilesInfo([]string{"../../LICENSE", ".gitignore", "croc.go"}, false, true)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
for _, file := range filesInfo {
|
||||
if strings.Contains(file.Name, "LICENSE") {
|
||||
t.Errorf("test failed, should ignore LICENSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrocLocal(t *testing.T) {
|
||||
log.SetLevel("trace")
|
||||
defer os.Remove("LICENSE")
|
||||
defer os.Remove("touched")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
log.Debug("setting up sender")
|
||||
sender, err := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8181",
|
||||
RelayPorts: []string{"8181", "8182"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: true,
|
||||
NoPrompt: true,
|
||||
DisableLocal: false,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
GitIgnore: false,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
log.Debug("setting up receiver")
|
||||
receiver, err := New(Options{
|
||||
IsSender: false,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8181",
|
||||
RelayPassword: "pass123",
|
||||
Stdout: true,
|
||||
NoPrompt: true,
|
||||
DisableLocal: false,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
os.Create("touched")
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../LICENSE", "touched"}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func 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)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
log.Debug(err)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestCleanUp(t *testing.T) {
|
||||
// windows allows files to be deleted only if they
|
||||
// are not open by another program so the remove actions
|
||||
// from the above tests will not always do a good clean up
|
||||
// This "test" will make sure
|
||||
operatingSystem := runtime.GOOS
|
||||
log.Debugf("The operating system is %s", operatingSystem)
|
||||
if operatingSystem == "windows" {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Debug("Full cleanup")
|
||||
var err error
|
||||
|
||||
for _, file := range []string{"README.md", "./README.md"} {
|
||||
err = os.Remove(file)
|
||||
if err == nil {
|
||||
log.Debugf("Successfully purged %s", file)
|
||||
} else {
|
||||
log.Debugf("%s was already purged.", file)
|
||||
}
|
||||
}
|
||||
for _, folder := range []string{"./testEmpty", "./link-in-folder"} {
|
||||
err = os.RemoveAll(folder)
|
||||
if err == nil {
|
||||
log.Debugf("Successfully purged %s", folder)
|
||||
} else {
|
||||
log.Debugf("%s was already purged.", folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
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 encrypt ChaCha20-Poly1305 using the pre-generated key
|
||||
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
|
||||
func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (encrypted []byte, err error) {
|
||||
if len(encryptedMsg) < aead.NonceSize() {
|
||||
err = fmt.Errorf("ciphertext too short")
|
||||
return
|
||||
}
|
||||
|
||||
// Split nonce and ciphertext.
|
||||
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]
|
||||
|
||||
// Decrypt the message and check it wasn't tampered with.
|
||||
encrypted, err = aead.Open(nil, nonce, ciphertext, nil)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package diskusage
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// DiskUsage contains usage data and provides user-friendly access methods
|
||||
type DiskUsage struct {
|
||||
stat *unix.Statfs_t
|
||||
}
|
||||
|
||||
// NewDiskUsage returns an object holding the disk usage of volumePath
|
||||
// or nil in case of error (invalid path, etc)
|
||||
func NewDiskUsage(volumePath string) *DiskUsage {
|
||||
stat := unix.Statfs_t{}
|
||||
err := unix.Statfs(volumePath, &stat)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &DiskUsage{&stat}
|
||||
}
|
||||
|
||||
// Free returns total free bytes on file system
|
||||
func (du *DiskUsage) Free() uint64 {
|
||||
return uint64(du.stat.Bfree) * uint64(du.stat.Bsize)
|
||||
}
|
||||
|
||||
// Available return total available bytes on file system to an unprivileged user
|
||||
func (du *DiskUsage) Available() uint64 {
|
||||
return uint64(du.stat.Bavail) * uint64(du.stat.Bsize)
|
||||
}
|
||||
|
||||
// Size returns total size of the file system
|
||||
func (du *DiskUsage) Size() uint64 {
|
||||
return uint64(du.stat.Blocks) * uint64(du.stat.Bsize)
|
||||
}
|
||||
|
||||
// Used returns total bytes used in file system
|
||||
func (du *DiskUsage) Used() uint64 {
|
||||
return du.Size() - du.Free()
|
||||
}
|
||||
|
||||
// Usage returns percentage of use on the file system
|
||||
func (du *DiskUsage) Usage() float32 {
|
||||
return float32(du.Used()) / float32(du.Size())
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package diskusage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var KB = uint64(1024)
|
||||
|
||||
func TestNewDiskUsage(t *testing.T) {
|
||||
usage := NewDiskUsage(".")
|
||||
fmt.Println("Free:", usage.Free()/(KB*KB))
|
||||
fmt.Println("Available:", usage.Available()/(KB*KB))
|
||||
fmt.Println("Size:", usage.Size()/(KB*KB))
|
||||
fmt.Println("Used:", usage.Used()/(KB*KB))
|
||||
fmt.Println("Usage:", usage.Usage()*100, "%")
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package diskusage
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type DiskUsage struct {
|
||||
freeBytes int64
|
||||
totalBytes int64
|
||||
availBytes int64
|
||||
}
|
||||
|
||||
// NewDiskUsage returns an object holding the disk usage of volumePath
|
||||
// or nil in case of error (invalid path, etc)
|
||||
func NewDiskUsage(volumePath string) *DiskUsage {
|
||||
h := windows.MustLoadDLL("kernel32.dll")
|
||||
c := h.MustFindProc("GetDiskFreeSpaceExW")
|
||||
|
||||
du := &DiskUsage{}
|
||||
|
||||
c.Call(
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(volumePath))),
|
||||
uintptr(unsafe.Pointer(&du.freeBytes)),
|
||||
uintptr(unsafe.Pointer(&du.totalBytes)),
|
||||
uintptr(unsafe.Pointer(&du.availBytes)))
|
||||
|
||||
return du
|
||||
}
|
||||
|
||||
// Free returns total free bytes on file system
|
||||
func (du *DiskUsage) Free() uint64 {
|
||||
return uint64(du.freeBytes)
|
||||
}
|
||||
|
||||
// Available returns total available bytes on file system to an unprivileged user
|
||||
func (du *DiskUsage) Available() uint64 {
|
||||
return uint64(du.availBytes)
|
||||
}
|
||||
|
||||
// Size returns total size of the file system
|
||||
func (du *DiskUsage) Size() uint64 {
|
||||
return uint64(du.totalBytes)
|
||||
}
|
||||
|
||||
// Used returns total bytes used in file system
|
||||
func (du *DiskUsage) Used() uint64 {
|
||||
return du.Size() - du.Free()
|
||||
}
|
||||
|
||||
// Usage returns percentage of use on the file system
|
||||
func (du *DiskUsage) Usage() float32 {
|
||||
return float32(du.Used()) / float32(du.Size())
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# VERSION=8.X.Y make release
|
||||
|
||||
release:
|
||||
cd ../../ && go run src/install/updateversion.go
|
||||
git commit -am "bump ${VERSION}"
|
||||
git tag -af v${VERSION} -m "v${VERSION}"
|
||||
git push
|
||||
git push --tags
|
||||
cp zsh_autocomplete ../../
|
||||
cp bash_autocomplete ../../
|
||||
cd ../../ && goreleaser release
|
||||
cd ../../ && ./src/install/prepare-sources-tarball.sh
|
||||
cd ../../ && ./src/install/upload-src-tarball.sh
|
||||
|
||||
test:
|
||||
cp zsh_autocomplete ../../
|
||||
cp bash_autocomplete ../../
|
||||
cd ../../ && go generate
|
||||
cd ../../ && goreleaser release --skip-publish
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
: ${PROG:=$(basename ${BASH_SOURCE})}
|
||||
|
||||
_cli_bash_autocomplete() {
|
||||
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
|
||||
local cur opts base
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
|
||||
else
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
|
||||
unset PROG
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 582 KiB |
|
|
@ -1,767 +0,0 @@
|
|||
#!/bin/bash -
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: default.txt
|
||||
#
|
||||
# USAGE: curl https://getcroc.schollz.com | bash
|
||||
# OR
|
||||
# wget -qO- https://getcroc.schollz.com | bash
|
||||
#
|
||||
# DESCRIPTION: croc Installer Script.
|
||||
#
|
||||
# This script installs croc into a specified prefix.
|
||||
# Default prefix = /usr/local/bin
|
||||
#
|
||||
# OPTIONS: -p, --prefix "${INSTALL_PREFIX}"
|
||||
# Prefix to install croc into. Defaults to /usr/local/bin
|
||||
# REQUIREMENTS: bash, uname, tar/unzip, curl/wget, sudo (if not run
|
||||
# as root), install, mktemp, sha256sum/shasum/sha256
|
||||
#
|
||||
# BUGS: ...hopefully not. Please report.
|
||||
#
|
||||
# NOTES: Homepage: https://schollz.com/software/croc
|
||||
# Issues: https://github.com/schollz/croc/issues
|
||||
#
|
||||
# CREATED: 08/10/2019 16:41
|
||||
# REVISION: 0.9.2
|
||||
#===============================================================================
|
||||
set -o nounset # Treat unset variables as an error
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# DEFAULTS
|
||||
#-------------------------------------------------------------------------------
|
||||
PREFIX="${PREFIX:-}"
|
||||
ANDROID_ROOT="${ANDROID_ROOT:-}"
|
||||
|
||||
# Termux on Android has ${PREFIX} set which already ends with '/usr'
|
||||
if [[ -n "${ANDROID_ROOT}" && -n "${PREFIX}" ]]; then
|
||||
INSTALL_PREFIX="${PREFIX}/bin"
|
||||
else
|
||||
INSTALL_PREFIX="/usr/local/bin"
|
||||
fi
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# FUNCTIONS
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_banner
|
||||
# DESCRIPTION: Prints a banner
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0
|
||||
#-------------------------------------------------------------------------------
|
||||
print_banner() {
|
||||
cat <<-'EOF'
|
||||
=================================================
|
||||
____
|
||||
/ ___|_ __ ___ ___
|
||||
| | | '__/ _ \ / __|
|
||||
| |___| | | (_) | (__
|
||||
\____|_| \___/ \___|
|
||||
|
||||
___ _ _ _
|
||||
|_ _|_ __ ___| |_ __ _| | | ___ _ __
|
||||
| || '_ \/ __| __/ _` | | |/ _ \ '__|
|
||||
| || | | \__ \ || (_| | | | __/ |
|
||||
|___|_| |_|___/\__\__,_|_|_|\___|_|
|
||||
==================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_help
|
||||
# DESCRIPTION: Prints out a help message
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0
|
||||
#-------------------------------------------------------------------------------
|
||||
print_help() {
|
||||
local help_header
|
||||
local help_message
|
||||
|
||||
help_header="croc Installer Script"
|
||||
help_message="Usage:
|
||||
-p INSTALL_PREFIX
|
||||
Prefix to install croc into. Directory must already exist.
|
||||
Default = /usr/local/bin ('\${PREFIX}/bin' on Termux for Android)
|
||||
|
||||
-h
|
||||
Prints this helpful message and exit."
|
||||
|
||||
echo "${help_header}"
|
||||
echo ""
|
||||
echo "${help_message}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_message
|
||||
# DESCRIPTION: Prints a message all fancy like
|
||||
# PARAMETERS: $1 = Message to print
|
||||
# $2 = Severity. info, ok, error, warn
|
||||
# RETURNS: Formatted Message to stdout
|
||||
#-------------------------------------------------------------------------------
|
||||
print_message() {
|
||||
local message
|
||||
local severity
|
||||
local red
|
||||
local green
|
||||
local yellow
|
||||
local nc
|
||||
|
||||
message="${1}"
|
||||
severity="${2}"
|
||||
red='\e[0;31m'
|
||||
green='\e[0;32m'
|
||||
yellow='\e[1;33m'
|
||||
nc='\e[0m'
|
||||
|
||||
case "${severity}" in
|
||||
"info" ) echo -e "${nc}${message}${nc}";;
|
||||
"ok" ) echo -e "${green}${message}${nc}";;
|
||||
"error" ) echo -e "${red}${message}${nc}";;
|
||||
"warn" ) echo -e "${yellow}${message}${nc}";;
|
||||
esac
|
||||
|
||||
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: make_tempdir
|
||||
# DESCRIPTION: Makes a temp dir using mktemp if available
|
||||
# PARAMETERS: $1 = Directory template
|
||||
# RETURNS: 0 = Created temp dir. Also prints temp file path to stdout
|
||||
# 1 = Failed to create temp dir
|
||||
# 20 = Failed to find mktemp
|
||||
#-------------------------------------------------------------------------------
|
||||
make_tempdir() {
|
||||
local template
|
||||
local tempdir
|
||||
local tempdir_rcode
|
||||
|
||||
template="${1}.XXXXXX"
|
||||
|
||||
if command -v mktemp >/dev/null 2>&1; then
|
||||
tempdir="$(mktemp -d -t "${template}")"
|
||||
tempdir_rcode="${?}"
|
||||
if [[ "${tempdir_rcode}" == "0" ]]; then
|
||||
echo "${tempdir}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: determine_os
|
||||
# DESCRIPTION: Attempts to determine host os using uname
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0 = OS Detected. Also prints detected os to stdout
|
||||
# 1 = Unknown OS
|
||||
# 20 = 'uname' not found in path
|
||||
#-------------------------------------------------------------------------------
|
||||
determine_os() {
|
||||
local uname_out
|
||||
|
||||
if command -v uname >/dev/null 2>&1; then
|
||||
uname_out="$(uname)"
|
||||
if [[ "${uname_out}" == "" ]]; then
|
||||
return 1
|
||||
else
|
||||
echo "${uname_out}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: determine_arch
|
||||
# DESCRIPTION: Attempt to determine architecture of host
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0 = Arch Detected. Also prints detected arch to stdout
|
||||
# 1 = Unknown arch
|
||||
# 20 = 'uname' not found in path
|
||||
#-------------------------------------------------------------------------------
|
||||
determine_arch() {
|
||||
local uname_out
|
||||
|
||||
if command -v uname >/dev/null 2>&1; then
|
||||
uname_out="$(uname -m)"
|
||||
if [[ "${uname_out}" == "" ]]; then
|
||||
return 1
|
||||
else
|
||||
echo "${uname_out}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: download_file
|
||||
# DESCRIPTION: Downloads a file into the specified directory. Attempts to
|
||||
# use curl, then wget. If neither is found, fail.
|
||||
# PARAMETERS: $1 = url of file to download
|
||||
# $2 = location to download file into on host system
|
||||
# RETURNS: If curl or wget found, returns the return code of curl or wget
|
||||
# 20 = Could not find curl and wget
|
||||
#-------------------------------------------------------------------------------
|
||||
download_file() {
|
||||
local url
|
||||
local dir
|
||||
local filename
|
||||
local rcode
|
||||
|
||||
url="${1}"
|
||||
dir="${2}"
|
||||
filename="${3}"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "${url}" -o "${dir}/${filename}"
|
||||
rcode="${?}"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget --quiet "${url}" -O "${dir}/${filename}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: checksum_check
|
||||
# DESCRIPTION: Attempt to verify checksum of downloaded file to ensure
|
||||
# integrity. Tries multiple tools before failing.
|
||||
# PARAMETERS: $1 = path to checksum file
|
||||
# $2 = location of file to check
|
||||
# $3 = working directory
|
||||
# RETURNS: 0 = checkusm verified
|
||||
# 1 = checksum verification failed
|
||||
# 20 = failed to determine tool to use to check checksum
|
||||
# 30 = failed to change into or go back from working dir
|
||||
#-------------------------------------------------------------------------------
|
||||
checksum_check() {
|
||||
local checksum_file
|
||||
local file
|
||||
local dir
|
||||
local rcode
|
||||
local shasum_1
|
||||
local shasum_2
|
||||
local shasum_c
|
||||
|
||||
checksum_file="${1}"
|
||||
file="${2}"
|
||||
dir="${3}"
|
||||
|
||||
cd "${dir}" || return 30
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
## Not all sha256sum versions seem to have --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_c="$(sha256sum -c "filtered_checksum.txt")"
|
||||
rcode="${?}"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
## With shasum on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_c="$(shasum -a 256 -c "filtered_checksum.txt")"
|
||||
rcode="${?}"
|
||||
elif command -v sha256 >/dev/null 2>&1; then
|
||||
## With sha256 on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
## Also sha256 -c option seems to fail, so fall back to an if statement
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_1="$(sha256 -q "${file}")"
|
||||
shasum_2="$(awk '{print $1}' filtered_checksum.txt)"
|
||||
if [[ "${shasum_1}" == "${shasum_2}" ]]; then
|
||||
rcode="0"
|
||||
else
|
||||
rcode="1"
|
||||
fi
|
||||
shasum_c="Expected: ${shasum_1}, Got: ${shasum_2}"
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
cd - >/dev/null 2>&1 || return 30
|
||||
|
||||
if [[ "${rcode}" -gt "0" ]]; then
|
||||
echo "${shasum_c}"
|
||||
fi
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: extract_file
|
||||
# DESCRIPTION: Extracts a file into a location. Attempts to determine which
|
||||
# tool to use by checking file extension.
|
||||
# PARAMETERS: $1 = file to extract
|
||||
# $2 = location to extract file into
|
||||
# $3 = extension
|
||||
# RETURNS: Return code of the tool used to extract the file
|
||||
# 20 = Failed to determine which tool to use
|
||||
# 30 = Failed to find tool in path
|
||||
#-------------------------------------------------------------------------------
|
||||
extract_file() {
|
||||
local file
|
||||
local dir
|
||||
local ext
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
dir="${2}"
|
||||
ext="${3}"
|
||||
|
||||
case "${ext}" in
|
||||
"zip" ) if command -v unzip >/dev/null 2>&1; then
|
||||
unzip "${file}" -d "${dir}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="30"
|
||||
fi
|
||||
;;
|
||||
"tar.gz" ) if command -v tar >/dev/null 2>&1; then
|
||||
tar -xf "${file}" -C "${dir}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="31"
|
||||
fi
|
||||
;;
|
||||
* ) rcode="20";;
|
||||
esac
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: create_prefix
|
||||
# DESCRIPTION: Creates the install prefix (and any parent directories). If
|
||||
# EUID not 0, then attempt to use sudo.
|
||||
# PARAMETERS: $1 = prefix
|
||||
# RETURNS: Return code of the tool used to make the directory
|
||||
# 0 = Created the directory
|
||||
# >0 = Failed to create directory
|
||||
# 20 = Could not find mkdir command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
create_prefix() {
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
prefix="${1}"
|
||||
|
||||
if command -v mkdir >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
mkdir -p "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo mkdir -p "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_freebsd
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo.
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 1 = File not installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_freebsd() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -C -b -B '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -C -b -B '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_linux
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo (unless on android).
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 1 = File not installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_linux() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -C -b -S '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
elif [[ "${ANDROID_ROOT}" != "" ]]; then
|
||||
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_cygwin
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo.
|
||||
# Not really 100% sure this is how to install croc in cygwin.
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_cygwin() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -m 755 "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: main
|
||||
# DESCRIPTION: Put it all together in a logical way
|
||||
# ...at least that is the hope...
|
||||
# PARAMETERS: 1 = prefix
|
||||
# RETURNS: 0 = All good
|
||||
# 1 = Something done broke
|
||||
#-------------------------------------------------------------------------------
|
||||
main() {
|
||||
local prefix
|
||||
local tmpdir
|
||||
local tmpdir_rcode
|
||||
local croc_arch
|
||||
local croc_arch_rcode
|
||||
local croc_os
|
||||
local croc_os_rcode
|
||||
local croc_base_url
|
||||
local croc_url
|
||||
local croc_file
|
||||
local croc_checksum_file
|
||||
local croc_bin_name
|
||||
local croc_version
|
||||
local croc_dl_ext
|
||||
local download_file_rcode
|
||||
local download_checksum_file_rcode
|
||||
local checksum_check_rcode
|
||||
local extract_file_rcode
|
||||
local install_file_rcode
|
||||
local create_prefix_rcode
|
||||
local bash_autocomplete_file
|
||||
local bash_autocomplete_prefix
|
||||
local zsh_autocomplete_file
|
||||
local zsh_autocomplete_prefix
|
||||
local autocomplete_install_rcode
|
||||
|
||||
croc_bin_name="croc"
|
||||
croc_version="10.1.1"
|
||||
croc_dl_ext="tar.gz"
|
||||
croc_base_url="https://github.com/schollz/croc/releases/download"
|
||||
prefix="${1}"
|
||||
bash_autocomplete_file="bash_autocomplete"
|
||||
bash_autocomplete_prefix="/etc/bash_completion.d"
|
||||
zsh_autocomplete_file="zsh_autocomplete"
|
||||
zsh_autocomplete_prefix="/etc/zsh"
|
||||
|
||||
print_banner
|
||||
print_message "== Install prefix set to ${prefix}" "info"
|
||||
|
||||
tmpdir="$(make_tempdir "${croc_bin_name}")"
|
||||
tmpdir_rcode="${?}"
|
||||
if [[ "${tmpdir_rcode}" == "0" ]]; then
|
||||
print_message "== Created temp dir at ${tmpdir}" "info"
|
||||
elif [[ "${tmpdir_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to create temp dir at ${tmpdir}" "error"
|
||||
else
|
||||
print_message "== 'mktemp' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
croc_arch="$(determine_arch)"
|
||||
croc_arch_rcode="${?}"
|
||||
if [[ "${croc_arch_rcode}" == "0" ]]; then
|
||||
print_message "== Architecture detected as ${croc_arch}" "info"
|
||||
elif [[ "${croc_arch_rcode}" == "1" ]]; then
|
||||
print_message "== Architecture not detected" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== 'uname' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
croc_os="$(determine_os)"
|
||||
croc_os_rcode="${?}"
|
||||
if [[ "${croc_os_rcode}" == "0" ]]; then
|
||||
print_message "== OS detected as ${croc_os}" "info"
|
||||
elif [[ "${croc_os_rcode}" == "1" ]]; then
|
||||
print_message "== OS not detected" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== 'uname' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${croc_os}" in
|
||||
"Darwin" ) croc_os="macOS";;
|
||||
*"BusyBox"* )
|
||||
croc_os="Linux"
|
||||
;;
|
||||
"CYGWIN"* ) croc_os="Windows";
|
||||
croc_dl_ext="zip";
|
||||
print_message "== Cygwin is currently unsupported." "error";
|
||||
exit 1;;
|
||||
esac
|
||||
|
||||
case "${croc_arch}" in
|
||||
"x86_64" ) croc_arch="64bit";;
|
||||
"amd64" ) croc_arch="64bit";;
|
||||
"aarch64" ) croc_arch="ARM64";;
|
||||
"arm64" ) croc_arch="ARM64";;
|
||||
"armv7l" ) croc_arch="ARM";;
|
||||
"armv8l" ) croc_arch="ARM";;
|
||||
"armv9l" ) croc_arch="ARM";;
|
||||
"i686" ) croc_arch="32bit";;
|
||||
* ) croc_arch="unknown";;
|
||||
esac
|
||||
|
||||
croc_file="${croc_bin_name}_v${croc_version}_${croc_os}-${croc_arch}.${croc_dl_ext}"
|
||||
croc_checksum_file="${croc_bin_name}_v${croc_version}_checksums.txt"
|
||||
croc_url="${croc_base_url}/v${croc_version}/${croc_file}"
|
||||
croc_checksum_url="${croc_base_url}/v${croc_version}/${croc_checksum_file}"
|
||||
echo "${croc_url}" "${tmpdir}" "${croc_file}"
|
||||
download_file "${croc_url}" "${tmpdir}" "${croc_file}"
|
||||
download_file_rcode="${?}"
|
||||
if [[ "${download_file_rcode}" == "0" ]]; then
|
||||
print_message "== Downloaded croc archive into ${tmpdir}" "info"
|
||||
elif [[ "${download_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to download croc archive" "error"
|
||||
exit 1
|
||||
elif [[ "${download_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate curl or wget" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Return code of download tool returned an unexpected value of ${download_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
download_file "${croc_checksum_url}" "${tmpdir}" "${croc_checksum_file}"
|
||||
download_checksum_file_rcode="${?}"
|
||||
if [[ "${download_checksum_file_rcode}" == "0" ]]; then
|
||||
print_message "== Downloaded croc checksums file into ${tmpdir}" "info"
|
||||
elif [[ "${download_checksum_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to download croc checksums" "error"
|
||||
exit 1
|
||||
elif [[ "${download_checksum_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate curl or wget" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Return code of download tool returned an unexpected value of ${download_checksum_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checksum_check "${tmpdir}/${croc_checksum_file}" "${tmpdir}/${croc_file}" "${tmpdir}"
|
||||
checksum_check_rcode="${?}"
|
||||
if [[ "${checksum_check_rcode}" == "0" ]]; then
|
||||
print_message "== Checksum of ${tmpdir}/${croc_file} verified" "ok"
|
||||
elif [[ "${checksum_check_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to verify checksum of ${tmpdir}/${croc_file}" "error"
|
||||
exit 1
|
||||
elif [[ "${checksum_check_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to find tool to verify sha256 sums" "error"
|
||||
exit 1
|
||||
elif [[ "${checksum_check_rcode}" == "30" ]]; then
|
||||
print_message "== Failed to change into working directory ${tmpdir}" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Unknown return code returned while checking checksum of ${tmpdir}/${croc_file}. Returned ${checksum_check_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
extract_file "${tmpdir}/${croc_file}" "${tmpdir}/" "${croc_dl_ext}"
|
||||
extract_file_rcode="${?}"
|
||||
if [[ "${extract_file_rcode}" == "0" ]]; then
|
||||
print_message "== Extracted ${croc_file} to ${tmpdir}/" "info"
|
||||
elif [[ "${extract_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to extract ${croc_file}" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to determine which extraction tool to use" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "30" ]]; then
|
||||
print_message "== Failed to find 'unzip' in path" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "31" ]]; then
|
||||
print_message "== Failed to find 'tar' in path" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Unknown error returned from extraction attempt" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${prefix}" ]]; then
|
||||
create_prefix "${prefix}"
|
||||
create_prefix_rcode="${?}"
|
||||
if [[ "${create_prefix_rcode}" == "0" ]]; then
|
||||
print_message "== Created install prefix at ${prefix}" "info"
|
||||
elif [[ "${create_prefix_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to find mkdir in path" "error"
|
||||
exit 1
|
||||
elif [[ "${create_prefix_rcode}" == "21" ]]; then
|
||||
print_message "== Failed to find sudo in path" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Failed to create the install prefix: ${prefix}" "error"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_message "== Install prefix already exists. No need to create it." "info"
|
||||
fi
|
||||
|
||||
[ ! -d "${bash_autocomplete_prefix}/croc" ] && mkdir -p "${bash_autocomplete_prefix}/croc" >/dev/null 2>&1
|
||||
case "${croc_os}" in
|
||||
"Linux" ) install_file_linux "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"FreeBSD" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"macOS" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"Windows" ) install_file_cygwin "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
esac
|
||||
|
||||
if [[ "${install_file_rcode}" == "0" ]] ; then
|
||||
print_message "== Installed ${croc_bin_name} to ${prefix}/" "ok"
|
||||
elif [[ "${install_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to install ${croc_bin_name}" "error"
|
||||
exit 1
|
||||
elif [[ "${install_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate 'install' command" "error"
|
||||
exit 1
|
||||
elif [[ "${install_file_rcode}" == "21" ]]; then
|
||||
print_message "== Failed to locate 'sudo' command" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Install attempt returned an unexpected value of ${install_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# case "$(basename ${SHELL})" in
|
||||
# "bash" ) install_file_linux "${tmpdir}/${bash_autocomplete_file}" "${bash_autocomplete_prefix}/croc";
|
||||
# autocomplete_install_rcode="${?}";;
|
||||
# "zsh" ) install_file_linux "${tmpdir}/${zsh_autocomplete_file}" "${zsh_autocomplete_prefix}/zsh_autocomplete_croc";
|
||||
# autocomplete_install_rcode="${?}";
|
||||
# print_message "== You will need to add the following to your ~/.zshrc to enable autocompletion" "info";
|
||||
# print_message "\nPROG=croc\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/zsh_autocomplete_croc\n" "info";;
|
||||
# *) autocomplete_install_rcode="1";;
|
||||
# esac
|
||||
|
||||
# if [[ "${autocomplete_install_rcode}" == "0" ]] ; then
|
||||
# print_message "== Installed autocompletions for $(basename "${SHELL}")" "ok"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "1" ]]; then
|
||||
# print_message "== Failed to install ${bash_autocomplete_file}" "error"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "20" ]]; then
|
||||
# print_message "== Failed to locate 'install' command" "error"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "21" ]]; then
|
||||
# print_message "== Failed to locate 'sudo' command" "error"
|
||||
# else
|
||||
# print_message "== Install attempt returned an unexpected value of ${autocomplete_install_rcode}" "error"
|
||||
# fi
|
||||
|
||||
print_message "== Installation complete" "ok"
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# ARGUMENT PARSING
|
||||
#-------------------------------------------------------------------------------
|
||||
OPTS="hp:"
|
||||
while getopts "${OPTS}" optchar; do
|
||||
case "${optchar}" in
|
||||
'h' ) print_help
|
||||
exit 0
|
||||
;;
|
||||
'p' ) INSTALL_PREFIX="${OPTARG}"
|
||||
;;
|
||||
/? ) print_message "Unknown option ${OPTARG}" "warn"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# CALL MAIN
|
||||
#-------------------------------------------------------------------------------
|
||||
main "${INSTALL_PREFIX}"
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
tmp=$(mktemp -d)
|
||||
echo $VERSION
|
||||
git clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-${VERSION}
|
||||
(cd $tmp/croc-${VERSION} && go mod tidy && go mod vendor)
|
||||
(cd $tmp && tar -cvzf croc_${VERSION}_src.tar.gz croc-${VERSION})
|
||||
mv $tmp/croc_${VERSION}_src.tar.gz dist/
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() (err error) {
|
||||
versionNew := "v" + os.Getenv("VERSION")
|
||||
versionHash, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
versionHashNew := strings.TrimSpace(string(versionHash))
|
||||
fmt.Println(versionNew)
|
||||
fmt.Println(versionHashNew)
|
||||
|
||||
err = replaceInFile("src/cli/cli.go", `Version = "`, `"`, versionNew+"-"+versionHashNew)
|
||||
if err == nil {
|
||||
fmt.Printf("updated cli.go to version %s\n", versionNew)
|
||||
}
|
||||
err = replaceInFile("README.md", `version-`, `-b`, strings.Split(versionNew, "-")[0])
|
||||
if err == nil {
|
||||
fmt.Printf("updated README to version %s\n", strings.Split(versionNew, "-")[0])
|
||||
}
|
||||
|
||||
err = replaceInFile("src/install/default.txt", `croc_version="`, `"`, strings.Split(versionNew, "-")[0][1:])
|
||||
if err == nil {
|
||||
fmt.Printf("updated default.txt to version %s\n", strings.Split(versionNew, "-")[0][1:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func replaceInFile(fname, start, end, replacement string) (err error) {
|
||||
b, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldVersion := GetStringInBetween(string(b), start, end)
|
||||
if oldVersion == "" {
|
||||
err = fmt.Errorf("nothing")
|
||||
return
|
||||
}
|
||||
newF := strings.Replace(
|
||||
string(b),
|
||||
fmt.Sprintf("%s%s%s", start, oldVersion, end),
|
||||
fmt.Sprintf("%s%s%s", start, replacement, end),
|
||||
1,
|
||||
)
|
||||
err = os.WriteFile(fname, []byte(newF), 0o644)
|
||||
return
|
||||
}
|
||||
|
||||
// GetStringInBetween Returns empty string if no start string found
|
||||
func GetStringInBetween(str string, start string, end string) (result string) {
|
||||
s := strings.Index(str, start)
|
||||
if s == -1 {
|
||||
return
|
||||
}
|
||||
s += len(start)
|
||||
e := strings.Index(str[s:], end)
|
||||
if e == -1 {
|
||||
return
|
||||
}
|
||||
e += s
|
||||
return str[s:e]
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#!/bin/bash
|
||||
VERSION=$(cat ./src/cli/cli.go | grep 'Version = "v' | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||
echo $VERSION
|
||||
|
||||
# Check dependencies.
|
||||
set -e
|
||||
xargs=$(which gxargs || which xargs)
|
||||
|
||||
# Validate settings.
|
||||
[ "$TRACE" ] && set -x
|
||||
|
||||
CONFIG=$@
|
||||
|
||||
for line in $CONFIG; do
|
||||
eval "$line"
|
||||
done
|
||||
|
||||
owner="schollz"
|
||||
repo="croc"
|
||||
tag="v${VERSION}"
|
||||
filename="dist/croc_${VERSION}_src.tar.gz"
|
||||
|
||||
# Define variables.
|
||||
GH_API="https://api.github.com"
|
||||
GH_REPO="$GH_API/repos/$owner/$repo"
|
||||
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
||||
AUTH="Authorization: token $GITHUB_TOKEN"
|
||||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
||||
CURL_ARGS="-LJO#"
|
||||
|
||||
if [[ "$tag" == 'LATEST' ]]; then
|
||||
GH_TAGS="$GH_REPO/releases/latest"
|
||||
fi
|
||||
|
||||
# Validate token.
|
||||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
||||
|
||||
# Read asset tags.
|
||||
response=$(curl -sH "$AUTH" $GH_TAGS)
|
||||
|
||||
# Get ID of the asset based on given filename.
|
||||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
||||
|
||||
# Upload asset
|
||||
echo "Uploading asset... "
|
||||
|
||||
# Construct url
|
||||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
||||
|
||||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" $GH_ASSET
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#compdef $PROG
|
||||
|
||||
_cli_zsh_autocomplete() {
|
||||
|
||||
local -a opts
|
||||
local cur
|
||||
cur=${words[-1]}
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
|
||||
else
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
|
||||
fi
|
||||
|
||||
if [[ "${opts[1]}" != "" ]]; then
|
||||
_describe 'values' opts
|
||||
else
|
||||
_files
|
||||
fi
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
compdef _cli_zsh_autocomplete $PROG
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/compress"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
log "github.com/schollz/logger"
|
||||
)
|
||||
|
||||
// Type is a message type
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypePAKE Type = "pake"
|
||||
TypeExternalIP Type = "externalip"
|
||||
TypeFinished Type = "finished"
|
||||
TypeError Type = "error"
|
||||
TypeCloseRecipient Type = "close-recipient"
|
||||
TypeCloseSender Type = "close-sender"
|
||||
TypeRecipientReady Type = "recipientready"
|
||||
TypeFileInfo Type = "fileinfo"
|
||||
)
|
||||
|
||||
// Message is the possible payload for messaging
|
||||
type Message struct {
|
||||
Type Type `json:"t,omitempty"`
|
||||
Message string `json:"m,omitempty"`
|
||||
Bytes []byte `json:"b,omitempty"`
|
||||
Bytes2 []byte `json:"b2,omitempty"`
|
||||
Num int `json:"n,omitempty"`
|
||||
}
|
||||
|
||||
func (m Message) String() string {
|
||||
b, _ := json.Marshal(m)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Send will send out
|
||||
func Send(c *comm.Comm, key []byte, m Message) (err error) {
|
||||
mSend, err := Encode(key, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(mSend)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode will convert to bytes
|
||||
func Encode(key []byte, m Message) (b []byte, err error) {
|
||||
b, err = json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = compress.Compress(b)
|
||||
if key != nil {
|
||||
log.Debugf("writing %s message (encrypted)", m.Type)
|
||||
b, err = crypt.Encrypt(b, key)
|
||||
} else {
|
||||
log.Debugf("writing %s message (unencrypted)", m.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Decode will convert from bytes
|
||||
func Decode(key []byte, b []byte) (m Message, err error) {
|
||||
if key != nil {
|
||||
b, err = crypt.Decrypt(b, key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
b = compress.Decompress(b)
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err == nil {
|
||||
if key != nil {
|
||||
log.Debugf("read %s message (encrypted)", m.Type)
|
||||
} else {
|
||||
log.Debugf("read %s message (unencrypted)", m.Type)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var TypeMessage Type = "message"
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
log.SetLevel("debug")
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
e, salt, err := crypt.New([]byte("pass"), nil)
|
||||
assert.Nil(t, err)
|
||||
fmt.Println(string(salt))
|
||||
b, err := Encode(e, m)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("%x\n", b)
|
||||
|
||||
m2, err := Decode(e, b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, m, m2)
|
||||
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
|
||||
_, err = Decode([]byte("not pass"), b)
|
||||
assert.NotNil(t, err)
|
||||
_, err = Encode([]byte("0"), m)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestMessageNoPass(t *testing.T) {
|
||||
log.SetLevel("debug")
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
b, err := Encode(nil, m)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("%x\n", b)
|
||||
|
||||
m2, err := Decode(nil, b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, m, m2)
|
||||
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
|
||||
}
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
token := make([]byte, 40000000)
|
||||
rand.Read(token)
|
||||
|
||||
port := "8801"
|
||||
go func() {
|
||||
log.Debugf("starting TCP server on " + port)
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(_ string, connection net.Conn) {
|
||||
c := comm.New(connection)
|
||||
err = c.Send([]byte("hello, world"))
|
||||
assert.Nil(t, err)
|
||||
data, err := c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, computer"), data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte{'\x00'}, data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token, data)
|
||||
}(port, connection)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
a, err := comm.NewConnection("127.0.0.1:"+port, 10*time.Minute)
|
||||
assert.Nil(t, err)
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
e, salt, err := crypt.New([]byte("pass"), nil)
|
||||
log.Debug(salt)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Nil(t, Send(a, e, m))
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
// From GitHub version/fork maintained by Stephen Paul Weber available at:
|
||||
// https://github.com/singpolyma/mnemonicode
|
||||
//
|
||||
// Originally from:
|
||||
// http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/
|
||||
|
||||
/*
|
||||
Copyright (c) 2000 Oren Tirosh <oren@hishome.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package mnemonicode
|
||||
|
||||
const base = 1626
|
||||
|
||||
// WordsRequired returns the number of words required to encode input
|
||||
// data of length bytes using mnomonic encoding.
|
||||
//
|
||||
// Every four bytes of input is encoded into three words. If there
|
||||
// is an extra one or two bytes they get an extra one or two words
|
||||
// respectively. If there is an extra three bytes, they will be encoded
|
||||
// into three words with the last word being one of a small set of very
|
||||
// short words (only needed to encode the last 3 bits).
|
||||
func WordsRequired(length int) int {
|
||||
return ((length + 1) * 3) / 4
|
||||
}
|
||||
|
||||
// EncodeWordList encodes src into mnemomic words which are appended to dst.
|
||||
// The final wordlist is returned.
|
||||
// There will be WordsRequired(len(src)) words appeneded.
|
||||
func EncodeWordList(dst []string, src []byte) (result []string) {
|
||||
if n := len(dst) + WordsRequired(len(src)); cap(dst) < n {
|
||||
result = make([]string, len(dst), n)
|
||||
copy(result, dst)
|
||||
} else {
|
||||
result = dst
|
||||
}
|
||||
|
||||
var x uint32
|
||||
for len(src) >= 4 {
|
||||
x = uint32(src[0])
|
||||
x |= uint32(src[1]) << 8
|
||||
x |= uint32(src[2]) << 16
|
||||
x |= uint32(src[3]) << 24
|
||||
src = src[4:]
|
||||
|
||||
i0 := int(x % base)
|
||||
i1 := int(x/base) % base
|
||||
i2 := int(x/base/base) % base
|
||||
result = append(result, WordList[i0], WordList[i1], WordList[i2])
|
||||
}
|
||||
if len(src) > 0 {
|
||||
x = 0
|
||||
for i := len(src) - 1; i >= 0; i-- {
|
||||
x <<= 8
|
||||
x |= uint32(src[i])
|
||||
}
|
||||
i := int(x % base)
|
||||
result = append(result, WordList[i])
|
||||
if len(src) >= 2 {
|
||||
i = int(x/base) % base
|
||||
result = append(result, WordList[i])
|
||||
}
|
||||
if len(src) == 3 {
|
||||
i = base + int(x/base/base)%7
|
||||
result = append(result, WordList[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
log "github.com/schollz/logger"
|
||||
)
|
||||
|
||||
// TCP_BUFFER_SIZE is the maximum packet size
|
||||
const TCP_BUFFER_SIZE = 1024 * 64
|
||||
|
||||
// DEFAULT_RELAY is the default relay used (can be set using --relay)
|
||||
var (
|
||||
DEFAULT_RELAY = "croc.schollz.com"
|
||||
DEFAULT_RELAY6 = "croc6.schollz.com"
|
||||
DEFAULT_PORT = "9009"
|
||||
DEFAULT_PASSPHRASE = "pass123"
|
||||
INTERNAL_DNS = false
|
||||
)
|
||||
|
||||
// publicDNS are servers to be queried if a local lookup fails
|
||||
var publicDNS = []string{
|
||||
"1.0.0.1", // Cloudflare
|
||||
"1.1.1.1", // Cloudflare
|
||||
"[2606:4700:4700::1111]", // Cloudflare
|
||||
"[2606:4700:4700::1001]", // Cloudflare
|
||||
"8.8.4.4", // Google
|
||||
"8.8.8.8", // Google
|
||||
"[2001:4860:4860::8844]", // Google
|
||||
"[2001:4860:4860::8888]", // Google
|
||||
"9.9.9.9", // Quad9
|
||||
"149.112.112.112", // Quad9
|
||||
"[2620:fe::fe]", // Quad9
|
||||
"[2620:fe::fe:9]", // Quad9
|
||||
"8.26.56.26", // Comodo
|
||||
"8.20.247.20", // Comodo
|
||||
"208.67.220.220", // Cisco OpenDNS
|
||||
"208.67.222.222", // Cisco OpenDNS
|
||||
"[2620:119:35::35]", // Cisco OpenDNS
|
||||
"[2620:119:53::53]", // Cisco OpenDNS
|
||||
}
|
||||
|
||||
func getConfigFile(requireValidPath bool) (fname string, err error) {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fname = path.Join(configFile, "internal-dns")
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.SetLevel("info")
|
||||
log.SetOutput(os.Stderr)
|
||||
doRemember := false
|
||||
for _, flag := range os.Args {
|
||||
if flag == "--internal-dns" {
|
||||
INTERNAL_DNS = true
|
||||
break
|
||||
}
|
||||
if flag == "--remember" {
|
||||
doRemember = true
|
||||
}
|
||||
}
|
||||
if doRemember {
|
||||
// save in config file
|
||||
fname, err := getConfigFile(true)
|
||||
if err == nil {
|
||||
f, _ := os.Create(fname)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
if !INTERNAL_DNS {
|
||||
fname, err := getConfigFile(false)
|
||||
if err == nil {
|
||||
INTERNAL_DNS = utils.Exists(fname)
|
||||
}
|
||||
}
|
||||
log.Trace("Using internal DNS: ", INTERNAL_DNS)
|
||||
var err error
|
||||
var addr string
|
||||
addr, err = lookup(DEFAULT_RELAY)
|
||||
if err == nil {
|
||||
DEFAULT_RELAY = net.JoinHostPort(addr, DEFAULT_PORT)
|
||||
} else {
|
||||
DEFAULT_RELAY = ""
|
||||
}
|
||||
log.Tracef("Default ipv4 relay: %s", addr)
|
||||
addr, err = lookup(DEFAULT_RELAY6)
|
||||
if err == nil {
|
||||
DEFAULT_RELAY6 = net.JoinHostPort(addr, DEFAULT_PORT)
|
||||
} else {
|
||||
DEFAULT_RELAY6 = ""
|
||||
}
|
||||
log.Tracef("Default ipv6 relay: %s", addr)
|
||||
}
|
||||
|
||||
// Resolve a hostname to an IP address using DNS.
|
||||
func lookup(address string) (ipaddress string, err error) {
|
||||
if !INTERNAL_DNS {
|
||||
log.Tracef("Using local DNS to resolve %s", address)
|
||||
return localLookupIP(address)
|
||||
}
|
||||
type Result struct {
|
||||
s string
|
||||
err error
|
||||
}
|
||||
result := make(chan Result, len(publicDNS))
|
||||
for _, dns := range publicDNS {
|
||||
go func(dns string) {
|
||||
var r Result
|
||||
r.s, r.err = remoteLookupIP(address, dns)
|
||||
log.Tracef("Resolved %s to %s using %s", address, r.s, dns)
|
||||
result <- r
|
||||
}(dns)
|
||||
}
|
||||
for i := 0; i < len(publicDNS); i++ {
|
||||
ipaddress = (<-result).s
|
||||
log.Tracef("Resolved %s to %s", address, ipaddress)
|
||||
if ipaddress != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("failed to resolve %s: all DNS servers exhausted", address)
|
||||
return
|
||||
}
|
||||
|
||||
// localLookupIP returns a host's IP address using the local DNS configuration.
|
||||
func localLookupIP(address string) (ipaddress string, err error) {
|
||||
// Create a context with a 500 millisecond timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
// Use the context with timeout in the LookupHost function
|
||||
ip, err := r.LookupHost(ctx, address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ipaddress = ip[0]
|
||||
return
|
||||
}
|
||||
|
||||
// remoteLookupIP returns a host's IP address based on a remote DNS server.
|
||||
func remoteLookupIP(address, dns string) (ipaddress string, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
d := new(net.Dialer)
|
||||
return d.DialContext(ctx, network, dns+":53")
|
||||
},
|
||||
}
|
||||
ip, err := r.LookupHost(ctx, address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ipaddress = ip[0]
|
||||
return
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
|
|
@ -1,9 +0,0 @@
|
|||
package tcp
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
DEFAULT_LOG_LEVEL = "debug"
|
||||
DEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute
|
||||
DEFAULT_ROOM_TTL = 3 * time.Hour
|
||||
)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: maybe export from logger library?
|
||||
var availableLogLevels = []string{"info", "error", "warn", "debug", "trace"}
|
||||
|
||||
type serverOptsFunc func(s *server) error
|
||||
|
||||
func WithBanner(banner ...string) serverOptsFunc {
|
||||
return func(s *server) error {
|
||||
if len(banner) > 0 {
|
||||
s.banner = banner[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogLevel(level string) serverOptsFunc {
|
||||
return func(s *server) error {
|
||||
if !containsSlice(availableLogLevels, level) {
|
||||
return fmt.Errorf("invalid log level specified: %s", level)
|
||||
}
|
||||
s.debugLevel = level
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRoomCleanupInterval(interval time.Duration) serverOptsFunc {
|
||||
return func(s *server) error {
|
||||
s.roomCleanupInterval = interval
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRoomTTL(ttl time.Duration) serverOptsFunc {
|
||||
return func(s *server) error {
|
||||
s.roomTTL = ttl
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func containsSlice(s []string, e string) bool {
|
||||
for _, ss := range s {
|
||||
if e == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
595
src/tcp/tcp.go
595
src/tcp/tcp.go
|
|
@ -1,595 +0,0 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/schollz/pake/v3"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
"github.com/schollz/croc/v10/src/models"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
host string
|
||||
port string
|
||||
debugLevel string
|
||||
banner string
|
||||
password string
|
||||
rooms roomMap
|
||||
|
||||
roomCleanupInterval time.Duration
|
||||
roomTTL time.Duration
|
||||
|
||||
stopRoomCleanup chan struct{}
|
||||
}
|
||||
|
||||
type roomInfo struct {
|
||||
first *comm.Comm
|
||||
second *comm.Comm
|
||||
opened time.Time
|
||||
full bool
|
||||
}
|
||||
|
||||
type roomMap struct {
|
||||
rooms map[string]roomInfo
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
const pingRoom = "pinglkasjdlfjsaldjf"
|
||||
|
||||
// newDefaultServer initializes a new server, with some default configuration options
|
||||
func newDefaultServer() *server {
|
||||
s := new(server)
|
||||
s.roomCleanupInterval = DEFAULT_ROOM_CLEANUP_INTERVAL
|
||||
s.roomTTL = DEFAULT_ROOM_TTL
|
||||
s.debugLevel = DEFAULT_LOG_LEVEL
|
||||
s.stopRoomCleanup = make(chan struct{})
|
||||
return s
|
||||
}
|
||||
|
||||
// RunWithOptionsAsync asynchronously starts a TCP listener.
|
||||
func RunWithOptionsAsync(host, port, password string, opts ...serverOptsFunc) error {
|
||||
s := newDefaultServer()
|
||||
s.host = host
|
||||
s.port = port
|
||||
s.password = password
|
||||
for _, opt := range opts {
|
||||
err := opt(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not apply optional configurations: %w", err)
|
||||
}
|
||||
}
|
||||
return s.start()
|
||||
}
|
||||
|
||||
// Run starts a tcp listener, run async
|
||||
func Run(debugLevel, host, port, password string, banner ...string) (err error) {
|
||||
return RunWithOptionsAsync(host, port, password, WithBanner(banner...), WithLogLevel(debugLevel))
|
||||
}
|
||||
|
||||
func (s *server) start() (err error) {
|
||||
log.SetLevel(s.debugLevel)
|
||||
|
||||
// Mask our password in logs
|
||||
maskedPassword := ""
|
||||
if len(s.password) > 2 {
|
||||
maskedPassword = fmt.Sprintf("%c***%c", s.password[0], s.password[len(s.password)-1])
|
||||
} else {
|
||||
maskedPassword = s.password
|
||||
}
|
||||
|
||||
log.Debugf("starting with password '%s'", maskedPassword)
|
||||
|
||||
s.rooms.Lock()
|
||||
s.rooms.rooms = make(map[string]roomInfo)
|
||||
s.rooms.Unlock()
|
||||
|
||||
go s.deleteOldRooms()
|
||||
defer s.stopRoomDeletion()
|
||||
|
||||
err = s.run()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) run() (err error) {
|
||||
network := "tcp"
|
||||
addr := net.JoinHostPort(s.host, s.port)
|
||||
if s.host != "" {
|
||||
ip := net.ParseIP(s.host)
|
||||
if ip == nil {
|
||||
var tcpIP *net.IPAddr
|
||||
tcpIP, err = net.ResolveIPAddr("ip", s.host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip = tcpIP.IP
|
||||
}
|
||||
addr = net.JoinHostPort(ip.String(), s.port)
|
||||
if s.host != "" {
|
||||
if ip.To4() != nil {
|
||||
network = "tcp4"
|
||||
} else {
|
||||
network = "tcp6"
|
||||
}
|
||||
}
|
||||
}
|
||||
addr = strings.Replace(addr, "127.0.0.1", "0.0.0.0", 1)
|
||||
log.Infof("starting TCP server on " + addr)
|
||||
server, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listening on %s: %w", addr, err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem accepting connection: %w", err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(port string, connection net.Conn) {
|
||||
c := comm.New(connection)
|
||||
room, errCommunication := s.clientCommunication(port, c)
|
||||
log.Debugf("room: %+v", room)
|
||||
log.Debugf("err: %+v", errCommunication)
|
||||
if errCommunication != nil {
|
||||
log.Debugf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
|
||||
connection.Close()
|
||||
return
|
||||
}
|
||||
if room == pingRoom {
|
||||
log.Debugf("got ping")
|
||||
connection.Close()
|
||||
return
|
||||
}
|
||||
for {
|
||||
// check connection
|
||||
log.Debugf("checking connection of room %s for %+v", room, c)
|
||||
deleteIt := false
|
||||
s.rooms.Lock()
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
log.Debug("room is gone")
|
||||
s.rooms.Unlock()
|
||||
return
|
||||
}
|
||||
log.Debugf("room: %+v", s.rooms.rooms[room])
|
||||
if s.rooms.rooms[room].first != nil && s.rooms.rooms[room].second != nil {
|
||||
log.Debug("rooms ready")
|
||||
s.rooms.Unlock()
|
||||
break
|
||||
} else {
|
||||
if s.rooms.rooms[room].first != nil {
|
||||
errSend := s.rooms.rooms[room].first.Send([]byte{1})
|
||||
if errSend != nil {
|
||||
log.Debug(errSend)
|
||||
deleteIt = true
|
||||
}
|
||||
}
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
if deleteIt {
|
||||
s.deleteRoom(room)
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}(s.port, connection)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteOldRooms checks for rooms at a regular interval and removes those that
|
||||
// have exceeded their allocated TTL.
|
||||
func (s *server) deleteOldRooms() {
|
||||
ticker := time.NewTicker(s.roomCleanupInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
var roomsToDelete []string
|
||||
s.rooms.Lock()
|
||||
for room := range s.rooms.rooms {
|
||||
if time.Since(s.rooms.rooms[room].opened) > s.roomTTL {
|
||||
roomsToDelete = append(roomsToDelete, room)
|
||||
}
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
|
||||
for _, room := range roomsToDelete {
|
||||
s.deleteRoom(room)
|
||||
log.Debugf("room cleaned up: %s", room)
|
||||
}
|
||||
case <-s.stopRoomCleanup:
|
||||
ticker.Stop()
|
||||
log.Debug("room cleanup stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) stopRoomDeletion() {
|
||||
log.Debug("stop room cleanup fired")
|
||||
s.stopRoomCleanup <- struct{}{}
|
||||
}
|
||||
|
||||
var weakKey = []byte{1, 2, 3}
|
||||
|
||||
func (s *server) clientCommunication(port string, c *comm.Comm) (room string, err error) {
|
||||
// establish secure password with PAKE for communication with relay
|
||||
B, err := pake.InitCurve(weakKey, 1, "siec")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Abytes, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("Abytes: %s", Abytes)
|
||||
if bytes.Equal(Abytes, []byte("ping")) {
|
||||
room = pingRoom
|
||||
log.Debug("sending back pong")
|
||||
c.Send([]byte("pong"))
|
||||
return
|
||||
}
|
||||
err = B.Update(Abytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(B.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strongKey, err := B.SessionKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("strongkey: %x", strongKey)
|
||||
|
||||
// receive salt
|
||||
salt, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strongKeyForEncryption, _, err := crypt.New(strongKey, salt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("waiting for password")
|
||||
passwordBytesEnc, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
passwordBytes, err := crypt.Decrypt(passwordBytesEnc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(string(passwordBytes)) != s.password {
|
||||
err = fmt.Errorf("bad password")
|
||||
enc, _ := crypt.Encrypt([]byte(err.Error()), strongKeyForEncryption)
|
||||
if err = c.Send(enc); err != nil {
|
||||
return "", fmt.Errorf("send error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// send ok to tell client they are connected
|
||||
banner := s.banner
|
||||
if len(banner) == 0 {
|
||||
banner = "ok"
|
||||
}
|
||||
log.Debugf("sending '%s'", banner)
|
||||
bSend, err := crypt.Encrypt([]byte(banner+"|||"+c.Connection().RemoteAddr().String()), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// wait for client to tell me which room they want
|
||||
log.Debug("waiting for answer")
|
||||
enc, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
roomBytes, err := crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
room = string(roomBytes)
|
||||
|
||||
s.rooms.Lock()
|
||||
// create the room if it is new
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
s.rooms.rooms[room] = roomInfo{
|
||||
first: c,
|
||||
opened: time.Now(),
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
// tell the client that they got the room
|
||||
|
||||
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
log.Debugf("room %s has 1", room)
|
||||
return
|
||||
}
|
||||
if s.rooms.rooms[room].full {
|
||||
s.rooms.Unlock()
|
||||
bSend, err = crypt.Encrypt([]byte("room full"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Debugf("room %s has 2", room)
|
||||
s.rooms.rooms[room] = roomInfo{
|
||||
first: s.rooms.rooms[room].first,
|
||||
second: c,
|
||||
opened: s.rooms.rooms[room].opened,
|
||||
full: true,
|
||||
}
|
||||
otherConnection := s.rooms.rooms[room].first
|
||||
s.rooms.Unlock()
|
||||
|
||||
// second connection is the sender, time to staple connections
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// start piping
|
||||
go func(com1, com2 *comm.Comm, wg *sync.WaitGroup) {
|
||||
log.Debug("starting pipes")
|
||||
pipe(com1.Connection(), com2.Connection())
|
||||
wg.Done()
|
||||
log.Debug("done piping")
|
||||
}(otherConnection, c, &wg)
|
||||
|
||||
// tell the sender everything is ready
|
||||
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// delete room
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) deleteRoom(room string) {
|
||||
s.rooms.Lock()
|
||||
defer s.rooms.Unlock()
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
return
|
||||
}
|
||||
log.Debugf("deleting room: %s", room)
|
||||
if s.rooms.rooms[room].first != nil {
|
||||
s.rooms.rooms[room].first.Close()
|
||||
}
|
||||
if s.rooms.rooms[room].second != nil {
|
||||
s.rooms.rooms[room].second.Close()
|
||||
}
|
||||
s.rooms.rooms[room] = roomInfo{first: nil, second: nil}
|
||||
delete(s.rooms.rooms, room)
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
//
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte, 1)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Warnf("can't set read deadline: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
b := make([]byte, models.TCP_BUFFER_SIZE)
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Debug("exiting")
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// pipe creates a full-duplex pipe between the two sockets and
|
||||
// transfers data from one to the other.
|
||||
func pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
}
|
||||
if _, err := conn2.Write(b1); err != nil {
|
||||
log.Errorf("write error on channel 1: %v", err)
|
||||
}
|
||||
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
}
|
||||
if _, err := conn1.Write(b2); err != nil {
|
||||
log.Errorf("write error on channel 2: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PingServer(address string) (err error) {
|
||||
log.Debugf("pinging %s", address)
|
||||
c, err := comm.NewConnection(address, 300*time.Millisecond)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send([]byte("ping"))
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
b, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(b, []byte("pong")) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no pong")
|
||||
}
|
||||
|
||||
// ConnectToTCPServer will initiate a new connection
|
||||
// to the specified address, room with optional time limit
|
||||
func ConnectToTCPServer(address, password, room string, timelimit ...time.Duration) (c *comm.Comm, banner string, ipaddr string, err error) {
|
||||
if len(timelimit) > 0 {
|
||||
c, err = comm.NewConnection(address, timelimit[0])
|
||||
} else {
|
||||
c, err = comm.NewConnection(address)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
// get PAKE connection with server to establish strong key to transfer info
|
||||
A, err := pake.InitCurve(weakKey, 0, "siec")
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(A.Bytes())
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
Bbytes, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = A.Update(Bbytes)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
strongKey, err := A.SessionKey()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("strong key: %x", strongKey)
|
||||
|
||||
strongKeyForEncryption, salt, err := crypt.New(strongKey, nil)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
// send salt
|
||||
err = c.Send(salt)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sending password")
|
||||
bSend, err := crypt.Encrypt([]byte(password), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("waiting for first ok")
|
||||
enc, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
data, err := crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(string(data), "|||") {
|
||||
err = fmt.Errorf("bad response: %s", string(data))
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
banner = strings.Split(string(data), "|||")[0]
|
||||
ipaddr = strings.Split(string(data), "|||")[1]
|
||||
log.Debugf("sending room; %s", room)
|
||||
bSend, err = crypt.Encrypt([]byte(room), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("waiting for room confirmation")
|
||||
enc, err = c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
data, err = crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(data, []byte("ok")) {
|
||||
err = fmt.Errorf("got bad response: %s", data)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("all set")
|
||||
return
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkConnection(b *testing.B) {
|
||||
log.SetLevel("trace")
|
||||
go Run("debug", "127.0.0.1", "8283", "pass123", "8284")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c, _, _, _ := ConnectToTCPServer("127.0.0.1:8283", "pass123", fmt.Sprintf("testroom%d", i), 1*time.Minute)
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCP(t *testing.T) {
|
||||
log.SetLevel("error")
|
||||
timeToRoomDeletion := 100 * time.Millisecond
|
||||
go RunWithOptionsAsync("127.0.0.1", "8381", "pass123", WithBanner("8382"), WithLogLevel("debug"), WithRoomTTL(timeToRoomDeletion))
|
||||
time.Sleep(timeToRoomDeletion)
|
||||
err := PingServer("127.0.0.1:8381")
|
||||
assert.Nil(t, err)
|
||||
err = PingServer("127.0.0.1:8333")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
time.Sleep(timeToRoomDeletion)
|
||||
c1, banner, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Minute)
|
||||
assert.Equal(t, banner, "8382")
|
||||
assert.Nil(t, err)
|
||||
c2, _, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
|
||||
assert.Nil(t, err)
|
||||
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
|
||||
assert.NotNil(t, err)
|
||||
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Nanosecond)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// try sending data
|
||||
assert.Nil(t, c1.Send([]byte("hello, c2")))
|
||||
var data []byte
|
||||
for {
|
||||
data, err = c2.Receive()
|
||||
if bytes.Equal(data, []byte{1}) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, c2"), data)
|
||||
|
||||
assert.Nil(t, c2.Send([]byte("hello, c1")))
|
||||
for {
|
||||
data, err = c1.Receive()
|
||||
if bytes.Equal(data, []byte{1}) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, c1"), data)
|
||||
|
||||
c1.Close()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
|
@ -1,637 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
"github.com/kalafut/imohash"
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/schollz/croc/v10/src/mnemonicode"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
const NbPinNumbers = 4
|
||||
const NbBytesWords = 4
|
||||
|
||||
// Get or create home directory
|
||||
func GetConfigDir(requireValidPath bool) (homedir string, err error) {
|
||||
if envHomedir, isSet := os.LookupEnv("CROC_CONFIG_DIR"); isSet {
|
||||
homedir = envHomedir
|
||||
} else if xdgConfigHome, isSet := os.LookupEnv("XDG_CONFIG_HOME"); isSet {
|
||||
homedir = path.Join(xdgConfigHome, "croc")
|
||||
} else {
|
||||
homedir, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
if !requireValidPath {
|
||||
err = nil
|
||||
homedir = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
homedir = path.Join(homedir, ".config", "croc")
|
||||
}
|
||||
|
||||
if requireValidPath {
|
||||
if _, err = os.Stat(homedir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(homedir, 0o700)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Exists reports whether the named file or directory exists.
|
||||
func Exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetInput returns the input with a given prompt
|
||||
func GetInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Fprintf(os.Stderr, "%s", prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// HashFile returns the hash of a file or, in case of a symlink, the
|
||||
// SHA256 hash of its target. Takes an argument to specify the algorithm to use.
|
||||
func HashFile(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {
|
||||
doShowProgress := false
|
||||
if len(showProgress) > 0 {
|
||||
doShowProgress = showProgress[0]
|
||||
}
|
||||
var fstats os.FileInfo
|
||||
fstats, err = os.Lstat(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fstats.Mode()&os.ModeSymlink != 0 {
|
||||
var target string
|
||||
target, err = os.Readlink(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(SHA256(target)), nil
|
||||
}
|
||||
switch algorithm {
|
||||
case "imohash":
|
||||
return IMOHashFile(fname)
|
||||
case "md5":
|
||||
return MD5HashFile(fname, doShowProgress)
|
||||
case "xxhash":
|
||||
return XXHashFile(fname, doShowProgress)
|
||||
case "highway":
|
||||
return HighwayHashFile(fname, doShowProgress)
|
||||
}
|
||||
err = fmt.Errorf("unspecified algorithm")
|
||||
return
|
||||
}
|
||||
|
||||
// HighwayHashFile returns highwayhash of a file
|
||||
func HighwayHashFile(fname string, doShowProgress bool) (hashHighway []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
key, err := hex.DecodeString("1553c5383fb0b86578c3310da665b4f6e0521acf22eb58a99532ffed02a6b115")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h, err := highwayhash.New(key)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not create highwayhash: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hashHighway = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// MD5HashFile returns MD5 hash
|
||||
func MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hash256 = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
var imofull = imohash.NewCustom(0, 0)
|
||||
var imopartial = imohash.NewCustom(16*16*8*1024, 128*1024)
|
||||
|
||||
// IMOHashFile returns imohash
|
||||
func IMOHashFile(fname string) (hash []byte, err error) {
|
||||
b, err := imopartial.SumFile(fname)
|
||||
hash = b[:]
|
||||
return
|
||||
}
|
||||
|
||||
// IMOHashFileFull returns imohash of full file
|
||||
func IMOHashFileFull(fname string) (hash []byte, err error) {
|
||||
b, err := imofull.SumFile(fname)
|
||||
hash = b[:]
|
||||
return
|
||||
}
|
||||
|
||||
// XXHashFile returns the xxhash of a file
|
||||
func XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := xxhash.New()
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hash256 = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// SHA256 returns sha256 sum
|
||||
func SHA256(s string) string {
|
||||
sha := sha256.New()
|
||||
sha.Write([]byte(s))
|
||||
return hex.EncodeToString(sha.Sum(nil))
|
||||
}
|
||||
|
||||
// PublicIP returns public ip address
|
||||
func PublicIP() (ip string, err error) {
|
||||
// ask ipv4.icanhazip.com for the public ip
|
||||
// by making http request
|
||||
// if the request fails, return nothing
|
||||
resp, err := http.Get("http://ipv4.icanhazip.com")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// read the body of the response
|
||||
// and return the ip address
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
ip = strings.TrimSpace(buf.String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// LocalIP returns local ip address
|
||||
func LocalIP() string {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
return localAddr.IP.String()
|
||||
}
|
||||
|
||||
func GenerateRandomPin() string {
|
||||
s := ""
|
||||
max := new(big.Int)
|
||||
max.SetInt64(9)
|
||||
for i := 0; i < NbPinNumbers; i++ {
|
||||
v, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s += fmt.Sprintf("%d", v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetRandomName returns mnemonicoded random name
|
||||
func GetRandomName() string {
|
||||
var result []string
|
||||
bs := make([]byte, NbBytesWords)
|
||||
rand.Read(bs)
|
||||
result = mnemonicode.EncodeWordList(result, bs)
|
||||
return GenerateRandomPin() + "-" + strings.Join(result, "-")
|
||||
}
|
||||
|
||||
// ByteCountDecimal converts bytes to human readable byte string
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// MissingChunks returns the positions of missing chunks.
|
||||
// If file doesn't exist, it returns an empty chunk list (all chunks).
|
||||
// If the file size is not the same as requested, it returns an empty chunk list (all chunks).
|
||||
func MissingChunks(fname string, fsize int64, chunkSize int) (chunkRanges []int64) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fstat, err := os.Stat(fname)
|
||||
if err != nil || fstat.Size() != fsize {
|
||||
return
|
||||
}
|
||||
|
||||
emptyBuffer := make([]byte, chunkSize)
|
||||
chunkNum := 0
|
||||
chunks := make([]int64, int64(math.Ceil(float64(fsize)/float64(chunkSize))))
|
||||
var currentLocation int64
|
||||
for {
|
||||
buffer := make([]byte, chunkSize)
|
||||
bytesread, err := f.Read(buffer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if bytes.Equal(buffer[:bytesread], emptyBuffer[:bytesread]) {
|
||||
chunks[chunkNum] = currentLocation
|
||||
chunkNum++
|
||||
}
|
||||
currentLocation += int64(bytesread)
|
||||
}
|
||||
if chunkNum == 0 {
|
||||
chunkRanges = []int64{}
|
||||
} else {
|
||||
chunks = chunks[:chunkNum]
|
||||
chunkRanges = []int64{int64(chunkSize), chunks[0]}
|
||||
curCount := 0
|
||||
for i, chunk := range chunks {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
curCount++
|
||||
if chunk-chunks[i-1] > int64(chunkSize) {
|
||||
chunkRanges = append(chunkRanges, int64(curCount))
|
||||
chunkRanges = append(chunkRanges, chunk)
|
||||
curCount = 0
|
||||
}
|
||||
}
|
||||
chunkRanges = append(chunkRanges, int64(curCount+1))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ChunkRangesToChunks converts chunk ranges to list
|
||||
func ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {
|
||||
if len(chunkRanges) == 0 {
|
||||
return
|
||||
}
|
||||
chunkSize := chunkRanges[0]
|
||||
chunks = []int64{}
|
||||
for i := 1; i < len(chunkRanges); i += 2 {
|
||||
for j := int64(0); j < (chunkRanges[i+1]); j++ {
|
||||
chunks = append(chunks, chunkRanges[i]+j*chunkSize)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetLocalIPs returns all local ips
|
||||
func GetLocalIPs() (ips []string, err error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ips = []string{}
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ips = append(ips, ipnet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RandomFileName() (fname string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fname = f.Name()
|
||||
_ = f.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func FindOpenPorts(host string, portNumStart, numPorts int) (openPorts []int) {
|
||||
openPorts = []int{}
|
||||
for port := portNumStart; port-portNumStart < 200; port++ {
|
||||
timeout := 100 * time.Millisecond
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, fmt.Sprint(port)), timeout)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
} else if err != nil {
|
||||
openPorts = append(openPorts, port)
|
||||
}
|
||||
if len(openPorts) >= numPorts {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// local ip determination
|
||||
// https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space
|
||||
var privateIPBlocks []*net.IPNet
|
||||
|
||||
func init() {
|
||||
for _, cidr := range []string{
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"192.168.0.0/16", // RFC1918
|
||||
"169.254.0.0/16", // RFC3927 link-local
|
||||
"::1/128", // IPv6 loopback
|
||||
"fe80::/10", // IPv6 link-local
|
||||
"fc00::/7", // IPv6 unique local addr
|
||||
} {
|
||||
_, block, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
|
||||
}
|
||||
privateIPBlocks = append(privateIPBlocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
func IsLocalIP(ipaddress string) bool {
|
||||
if strings.Contains(ipaddress, "127.0.0.1") {
|
||||
return true
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(ipaddress)
|
||||
ip := net.ParseIP(host)
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
for _, block := range privateIPBlocks {
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ZipDirectory(destination string, source string) (err error) {
|
||||
if _, err = os.Stat(destination); err == nil {
|
||||
log.Errorf("%s file already exists!\n", destination)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Zipping %s to %s\n", source, destination)
|
||||
file, err := os.Create(destination)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer file.Close()
|
||||
writer := zip.NewWriter(file)
|
||||
// no compression because croc does its compression on the fly
|
||||
writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
|
||||
return flate.NewWriter(out, flate.NoCompression)
|
||||
})
|
||||
defer writer.Close()
|
||||
err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
f1, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer f1.Close()
|
||||
zipPath := strings.ReplaceAll(path, source, strings.TrimSuffix(destination, ".zip"))
|
||||
zipPath = filepath.ToSlash(zipPath)
|
||||
w1, err := writer.Create(zipPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if _, err := io.Copy(w1, f1); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\r\033[2K")
|
||||
fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnzipDirectory(destination string, source string) error {
|
||||
archive, err := zip.OpenReader(source)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
for _, f := range archive.File {
|
||||
filePath := filepath.Join(destination, f.Name)
|
||||
fmt.Fprintf(os.Stderr, "\r\033[2K")
|
||||
fmt.Fprintf(os.Stderr, "\rUnzipping file %s", filePath)
|
||||
// Issue #593 conceal path traversal vulnerability
|
||||
// make sure the filepath does not have ".."
|
||||
filePath = filepath.Clean(filePath)
|
||||
if strings.Contains(filePath, "..") {
|
||||
log.Errorf("Invalid file path %s\n", filePath)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(filePath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// check if file exists
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
prompt := fmt.Sprintf("\nOverwrite '%s'? (y/N) ", filePath)
|
||||
choice := strings.ToLower(GetInput(prompt))
|
||||
if choice != "y" && choice != "yes" {
|
||||
fmt.Fprintf(os.Stderr, "Skipping '%s'\n", filePath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
fileInArchive, err := f.Open()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
dstFile.Close()
|
||||
fileInArchive.Close()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidFileName checks if a filename is valid
|
||||
// by making sure it has no invisible characters
|
||||
func ValidFileName(fname string) (err error) {
|
||||
// make sure it doesn't contain unicode or invisible characters
|
||||
for _, r := range fname {
|
||||
if !unicode.IsGraphic(r) {
|
||||
err = fmt.Errorf("non-graphical unicode: %x U+%d in '%x'", string(r), r, fname)
|
||||
return
|
||||
}
|
||||
if !unicode.IsPrint(r) {
|
||||
err = fmt.Errorf("non-printable unicode: %x U+%d in '%x'", string(r), r, fname)
|
||||
return
|
||||
}
|
||||
}
|
||||
// make sure basename does not include ".." or path separators
|
||||
_, basename := filepath.Split(fname)
|
||||
if strings.Contains(basename, "..") {
|
||||
err = fmt.Errorf("basename cannot contain '..': '%s'", basename)
|
||||
return
|
||||
}
|
||||
if strings.Contains(basename, string(os.PathSeparator)) {
|
||||
err = fmt.Errorf("basename cannot contain path separators: '%s'", basename)
|
||||
return
|
||||
}
|
||||
// make sure the filename is not an absolute path
|
||||
if filepath.IsAbs(fname) {
|
||||
err = fmt.Errorf("filename cannot be an absolute path: '%s'", fname)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const crocRemovalFile = "croc-marked-files.txt"
|
||||
|
||||
func MarkFileForRemoval(fname string) {
|
||||
// append the fname to the list of files to remove
|
||||
f, err := os.OpenFile(crocRemovalFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(fname + "\n")
|
||||
}
|
||||
|
||||
func RemoveMarkedFiles() (err error) {
|
||||
// read the file and remove all the files
|
||||
f, err := os.Open(crocRemovalFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
fname := scanner.Text()
|
||||
err = os.Remove(fname)
|
||||
if err == nil {
|
||||
log.Tracef("Removed %s", fname)
|
||||
}
|
||||
}
|
||||
os.Remove(crocRemovalFile)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const TCP_BUFFER_SIZE = 1024 * 64
|
||||
|
||||
var bigFileSize = 75000000
|
||||
|
||||
func bigFile() {
|
||||
os.WriteFile("bigfile.test", bytes.Repeat([]byte("z"), bigFileSize), 0o666)
|
||||
}
|
||||
|
||||
func BenchmarkMD5(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MD5HashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkXXHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
XXHashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkImoHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IMOHashFile("bigfile.test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHighwayHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
HighwayHashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkImoHashFull(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IMOHashFileFull("bigfile.test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSha256(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SHA256("hello,world")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMissingChunks(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MissingChunks("bigfile.test", int64(bigFileSize), TCP_BUFFER_SIZE/2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
fmt.Println(GetLocalIPs())
|
||||
assert.True(t, Exists("bigfile.test"))
|
||||
assert.False(t, Exists("doesnotexist"))
|
||||
}
|
||||
|
||||
func TestMD5HashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := MD5HashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "8304ff018e02baad0e3555bade29a405", fmt.Sprintf("%x", b))
|
||||
_, err = MD5HashFile("bigfile.test.nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestHighwayHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := HighwayHashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329", fmt.Sprintf("%x", b))
|
||||
_, err = HighwayHashFile("bigfile.test.nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestIMOHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := IMOHashFile("bigfile.test")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "c0d1e12301e6c635f6d4a8ea5c897437", fmt.Sprintf("%x", b))
|
||||
}
|
||||
|
||||
func TestXXHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := XXHashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "4918740eb5ccb6f7", fmt.Sprintf("%x", b))
|
||||
_, err = XXHashFile("nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestSHA256(t *testing.T) {
|
||||
assert.Equal(t, "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b", SHA256("hello, world"))
|
||||
}
|
||||
|
||||
func TestByteCountDecimal(t *testing.T) {
|
||||
assert.Equal(t, "10.0 kB", ByteCountDecimal(10240))
|
||||
assert.Equal(t, "50 B", ByteCountDecimal(50))
|
||||
assert.Equal(t, "12.4 MB", ByteCountDecimal(13002343))
|
||||
}
|
||||
|
||||
func TestMissingChunks(t *testing.T) {
|
||||
fileSize := 100
|
||||
chunkSize := 10
|
||||
rand.Seed(1)
|
||||
bigBuff := make([]byte, fileSize)
|
||||
rand.Read(bigBuff)
|
||||
os.WriteFile("missing.test", bigBuff, 0o644)
|
||||
empty := make([]byte, chunkSize)
|
||||
f, err := os.OpenFile("missing.test", os.O_RDWR, 0o644)
|
||||
assert.Nil(t, err)
|
||||
for block := 0; block < fileSize/chunkSize; block++ {
|
||||
if block == 0 || block == 4 || block == 5 || block >= 7 {
|
||||
f.WriteAt(empty, int64(block*chunkSize))
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
|
||||
chunkRanges := MissingChunks("missing.test", int64(fileSize), chunkSize)
|
||||
assert.Equal(t, []int64{10, 0, 1, 40, 2, 70, 3}, chunkRanges)
|
||||
|
||||
chunks := ChunkRangesToChunks(chunkRanges)
|
||||
assert.Equal(t, []int64{0, 40, 50, 70, 80, 90}, chunks)
|
||||
|
||||
os.Remove("missing.test")
|
||||
|
||||
content := []byte("temporary file's content")
|
||||
tmpfile, err := os.CreateTemp("", "example")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)+10), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunkRanges = MissingChunks(tmpfile.Name()+"ok", int64(len(content)), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunks = ChunkRangesToChunks(chunkRanges)
|
||||
assert.Empty(t, chunks)
|
||||
}
|
||||
|
||||
// func Test1(t *testing.T) {
|
||||
// chunkRanges := MissingChunks("../../m/bigfile.test", int64(75000000), 1024*64/2)
|
||||
// fmt.Println(chunkRanges)
|
||||
// fmt.Println(ChunkRangesToChunks((chunkRanges)))
|
||||
// assert.Nil(t, nil)
|
||||
// }
|
||||
|
||||
func TestHashFile(t *testing.T) {
|
||||
content := []byte("temporary file's content")
|
||||
tmpfile, err := os.CreateTemp("", "example")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err = tmpfile.Write(content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
hashed, err := HashFile(tmpfile.Name(), "xxhash")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "e66c561610ad51e2", fmt.Sprintf("%x", hashed))
|
||||
}
|
||||
|
||||
func TestPublicIP(t *testing.T) {
|
||||
ip, err := PublicIP()
|
||||
fmt.Println(ip)
|
||||
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestLocalIP(t *testing.T) {
|
||||
ip := LocalIP()
|
||||
fmt.Println(ip)
|
||||
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
|
||||
}
|
||||
|
||||
func TestGetRandomName(t *testing.T) {
|
||||
name := GetRandomName()
|
||||
fmt.Println(name)
|
||||
assert.NotEmpty(t, name)
|
||||
}
|
||||
|
||||
func intSliceSame(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestFindOpenPorts(t *testing.T) {
|
||||
openPorts := FindOpenPorts("127.0.0.1", 9009, 4)
|
||||
if !intSliceSame(openPorts, []int{9009, 9010, 9011, 9012}) && !intSliceSame(openPorts, []int{9014, 9015, 9016, 9017}) {
|
||||
t.Errorf("openPorts: %v", openPorts)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLocalIP(t *testing.T) {
|
||||
assert.True(t, IsLocalIP("192.168.0.14:9009"))
|
||||
}
|
||||
|
||||
func TestValidFileName(t *testing.T) {
|
||||
// contains regular characters
|
||||
assert.Nil(t, ValidFileName("中文.csl"))
|
||||
// contains regular characters
|
||||
assert.Nil(t, ValidFileName("[something].csl"))
|
||||
// contains regular characters
|
||||
assert.Nil(t, ValidFileName("[(something)].csl"))
|
||||
// contains invisible character
|
||||
err := ValidFileName("D中文.cslouglas")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "non-graphical unicode: e2808b U+8203 in '44e4b8ade696872e63736c6f75676c6173e2808b'", err.Error())
|
||||
assert.NotNil(t, ValidFileName("hi..txt"))
|
||||
assert.NotNil(t, ValidFileName(path.Join(string(os.PathSeparator), "abs", string(os.PathSeparator), "hi.txt")))
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func CatFiles(files []string, outfile string, remove ...bool) error {
|
||||
finished, err := os.Create(outfile)
|
||||
defer finished.Close()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles create: ")
|
||||
}
|
||||
for i := range files {
|
||||
fh, err := os.Open(files[i])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles open "+files[i]+": ")
|
||||
}
|
||||
|
||||
_, err = io.Copy(finished, fh)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles copy: ")
|
||||
}
|
||||
fh.Close()
|
||||
if len(remove) > 0 && remove[0] {
|
||||
os.Remove(files[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitFile
|
||||
func SplitFile(fileName string, numPieces int) (err error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytesPerPiece := int(math.Ceil(float64(fi.Size()) / float64(numPieces)))
|
||||
bytesRead := 0
|
||||
i := 0
|
||||
out, err := os.Create(fileName + "." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
if bytesPerPiece < 4096/numPieces {
|
||||
buf = make([]byte, bytesPerPiece)
|
||||
}
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
out.Write(buf[:n])
|
||||
bytesRead += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if bytesRead >= bytesPerPiece {
|
||||
// Close file and open a new one
|
||||
out.Close()
|
||||
i++
|
||||
out, err = os.Create(fileName + "." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bytesRead = 0
|
||||
}
|
||||
}
|
||||
out.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from src to dst. If src and dst files exist, and are
|
||||
// the same, then return success. Otherise, attempt to create a hard link
|
||||
// between the two files. If that fail, copy the file contents from src to dst.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !sfi.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories,
|
||||
// symlinks, devices, etc.)
|
||||
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||
}
|
||||
dfi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !(dfi.Mode().IsRegular()) {
|
||||
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
}
|
||||
if os.SameFile(sfi, dfi) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = os.Link(src, dst); err == nil {
|
||||
return
|
||||
}
|
||||
err = copyFileContents(src, dst)
|
||||
return
|
||||
}
|
||||
|
||||
// copyFileContents copies the contents of the file named src to the file named
|
||||
// by dst. The file will be created if it does not already exist. If the
|
||||
// destination file exists, all it's contents will be replaced by the contents
|
||||
// of the source file.
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
err = out.Sync()
|
||||
return
|
||||
}
|
||||
|
||||
// HashFile does a md5 hash on the file
|
||||
// from https://golang.org/pkg/crypto/md5/#example_New_file
|
||||
func HashFile(filename string) (hash string, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
hash = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
}
|
||||
|
||||
// FileSize returns the size of a file
|
||||
func FileSize(filename string) (int, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
size := int(fi.Size())
|
||||
return size, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitFile(t *testing.T) {
|
||||
err := SplitFile("README.md", 3)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBigByteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
exp uint64
|
||||
}{
|
||||
{"42", 42},
|
||||
{"42MB", 42000000},
|
||||
{"42MiB", 44040192},
|
||||
{"42mb", 42000000},
|
||||
{"42mib", 44040192},
|
||||
{"42MIB", 44040192},
|
||||
{"42 MB", 42000000},
|
||||
{"42 MiB", 44040192},
|
||||
{"42 mb", 42000000},
|
||||
{"42 mib", 44040192},
|
||||
{"42 MIB", 44040192},
|
||||
{"42.5MB", 42500000},
|
||||
{"42.5MiB", 44564480},
|
||||
{"42.5 MB", 42500000},
|
||||
{"42.5 MiB", 44564480},
|
||||
// No need to say B
|
||||
{"42M", 42000000},
|
||||
{"42Mi", 44040192},
|
||||
{"42m", 42000000},
|
||||
{"42mi", 44040192},
|
||||
{"42MI", 44040192},
|
||||
{"42 M", 42000000},
|
||||
{"42 Mi", 44040192},
|
||||
{"42 m", 42000000},
|
||||
{"42 mi", 44040192},
|
||||
{"42 MI", 44040192},
|
||||
{"42.5M", 42500000},
|
||||
{"42.5Mi", 44564480},
|
||||
{"42.5 M", 42500000},
|
||||
{"42.5 Mi", 44564480},
|
||||
{"1,005.03 MB", 1005030000},
|
||||
// Large testing, breaks when too much larger than
|
||||
// this.
|
||||
{"12.5 EB", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 E", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 EiB", uint64(12.5 * float64(EiByte))},
|
||||
}
|
||||
|
||||
for _, p := range tests {
|
||||
got, err := ParseBigBytes(p.in)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't parse %v: %v", p.in, err)
|
||||
} else {
|
||||
if got.Uint64() != p.exp {
|
||||
t.Errorf("Expected %v for %v, got %v",
|
||||
p.exp, p.in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBigByteErrors(t *testing.T) {
|
||||
got, err := ParseBigBytes("84 JB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
got, err = ParseBigBytes("")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error parsing nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func bbyte(in uint64) string {
|
||||
return BigBytes((&big.Int{}).SetUint64(in))
|
||||
}
|
||||
|
||||
func bibyte(in uint64) string {
|
||||
return BigIBytes((&big.Int{}).SetUint64(in))
|
||||
}
|
||||
|
||||
func TestBigBytes(t *testing.T) {
|
||||
testList{
|
||||
{"bytes(0)", bbyte(0), "0 B"},
|
||||
{"bytes(1)", bbyte(1), "1 B"},
|
||||
{"bytes(803)", bbyte(803), "803 B"},
|
||||
{"bytes(999)", bbyte(999), "999 B"},
|
||||
|
||||
{"bytes(1024)", bbyte(1024), "1.0 kB"},
|
||||
{"bytes(1MB - 1)", bbyte(MByte - Byte), "1000 kB"},
|
||||
|
||||
{"bytes(1MB)", bbyte(1024 * 1024), "1.0 MB"},
|
||||
{"bytes(1GB - 1K)", bbyte(GByte - KByte), "1000 MB"},
|
||||
|
||||
{"bytes(1GB)", bbyte(GByte), "1.0 GB"},
|
||||
{"bytes(1TB - 1M)", bbyte(TByte - MByte), "1000 GB"},
|
||||
|
||||
{"bytes(1TB)", bbyte(TByte), "1.0 TB"},
|
||||
{"bytes(1PB - 1T)", bbyte(PByte - TByte), "999 TB"},
|
||||
|
||||
{"bytes(1PB)", bbyte(PByte), "1.0 PB"},
|
||||
{"bytes(1PB - 1T)", bbyte(EByte - PByte), "999 PB"},
|
||||
|
||||
{"bytes(1EB)", bbyte(EByte), "1.0 EB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"},
|
||||
|
||||
{"bytes(0)", bibyte(0), "0 B"},
|
||||
{"bytes(1)", bibyte(1), "1 B"},
|
||||
{"bytes(803)", bibyte(803), "803 B"},
|
||||
{"bytes(1023)", bibyte(1023), "1023 B"},
|
||||
|
||||
{"bytes(1024)", bibyte(1024), "1.0 KiB"},
|
||||
{"bytes(1MB - 1)", bibyte(MiByte - IByte), "1024 KiB"},
|
||||
|
||||
{"bytes(1MB)", bibyte(1024 * 1024), "1.0 MiB"},
|
||||
{"bytes(1GB - 1K)", bibyte(GiByte - KiByte), "1024 MiB"},
|
||||
|
||||
{"bytes(1GB)", bibyte(GiByte), "1.0 GiB"},
|
||||
{"bytes(1TB - 1M)", bibyte(TiByte - MiByte), "1024 GiB"},
|
||||
|
||||
{"bytes(1TB)", bibyte(TiByte), "1.0 TiB"},
|
||||
{"bytes(1PB - 1T)", bibyte(PiByte - TiByte), "1023 TiB"},
|
||||
|
||||
{"bytes(1PB)", bibyte(PiByte), "1.0 PiB"},
|
||||
{"bytes(1PB - 1T)", bibyte(EiByte - PiByte), "1023 PiB"},
|
||||
|
||||
{"bytes(1EiB)", bibyte(EiByte), "1.0 EiB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", bibyte((KIByte*EIByte)-PiByte), "1023EB"},
|
||||
|
||||
{"bytes(5.5GiB)", bibyte(5.5 * GiByte), "5.5 GiB"},
|
||||
|
||||
{"bytes(5.5GB)", bbyte(5.5 * GByte), "5.5 GB"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestVeryBigBytes(t *testing.T) {
|
||||
b, _ := (&big.Int{}).SetString("15347691069326346944512", 10)
|
||||
s := BigBytes(b)
|
||||
if s != "15 ZB" {
|
||||
t.Errorf("Expected 15 ZB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13 ZiB" {
|
||||
t.Errorf("Expected 13 ZiB, got %v", s)
|
||||
}
|
||||
|
||||
b, _ = (&big.Int{}).SetString("15716035654990179271180288", 10)
|
||||
s = BigBytes(b)
|
||||
if s != "16 YB" {
|
||||
t.Errorf("Expected 16 YB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13 YiB" {
|
||||
t.Errorf("Expected 13 YiB, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVeryVeryBigBytes(t *testing.T) {
|
||||
b, _ := (&big.Int{}).SetString("16093220510709943573688614912", 10)
|
||||
s := BigBytes(b)
|
||||
if s != "16093 YB" {
|
||||
t.Errorf("Expected 16093 YB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13312 YiB" {
|
||||
t.Errorf("Expected 13312 YiB, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVeryBig(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"16 ZB", "16000000000000000000000"},
|
||||
{"16 ZiB", "18889465931478580854784"},
|
||||
{"16.5 ZB", "16500000000000000000000"},
|
||||
{"16.5 ZiB", "19479761741837286506496"},
|
||||
{"16 Z", "16000000000000000000000"},
|
||||
{"16 Zi", "18889465931478580854784"},
|
||||
{"16.5 Z", "16500000000000000000000"},
|
||||
{"16.5 Zi", "19479761741837286506496"},
|
||||
|
||||
{"16 YB", "16000000000000000000000000"},
|
||||
{"16 YiB", "19342813113834066795298816"},
|
||||
{"16.5 YB", "16500000000000000000000000"},
|
||||
{"16.5 YiB", "19947276023641381382651904"},
|
||||
{"16 Y", "16000000000000000000000000"},
|
||||
{"16 Yi", "19342813113834066795298816"},
|
||||
{"16.5 Y", "16500000000000000000000000"},
|
||||
{"16.5 Yi", "19947276023641381382651904"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
x, err := ParseBigBytes(test.in)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %q: %v", test.in, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if x.String() != test.out {
|
||||
t.Errorf("Expected %q for %q, got %v", test.out, test.in, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseBigBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseBigBytes("16.5 Z")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
bibyte(16.5 * GByte)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestByteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
exp uint64
|
||||
}{
|
||||
{"42", 42},
|
||||
{"42MB", 42000000},
|
||||
{"42MiB", 44040192},
|
||||
{"42mb", 42000000},
|
||||
{"42mib", 44040192},
|
||||
{"42MIB", 44040192},
|
||||
{"42 MB", 42000000},
|
||||
{"42 MiB", 44040192},
|
||||
{"42 mb", 42000000},
|
||||
{"42 mib", 44040192},
|
||||
{"42 MIB", 44040192},
|
||||
{"42.5MB", 42500000},
|
||||
{"42.5MiB", 44564480},
|
||||
{"42.5 MB", 42500000},
|
||||
{"42.5 MiB", 44564480},
|
||||
// No need to say B
|
||||
{"42M", 42000000},
|
||||
{"42Mi", 44040192},
|
||||
{"42m", 42000000},
|
||||
{"42mi", 44040192},
|
||||
{"42MI", 44040192},
|
||||
{"42 M", 42000000},
|
||||
{"42 Mi", 44040192},
|
||||
{"42 m", 42000000},
|
||||
{"42 mi", 44040192},
|
||||
{"42 MI", 44040192},
|
||||
{"42.5M", 42500000},
|
||||
{"42.5Mi", 44564480},
|
||||
{"42.5 M", 42500000},
|
||||
{"42.5 Mi", 44564480},
|
||||
// Bug #42
|
||||
{"1,005.03 MB", 1005030000},
|
||||
// Large testing, breaks when too much larger than
|
||||
// this.
|
||||
{"12.5 EB", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 E", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 EiB", uint64(12.5 * float64(EiByte))},
|
||||
}
|
||||
|
||||
for _, p := range tests {
|
||||
got, err := ParseBytes(p.in)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't parse %v: %v", p.in, err)
|
||||
}
|
||||
if got != p.exp {
|
||||
t.Errorf("Expected %v for %v, got %v",
|
||||
p.exp, p.in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteErrors(t *testing.T) {
|
||||
got, err := ParseBytes("84 JB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
got, err = ParseBytes("")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error parsing nothing")
|
||||
}
|
||||
got, err = ParseBytes("16 EiB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytes(t *testing.T) {
|
||||
testList{
|
||||
{"bytes(0)", Bytes(0), "0 B"},
|
||||
{"bytes(1)", Bytes(1), "1 B"},
|
||||
{"bytes(803)", Bytes(803), "803 B"},
|
||||
{"bytes(999)", Bytes(999), "999 B"},
|
||||
|
||||
{"bytes(1024)", Bytes(1024), "1.0 kB"},
|
||||
{"bytes(9999)", Bytes(9999), "10 kB"},
|
||||
{"bytes(1MB - 1)", Bytes(MByte - Byte), "1000 kB"},
|
||||
|
||||
{"bytes(1MB)", Bytes(1024 * 1024), "1.0 MB"},
|
||||
{"bytes(1GB - 1K)", Bytes(GByte - KByte), "1000 MB"},
|
||||
|
||||
{"bytes(1GB)", Bytes(GByte), "1.0 GB"},
|
||||
{"bytes(1TB - 1M)", Bytes(TByte - MByte), "1000 GB"},
|
||||
{"bytes(10MB)", Bytes(9999 * 1000), "10 MB"},
|
||||
|
||||
{"bytes(1TB)", Bytes(TByte), "1.0 TB"},
|
||||
{"bytes(1PB - 1T)", Bytes(PByte - TByte), "999 TB"},
|
||||
|
||||
{"bytes(1PB)", Bytes(PByte), "1.0 PB"},
|
||||
{"bytes(1PB - 1T)", Bytes(EByte - PByte), "999 PB"},
|
||||
|
||||
{"bytes(1EB)", Bytes(EByte), "1.0 EB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"},
|
||||
|
||||
{"bytes(0)", IBytes(0), "0 B"},
|
||||
{"bytes(1)", IBytes(1), "1 B"},
|
||||
{"bytes(803)", IBytes(803), "803 B"},
|
||||
{"bytes(1023)", IBytes(1023), "1023 B"},
|
||||
|
||||
{"bytes(1024)", IBytes(1024), "1.0 KiB"},
|
||||
{"bytes(1MB - 1)", IBytes(MiByte - IByte), "1024 KiB"},
|
||||
|
||||
{"bytes(1MB)", IBytes(1024 * 1024), "1.0 MiB"},
|
||||
{"bytes(1GB - 1K)", IBytes(GiByte - KiByte), "1024 MiB"},
|
||||
|
||||
{"bytes(1GB)", IBytes(GiByte), "1.0 GiB"},
|
||||
{"bytes(1TB - 1M)", IBytes(TiByte - MiByte), "1024 GiB"},
|
||||
|
||||
{"bytes(1TB)", IBytes(TiByte), "1.0 TiB"},
|
||||
{"bytes(1PB - 1T)", IBytes(PiByte - TiByte), "1023 TiB"},
|
||||
|
||||
{"bytes(1PB)", IBytes(PiByte), "1.0 PiB"},
|
||||
{"bytes(1PB - 1T)", IBytes(EiByte - PiByte), "1023 PiB"},
|
||||
|
||||
{"bytes(1EiB)", IBytes(EiByte), "1.0 EiB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", IBytes((KIByte*EIByte)-PiByte), "1023EB"},
|
||||
|
||||
{"bytes(5.5GiB)", IBytes(5.5 * GiByte), "5.5 GiB"},
|
||||
|
||||
{"bytes(5.5GB)", Bytes(5.5 * GByte), "5.5 GB"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkParseBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseBytes("16.5 GB")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Bytes(16.5 * GByte)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// minin64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommas(t *testing.T) {
|
||||
testList{
|
||||
{"0", Comma(0), "0"},
|
||||
{"10", Comma(10), "10"},
|
||||
{"100", Comma(100), "100"},
|
||||
{"1,000", Comma(1000), "1,000"},
|
||||
{"10,000", Comma(10000), "10,000"},
|
||||
{"100,000", Comma(100000), "100,000"},
|
||||
{"10,000,000", Comma(10000000), "10,000,000"},
|
||||
{"10,100,000", Comma(10100000), "10,100,000"},
|
||||
{"10,010,000", Comma(10010000), "10,010,000"},
|
||||
{"10,001,000", Comma(10001000), "10,001,000"},
|
||||
{"123,456,789", Comma(123456789), "123,456,789"},
|
||||
{"maxint", Comma(9.223372e+18), "9,223,372,000,000,000,000"},
|
||||
{"math.maxint", Comma(math.MaxInt64), "9,223,372,036,854,775,807"},
|
||||
{"math.minint", Comma(math.MinInt64), "-9,223,372,036,854,775,808"},
|
||||
{"minint", Comma(-9.223372e+18), "-9,223,372,000,000,000,000"},
|
||||
{"-123,456,789", Comma(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", Comma(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", Comma(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", Comma(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", Comma(-10000000), "-10,000,000"},
|
||||
{"-100,000", Comma(-100000), "-100,000"},
|
||||
{"-10,000", Comma(-10000), "-10,000"},
|
||||
{"-1,000", Comma(-1000), "-1,000"},
|
||||
{"-100", Comma(-100), "-100"},
|
||||
{"-10", Comma(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestCommafs(t *testing.T) {
|
||||
testList{
|
||||
{"0", Commaf(0), "0"},
|
||||
{"10.11", Commaf(10.11), "10.11"},
|
||||
{"100", Commaf(100), "100"},
|
||||
{"1,000", Commaf(1000), "1,000"},
|
||||
{"10,000", Commaf(10000), "10,000"},
|
||||
{"100,000", Commaf(100000), "100,000"},
|
||||
{"834,142.32", Commaf(834142.32), "834,142.32"},
|
||||
{"10,000,000", Commaf(10000000), "10,000,000"},
|
||||
{"10,100,000", Commaf(10100000), "10,100,000"},
|
||||
{"10,010,000", Commaf(10010000), "10,010,000"},
|
||||
{"10,001,000", Commaf(10001000), "10,001,000"},
|
||||
{"123,456,789", Commaf(123456789), "123,456,789"},
|
||||
{"maxf64", Commaf(math.MaxFloat64), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"},
|
||||
{"minf64", Commaf(math.SmallestNonzeroFloat64), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005"},
|
||||
{"-123,456,789", Commaf(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", Commaf(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", Commaf(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", Commaf(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", Commaf(-10000000), "-10,000,000"},
|
||||
{"-100,000", Commaf(-100000), "-100,000"},
|
||||
{"-10,000", Commaf(-10000), "-10,000"},
|
||||
{"-1,000", Commaf(-1000), "-1,000"},
|
||||
{"-100.11", Commaf(-100.11), "-100.11"},
|
||||
{"-10", Commaf(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkCommas(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Comma(1234567890)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCommaf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Commaf(1234567890.83584)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigCommas(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
BigComma(big.NewInt(1234567890))
|
||||
}
|
||||
}
|
||||
|
||||
func bigComma(i int64) string {
|
||||
return BigComma(big.NewInt(i))
|
||||
}
|
||||
|
||||
func TestBigCommas(t *testing.T) {
|
||||
testList{
|
||||
{"0", bigComma(0), "0"},
|
||||
{"10", bigComma(10), "10"},
|
||||
{"100", bigComma(100), "100"},
|
||||
{"1,000", bigComma(1000), "1,000"},
|
||||
{"10,000", bigComma(10000), "10,000"},
|
||||
{"100,000", bigComma(100000), "100,000"},
|
||||
{"10,000,000", bigComma(10000000), "10,000,000"},
|
||||
{"10,100,000", bigComma(10100000), "10,100,000"},
|
||||
{"10,010,000", bigComma(10010000), "10,010,000"},
|
||||
{"10,001,000", bigComma(10001000), "10,001,000"},
|
||||
{"123,456,789", bigComma(123456789), "123,456,789"},
|
||||
{"maxint", bigComma(9.223372e+18), "9,223,372,000,000,000,000"},
|
||||
{"minint", bigComma(-9.223372e+18), "-9,223,372,000,000,000,000"},
|
||||
{"-123,456,789", bigComma(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", bigComma(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", bigComma(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", bigComma(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", bigComma(-10000000), "-10,000,000"},
|
||||
{"-100,000", bigComma(-100000), "-100,000"},
|
||||
{"-10,000", bigComma(-10000), "-10,000"},
|
||||
{"-1,000", bigComma(-1000), "-1,000"},
|
||||
{"-100", bigComma(-100), "-100"},
|
||||
{"-10", bigComma(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestVeryBigCommas(t *testing.T) {
|
||||
tests := []struct{ in, exp string }{
|
||||
{
|
||||
"84889279597249724975972597249849757294578485",
|
||||
"84,889,279,597,249,724,975,972,597,249,849,757,294,578,485",
|
||||
},
|
||||
{
|
||||
"-84889279597249724975972597249849757294578485",
|
||||
"-84,889,279,597,249,724,975,972,597,249,849,757,294,578,485",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
n, _ := (&big.Int{}).SetString(test.in, 10)
|
||||
got := BigComma(n)
|
||||
if test.exp != got {
|
||||
t.Errorf("Expected %q, got %q", test.exp, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkBigCommaf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Commaf(1234567890.83584)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBigCommafs(t *testing.T) {
|
||||
testList{
|
||||
{"0", BigCommaf(big.NewFloat(0)), "0"},
|
||||
{"10.11", BigCommaf(big.NewFloat(10.11)), "10.11"},
|
||||
{"100", BigCommaf(big.NewFloat(100)), "100"},
|
||||
{"1,000", BigCommaf(big.NewFloat(1000)), "1,000"},
|
||||
{"10,000", BigCommaf(big.NewFloat(10000)), "10,000"},
|
||||
{"100,000", BigCommaf(big.NewFloat(100000)), "100,000"},
|
||||
{"834,142.32", BigCommaf(big.NewFloat(834142.32)), "834,142.32"},
|
||||
{"10,000,000", BigCommaf(big.NewFloat(10000000)), "10,000,000"},
|
||||
{"10,100,000", BigCommaf(big.NewFloat(10100000)), "10,100,000"},
|
||||
{"10,010,000", BigCommaf(big.NewFloat(10010000)), "10,010,000"},
|
||||
{"10,001,000", BigCommaf(big.NewFloat(10001000)), "10,001,000"},
|
||||
{"123,456,789", BigCommaf(big.NewFloat(123456789)), "123,456,789"},
|
||||
{"maxf64", BigCommaf(big.NewFloat(math.MaxFloat64)), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"},
|
||||
{"minf64", BigCommaf(big.NewFloat(math.SmallestNonzeroFloat64)), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465"},
|
||||
{"-123,456,789", BigCommaf(big.NewFloat(-123456789)), "-123,456,789"},
|
||||
{"-10,100,000", BigCommaf(big.NewFloat(-10100000)), "-10,100,000"},
|
||||
{"-10,010,000", BigCommaf(big.NewFloat(-10010000)), "-10,010,000"},
|
||||
{"-10,001,000", BigCommaf(big.NewFloat(-10001000)), "-10,001,000"},
|
||||
{"-10,000,000", BigCommaf(big.NewFloat(-10000000)), "-10,000,000"},
|
||||
{"-100,000", BigCommaf(big.NewFloat(-100000)), "-100,000"},
|
||||
{"-10,000", BigCommaf(big.NewFloat(-10000)), "-10,000"},
|
||||
{"-1,000", BigCommaf(big.NewFloat(-1000)), "-1,000"},
|
||||
{"-100.11", BigCommaf(big.NewFloat(-100.11)), "-100.11"},
|
||||
{"-10", BigCommaf(big.NewFloat(-10)), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testList []struct {
|
||||
name, got, exp string
|
||||
}
|
||||
|
||||
func (tl testList) validate(t *testing.T) {
|
||||
for _, test := range tl {
|
||||
if test.got != test.exp {
|
||||
t.Errorf("On %v, expected '%v', but got '%v'",
|
||||
test.name, test.exp, test.got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFtoa(t *testing.T) {
|
||||
testList{
|
||||
{"200", Ftoa(200), "200"},
|
||||
{"2", Ftoa(2), "2"},
|
||||
{"2.2", Ftoa(2.2), "2.2"},
|
||||
{"2.02", Ftoa(2.02), "2.02"},
|
||||
{"200.02", Ftoa(200.02), "200.02"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkFtoaRegexTrailing(b *testing.B) {
|
||||
trailingZerosRegex := regexp.MustCompile(`\.?0+$`)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
trailingZerosRegex.ReplaceAllString("2.00000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.0000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.00", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.0", "")
|
||||
trailingZerosRegex.ReplaceAllString("2", "")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFtoaFunc(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
stripTrailingZeros("2.00000")
|
||||
stripTrailingZeros("2.0000")
|
||||
stripTrailingZeros("2.000")
|
||||
stripTrailingZeros("2.00")
|
||||
stripTrailingZeros("2.0")
|
||||
stripTrailingZeros("2")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFmtF(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%f", 2.03584)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStrconvF(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
strconv.FormatFloat(2.03584, 'f', 6, 64)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
name string
|
||||
format string
|
||||
num float64
|
||||
formatted string
|
||||
}
|
||||
|
||||
func TestFormatFloat(t *testing.T) {
|
||||
tests := []TestStruct{
|
||||
{"default", "", 12345.6789, "12,345.68"},
|
||||
{"#", "#", 12345.6789, "12345.678900000"},
|
||||
{"#.", "#.", 12345.6789, "12346"},
|
||||
{"#,#", "#,#", 12345.6789, "12345,7"},
|
||||
{"#,##", "#,##", 12345.6789, "12345,68"},
|
||||
{"#,###", "#,###", 12345.6789, "12345,679"},
|
||||
{"#,###.", "#,###.", 12345.6789, "12,346"},
|
||||
{"#,###.##", "#,###.##", 12345.6789, "12,345.68"},
|
||||
{"#,###.###", "#,###.###", 12345.6789, "12,345.679"},
|
||||
{"#,###.####", "#,###.####", 12345.6789, "12,345.6789"},
|
||||
{"#.###,######", "#.###,######", 12345.6789, "12.345,678900"},
|
||||
{"bug46", "#,###.##", 52746220055.92342, "52,746,220,055.92"},
|
||||
{"#\u202f###,##", "#\u202f###,##", 12345.6789, "12 345,68"},
|
||||
|
||||
// special cases
|
||||
{"NaN", "#", math.NaN(), "NaN"},
|
||||
{"+Inf", "#", math.Inf(1), "Infinity"},
|
||||
{"-Inf", "#", math.Inf(-1), "-Infinity"},
|
||||
{"signStr <= -0.000000001", "", -0.000000002, "-0.00"},
|
||||
{"signStr = 0", "", 0, "0.00"},
|
||||
{"Format directive must start with +", "+000", 12345.6789, "+12345.678900000"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := FormatFloat(test.format, test.num)
|
||||
if got != test.formatted {
|
||||
t.Errorf("On %v (%v, %v), got %v, wanted %v",
|
||||
test.name, test.format, test.num, got, test.formatted)
|
||||
}
|
||||
}
|
||||
// Test a single integer
|
||||
got := FormatInteger("#", 12345)
|
||||
if got != "12345.000000000" {
|
||||
t.Errorf("On %v (%v, %v), got %v, wanted %v",
|
||||
"integerTest", "#", 12345, got, "12345.000000000")
|
||||
}
|
||||
// Test the things that could panic
|
||||
panictests := []TestStruct{
|
||||
{"RenderFloat(): invalid positive sign directive", "-", 12345.6789, "12,345.68"},
|
||||
{"RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers", "0.01", 12345.6789, "12,345.68"},
|
||||
}
|
||||
for _, test := range panictests {
|
||||
didPanic := false
|
||||
var message interface{}
|
||||
func() {
|
||||
|
||||
defer func() {
|
||||
if message = recover(); message != nil {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
|
||||
// call the target function
|
||||
_ = FormatFloat(test.format, test.num)
|
||||
|
||||
}()
|
||||
if didPanic != true {
|
||||
t.Errorf("On %v, should have panic and did not.",
|
||||
test.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrdinals(t *testing.T) {
|
||||
testList{
|
||||
{"0", Ordinal(0), "0th"},
|
||||
{"1", Ordinal(1), "1st"},
|
||||
{"2", Ordinal(2), "2nd"},
|
||||
{"3", Ordinal(3), "3rd"},
|
||||
{"4", Ordinal(4), "4th"},
|
||||
{"10", Ordinal(10), "10th"},
|
||||
{"11", Ordinal(11), "11th"},
|
||||
{"12", Ordinal(12), "12th"},
|
||||
{"13", Ordinal(13), "13th"},
|
||||
{"101", Ordinal(101), "101st"},
|
||||
{"102", Ordinal(102), "102nd"},
|
||||
{"103", Ordinal(103), "103rd"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
num float64
|
||||
formatted string
|
||||
}{
|
||||
{"e-24", 1e-24, "1 yF"},
|
||||
{"e-21", 1e-21, "1 zF"},
|
||||
{"e-18", 1e-18, "1 aF"},
|
||||
{"e-15", 1e-15, "1 fF"},
|
||||
{"e-12", 1e-12, "1 pF"},
|
||||
{"e-12", 2.2345e-12, "2.2345 pF"},
|
||||
{"e-12", 2.23e-12, "2.23 pF"},
|
||||
{"e-11", 2.23e-11, "22.3 pF"},
|
||||
{"e-10", 2.2e-10, "220 pF"},
|
||||
{"e-9", 2.2e-9, "2.2 nF"},
|
||||
{"e-8", 2.2e-8, "22 nF"},
|
||||
{"e-7", 2.2e-7, "220 nF"},
|
||||
{"e-6", 2.2e-6, "2.2 µF"},
|
||||
{"e-6", 1e-6, "1 µF"},
|
||||
{"e-5", 2.2e-5, "22 µF"},
|
||||
{"e-4", 2.2e-4, "220 µF"},
|
||||
{"e-3", 2.2e-3, "2.2 mF"},
|
||||
{"e-2", 2.2e-2, "22 mF"},
|
||||
{"e-1", 2.2e-1, "220 mF"},
|
||||
{"e+0", 2.2e-0, "2.2 F"},
|
||||
{"e+0", 2.2, "2.2 F"},
|
||||
{"e+1", 2.2e+1, "22 F"},
|
||||
{"0", 0, "0 F"},
|
||||
{"e+1", 22, "22 F"},
|
||||
{"e+2", 2.2e+2, "220 F"},
|
||||
{"e+2", 220, "220 F"},
|
||||
{"e+3", 2.2e+3, "2.2 kF"},
|
||||
{"e+3", 2200, "2.2 kF"},
|
||||
{"e+4", 2.2e+4, "22 kF"},
|
||||
{"e+4", 22000, "22 kF"},
|
||||
{"e+5", 2.2e+5, "220 kF"},
|
||||
{"e+6", 2.2e+6, "2.2 MF"},
|
||||
{"e+6", 1e+6, "1 MF"},
|
||||
{"e+7", 2.2e+7, "22 MF"},
|
||||
{"e+8", 2.2e+8, "220 MF"},
|
||||
{"e+9", 2.2e+9, "2.2 GF"},
|
||||
{"e+10", 2.2e+10, "22 GF"},
|
||||
{"e+11", 2.2e+11, "220 GF"},
|
||||
{"e+12", 2.2e+12, "2.2 TF"},
|
||||
{"e+15", 2.2e+15, "2.2 PF"},
|
||||
{"e+18", 2.2e+18, "2.2 EF"},
|
||||
{"e+21", 2.2e+21, "2.2 ZF"},
|
||||
{"e+24", 2.2e+24, "2.2 YF"},
|
||||
|
||||
// special case
|
||||
{"1F", 1000 * 1000, "1 MF"},
|
||||
{"1F", 1e6, "1 MF"},
|
||||
|
||||
// negative number
|
||||
{"-100 F", -100, "-100 F"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := SI(test.num, "F")
|
||||
if got != test.formatted {
|
||||
t.Errorf("On %v (%v), got %v, wanted %v",
|
||||
test.name, test.num, got, test.formatted)
|
||||
}
|
||||
|
||||
gotf, gotu, err := ParseSI(test.formatted)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %v (%v): %v", test.name, test.formatted, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if math.Abs(1-(gotf/test.num)) > 0.01 {
|
||||
t.Errorf("On %v (%v), got %v, wanted %v (±%v)",
|
||||
test.name, test.formatted, gotf, test.num,
|
||||
math.Abs(1-(gotf/test.num)))
|
||||
}
|
||||
if gotu != "F" {
|
||||
t.Errorf("On %v (%v), expected unit F, got %v",
|
||||
test.name, test.formatted, gotu)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error
|
||||
gotf, gotu, err := ParseSI("x1.21JW") // 1.21 jigga whats
|
||||
if err == nil {
|
||||
t.Errorf("Expected error on x1.21JW, got %v %v", gotf, gotu)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseSI(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseSI("2.2346ZB")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D >= diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPast(t *testing.T) {
|
||||
now := time.Now()
|
||||
testList{
|
||||
{"now", Time(now), "now"},
|
||||
{"1 second ago", Time(now.Add(-1 * time.Second)), "1 second ago"},
|
||||
{"12 seconds ago", Time(now.Add(-12 * time.Second)), "12 seconds ago"},
|
||||
{"30 seconds ago", Time(now.Add(-30 * time.Second)), "30 seconds ago"},
|
||||
{"45 seconds ago", Time(now.Add(-45 * time.Second)), "45 seconds ago"},
|
||||
{"1 minute ago", Time(now.Add(-63 * time.Second)), "1 minute ago"},
|
||||
{"15 minutes ago", Time(now.Add(-15 * time.Minute)), "15 minutes ago"},
|
||||
{"1 hour ago", Time(now.Add(-63 * time.Minute)), "1 hour ago"},
|
||||
{"2 hours ago", Time(now.Add(-2 * time.Hour)), "2 hours ago"},
|
||||
{"21 hours ago", Time(now.Add(-21 * time.Hour)), "21 hours ago"},
|
||||
{"1 day ago", Time(now.Add(-26 * time.Hour)), "1 day ago"},
|
||||
{"2 days ago", Time(now.Add(-49 * time.Hour)), "2 days ago"},
|
||||
{"3 days ago", Time(now.Add(-3 * Day)), "3 days ago"},
|
||||
{"1 week ago (1)", Time(now.Add(-7 * Day)), "1 week ago"},
|
||||
{"1 week ago (2)", Time(now.Add(-12 * Day)), "1 week ago"},
|
||||
{"2 weeks ago", Time(now.Add(-15 * Day)), "2 weeks ago"},
|
||||
{"1 month ago", Time(now.Add(-39 * Day)), "1 month ago"},
|
||||
{"3 months ago", Time(now.Add(-99 * Day)), "3 months ago"},
|
||||
{"1 year ago (1)", Time(now.Add(-365 * Day)), "1 year ago"},
|
||||
{"1 year ago (1)", Time(now.Add(-400 * Day)), "1 year ago"},
|
||||
{"2 years ago (1)", Time(now.Add(-548 * Day)), "2 years ago"},
|
||||
{"2 years ago (2)", Time(now.Add(-725 * Day)), "2 years ago"},
|
||||
{"2 years ago (3)", Time(now.Add(-800 * Day)), "2 years ago"},
|
||||
{"3 years ago", Time(now.Add(-3 * Year)), "3 years ago"},
|
||||
{"long ago", Time(now.Add(-LongTime)), "a long while ago"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestFuture(t *testing.T) {
|
||||
// Add a little time so that these things properly line up in
|
||||
// the future.
|
||||
now := time.Now().Add(time.Millisecond * 250)
|
||||
testList{
|
||||
{"now", Time(now), "now"},
|
||||
{"1 second from now", Time(now.Add(+1 * time.Second)), "1 second from now"},
|
||||
{"12 seconds from now", Time(now.Add(+12 * time.Second)), "12 seconds from now"},
|
||||
{"30 seconds from now", Time(now.Add(+30 * time.Second)), "30 seconds from now"},
|
||||
{"45 seconds from now", Time(now.Add(+45 * time.Second)), "45 seconds from now"},
|
||||
{"15 minutes from now", Time(now.Add(+15 * time.Minute)), "15 minutes from now"},
|
||||
{"2 hours from now", Time(now.Add(+2 * time.Hour)), "2 hours from now"},
|
||||
{"21 hours from now", Time(now.Add(+21 * time.Hour)), "21 hours from now"},
|
||||
{"1 day from now", Time(now.Add(+26 * time.Hour)), "1 day from now"},
|
||||
{"2 days from now", Time(now.Add(+49 * time.Hour)), "2 days from now"},
|
||||
{"3 days from now", Time(now.Add(+3 * Day)), "3 days from now"},
|
||||
{"1 week from now (1)", Time(now.Add(+7 * Day)), "1 week from now"},
|
||||
{"1 week from now (2)", Time(now.Add(+12 * Day)), "1 week from now"},
|
||||
{"2 weeks from now", Time(now.Add(+15 * Day)), "2 weeks from now"},
|
||||
{"1 month from now", Time(now.Add(+30 * Day)), "1 month from now"},
|
||||
{"1 year from now", Time(now.Add(+365 * Day)), "1 year from now"},
|
||||
{"2 years from now", Time(now.Add(+2 * Year)), "2 years from now"},
|
||||
{"a while from now", Time(now.Add(+LongTime)), "a long while from now"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
start := time.Time{}
|
||||
end := time.Unix(math.MaxInt64, math.MaxInt64)
|
||||
x := RelTime(start, end, "ago", "from now")
|
||||
if x != "a long while from now" {
|
||||
t.Errorf("Expected a long while from now, got %q", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomRelTime(t *testing.T) {
|
||||
now := time.Now().Add(time.Millisecond * 250)
|
||||
magnitudes := []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{Day - time.Second, "%d minutes %s", time.Minute},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{6 * Month, "%d weeks %s", Week},
|
||||
{Year, "%d months %s", Month},
|
||||
}
|
||||
customRelTime := func(then time.Time) string {
|
||||
return CustomRelTime(then, time.Now(), "ago", "from now", magnitudes)
|
||||
}
|
||||
testList{
|
||||
{"now", customRelTime(now), "now"},
|
||||
{"1 second from now", customRelTime(now.Add(+1 * time.Second)), "1 second from now"},
|
||||
{"12 seconds from now", customRelTime(now.Add(+12 * time.Second)), "12 seconds from now"},
|
||||
{"30 seconds from now", customRelTime(now.Add(+30 * time.Second)), "30 seconds from now"},
|
||||
{"45 seconds from now", customRelTime(now.Add(+45 * time.Second)), "45 seconds from now"},
|
||||
{"15 minutes from now", customRelTime(now.Add(+15 * time.Minute)), "15 minutes from now"},
|
||||
{"2 hours from now", customRelTime(now.Add(+2 * time.Hour)), "120 minutes from now"},
|
||||
{"21 hours from now", customRelTime(now.Add(+21 * time.Hour)), "1260 minutes from now"},
|
||||
{"1 day from now", customRelTime(now.Add(+26 * time.Hour)), "1 day from now"},
|
||||
{"2 days from now", customRelTime(now.Add(+49 * time.Hour)), "2 days from now"},
|
||||
{"3 days from now", customRelTime(now.Add(+3 * Day)), "3 days from now"},
|
||||
{"1 week from now (1)", customRelTime(now.Add(+7 * Day)), "1 week from now"},
|
||||
{"1 week from now (2)", customRelTime(now.Add(+12 * Day)), "1 week from now"},
|
||||
{"2 weeks from now", customRelTime(now.Add(+15 * Day)), "2 weeks from now"},
|
||||
{"1 month from now", customRelTime(now.Add(+30 * Day)), "4 weeks from now"},
|
||||
{"6 months from now", customRelTime(now.Add(+6*Month - time.Second)), "25 weeks from now"},
|
||||
{"1 year from now", customRelTime(now.Add(+365 * Day)), "12 months from now"},
|
||||
{"2 years from now", customRelTime(now.Add(+2 * Year)), "24 months from now"},
|
||||
{"a while from now", customRelTime(now.Add(+LongTime)), "444 months from now"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
language: go
|
||||
sudo: false
|
||||
install:
|
||||
- go get ./...
|
||||
go:
|
||||
- 1.4
|
||||
- tip
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
MIT License
|
||||
===========
|
||||
|
||||
Copyright (c) 2015, Greg Osuri
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# uilive [](https://godoc.org/github.com/gosuri/uilive) [](https://travis-ci.org/gosuri/uilive)
|
||||
|
||||
uilive is a go library for updating terminal output in realtime. It provides a buffered [io.Writer](https://golang.org/pkg/io/#Writer) that is flushed at a timed interval. uilive powers [uiprogress](https://github.com/gosuri/uiprogress).
|
||||
|
||||
## Usage Example
|
||||
|
||||
Calling `uilive.New()` will create a new writer. To start rendering, simply call `writer.Start()` and update the ui by writing to the `writer`. Full source for the below example is in [example/main.go](example/main.go).
|
||||
|
||||
```go
|
||||
writer := uilive.New()
|
||||
// start listening for updates and render
|
||||
writer.Start()
|
||||
|
||||
for i := 0; i <= 100; i++ {
|
||||
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
|
||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||
writer.Stop() // flush and stop rendering
|
||||
```
|
||||
|
||||
The above will render
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ go get -v github.com/gosuri/uilive
|
||||
```
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// Package uilive provides a writer that live updates the terminal. It provides a buffered io.Writer that is flushed at a timed interval.
|
||||
package uilive
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gosuri/uilive"
|
||||
)
|
||||
|
||||
func main() {
|
||||
writer := uilive.New()
|
||||
|
||||
// start listening for updates and render
|
||||
writer.Start()
|
||||
|
||||
for _, f := range []string{"Foo.zip", "Bar.iso"} {
|
||||
for i := 0; i <= 50; i++ {
|
||||
fmt.Fprintf(writer, "Downloading %s.. (%d/%d) GB\n", f, i, 50)
|
||||
time.Sleep(time.Millisecond * 25)
|
||||
}
|
||||
fmt.Fprintf(writer.Bypass(), "Downloaded %s\n", f)
|
||||
}
|
||||
|
||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||
writer.Stop() // flush and stop rendering
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package uilive_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gosuri/uilive"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
writer := uilive.New()
|
||||
|
||||
// start listening to updates and render
|
||||
writer.Start()
|
||||
|
||||
for i := 0; i <= 100; i++ {
|
||||
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
|
||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||
writer.Stop() // flush and stop rendering
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package uilive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ESC is the ASCII code for escape character
|
||||
const ESC = 27
|
||||
|
||||
// RefreshInterval is the default refresh interval to update the ui
|
||||
var RefreshInterval = time.Millisecond
|
||||
|
||||
// Out is the default output writer for the Writer
|
||||
var Out = os.Stdout
|
||||
|
||||
// ErrClosedPipe is the error returned when trying to writer is not listening
|
||||
var ErrClosedPipe = errors.New("uilive: read/write on closed pipe")
|
||||
|
||||
// FdWriter is a writer with a file descriptor.
|
||||
type FdWriter interface {
|
||||
io.Writer
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
// Writer is a buffered the writer that updates the terminal. The contents of writer will be flushed on a timed interval or when Flush is called.
|
||||
type Writer struct {
|
||||
// Out is the writer to write to
|
||||
Out io.Writer
|
||||
|
||||
// RefreshInterval is the time the UI sould refresh
|
||||
RefreshInterval time.Duration
|
||||
|
||||
ticker *time.Ticker
|
||||
tdone chan bool
|
||||
|
||||
buf bytes.Buffer
|
||||
mtx *sync.Mutex
|
||||
lineCount int
|
||||
}
|
||||
|
||||
type bypass struct {
|
||||
writer *Writer
|
||||
}
|
||||
|
||||
// New returns a new Writer with defaults
|
||||
func New() *Writer {
|
||||
return &Writer{
|
||||
Out: Out,
|
||||
RefreshInterval: RefreshInterval,
|
||||
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Flush writes to the out and resets the buffer. It should be called after the last call to Write to ensure that any data buffered in the Writer is written to output.
|
||||
// Any incomplete escape sequence at the end is considered complete for formatting purposes.
|
||||
// An error is returned if the contents of the buffer cannot be written to the underlying output stream
|
||||
func (w *Writer) Flush() error {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
|
||||
// do nothing is buffer is empty
|
||||
if len(w.buf.Bytes()) == 0 {
|
||||
return nil
|
||||
}
|
||||
w.clearLines()
|
||||
|
||||
lines := 0
|
||||
for _, b := range w.buf.Bytes() {
|
||||
if b == '\n' {
|
||||
lines++
|
||||
}
|
||||
}
|
||||
w.lineCount = lines
|
||||
_, err := w.Out.Write(w.buf.Bytes())
|
||||
w.buf.Reset()
|
||||
return err
|
||||
}
|
||||
|
||||
// Start starts the listener in a non-blocking manner
|
||||
func (w *Writer) Start() {
|
||||
if w.ticker == nil {
|
||||
w.ticker = time.NewTicker(w.RefreshInterval)
|
||||
w.tdone = make(chan bool, 1)
|
||||
}
|
||||
|
||||
go w.Listen()
|
||||
}
|
||||
|
||||
// Stop stops the listener that updates the terminal
|
||||
func (w *Writer) Stop() {
|
||||
w.Flush()
|
||||
close(w.tdone)
|
||||
}
|
||||
|
||||
// Listen listens for updates to the writer's buffer and flushes to the out provided. It blocks the runtime.
|
||||
func (w *Writer) Listen() {
|
||||
for {
|
||||
select {
|
||||
case <-w.ticker.C:
|
||||
if w.ticker != nil {
|
||||
w.Flush()
|
||||
}
|
||||
case <-w.tdone:
|
||||
w.mtx.Lock()
|
||||
w.ticker.Stop()
|
||||
w.ticker = nil
|
||||
w.mtx.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write save the contents of b to its buffers. The only errors returned are ones encountered while writing to the underlying buffer.
|
||||
func (w *Writer) Write(b []byte) (n int, err error) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
return w.buf.Write(b)
|
||||
}
|
||||
|
||||
// Bypass creates an io.Writer which allows non-buffered output to be written to the underlying output
|
||||
func (w *Writer) Bypass() io.Writer {
|
||||
return &bypass{writer: w}
|
||||
}
|
||||
|
||||
func (b *bypass) Write(p []byte) (n int, err error) {
|
||||
b.writer.mtx.Lock()
|
||||
defer b.writer.mtx.Unlock()
|
||||
|
||||
b.writer.clearLines()
|
||||
b.writer.lineCount = 0
|
||||
return b.writer.Out.Write(p)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// +build !windows
|
||||
|
||||
package uilive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (w *Writer) clearLines() {
|
||||
for i := 0; i < w.lineCount; i++ {
|
||||
fmt.Fprintf(w.Out, "%c[2K", ESC) // clear the line
|
||||
fmt.Fprintf(w.Out, "%c[%dA", ESC, 1) // move the cursor up
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package uilive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
w := New()
|
||||
b := &bytes.Buffer{}
|
||||
w.Out = b
|
||||
w.Start()
|
||||
for i := 0; i < 2; i++ {
|
||||
fmt.Fprintln(w, "foo")
|
||||
}
|
||||
w.Stop()
|
||||
fmt.Fprintln(b, "bar")
|
||||
|
||||
want := "foo\nfoo\nbar\n"
|
||||
if b.String() != want {
|
||||
t.Fatalf("want %q, got %q", want, b.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// +build windows
|
||||
|
||||
package uilive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattn/go-isatty"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
var (
|
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
|
||||
)
|
||||
|
||||
type short int16
|
||||
type dword uint32
|
||||
type word uint16
|
||||
|
||||
type coord struct {
|
||||
x short
|
||||
y short
|
||||
}
|
||||
|
||||
type smallRect struct {
|
||||
left short
|
||||
top short
|
||||
right short
|
||||
bottom short
|
||||
}
|
||||
|
||||
type consoleScreenBufferInfo struct {
|
||||
size coord
|
||||
cursorPosition coord
|
||||
attributes word
|
||||
window smallRect
|
||||
maximumWindowSize coord
|
||||
}
|
||||
|
||||
func (w *Writer) clearLines() {
|
||||
f, ok := w.Out.(FdWriter)
|
||||
if ok && !isatty.IsTerminal(f.Fd()) {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
for i := 0; i < w.lineCount; i++ {
|
||||
fmt.Fprintf(w.Out, "%c[%dA", ESC, 0) // move the cursor up
|
||||
fmt.Fprintf(w.Out, "%c[2K\r", ESC) // clear the line
|
||||
}
|
||||
return
|
||||
}
|
||||
fd := f.Fd()
|
||||
var csbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi)))
|
||||
|
||||
for i := 0; i < w.lineCount; i++ {
|
||||
// move the cursor up
|
||||
csbi.cursorPosition.y--
|
||||
procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&csbi.cursorPosition))))
|
||||
// clear the line
|
||||
cursor := coord{
|
||||
x: csbi.window.left,
|
||||
y: csbi.window.top + csbi.cursorPosition.y,
|
||||
}
|
||||
var count, w dword
|
||||
count = dword(csbi.size.x)
|
||||
procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
language: go
|
||||
sudo: false
|
||||
install:
|
||||
- go get ./...
|
||||
go:
|
||||
- 1.4
|
||||
- tip
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
MIT License
|
||||
===========
|
||||
|
||||
Copyright (c) 2015, Greg Osuri
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
test:
|
||||
@go test -race .
|
||||
@go test -race ./util/strutil
|
||||
|
||||
examples:
|
||||
go run -race example/full/full.go
|
||||
go run -race example/incr/incr.go
|
||||
go run -race example/multi/multi.go
|
||||
go run -race example/simple/simple.go
|
||||
|
||||
.PHONY: test examples
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
# uiprogress [](https://godoc.org/github.com/gosuri/uiprogress) [](https://travis-ci.org/gosuri/uiprogress)
|
||||
|
||||
A Go library to render progress bars in terminal applications. It provides a set of flexible features with a customizable API.
|
||||
|
||||

|
||||
|
||||
Progress bars improve readability for terminal applications with long outputs by providing a concise feedback loop.
|
||||
|
||||
## Features
|
||||
|
||||
* __Multiple Bars__: uiprogress can render multiple progress bars that can be tracked concurrently
|
||||
* __Dynamic Addition__: Add additional progress bars any time, even after the progress tracking has started
|
||||
* __Prepend and Append Functions__: Append or prepend completion percent and time elapsed to the progress bars
|
||||
* __Custom Decorator Functions__: Add custom functions around the bar along with helper functions
|
||||
|
||||
## Usage
|
||||
|
||||
To start listening for progress bars, call `uiprogress.Start()` and add a progress bar using `uiprogress.AddBar(total int)`. Update the progress using `bar.Incr()` or `bar.Set(n int)`. Full source code for the below example is available at [example/simple/simple.go](example/simple/simple.go)
|
||||
|
||||
```go
|
||||
uiprogress.Start() // start rendering
|
||||
bar := uiprogress.AddBar(100) // Add a new bar
|
||||
|
||||
// optionally, append and prepend completion and elapsed time
|
||||
bar.AppendCompleted()
|
||||
bar.PrependElapsed()
|
||||
|
||||
for bar.Incr() {
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
}
|
||||
```
|
||||
|
||||
This will render the below in the terminal
|
||||
|
||||

|
||||
|
||||
### Using Custom Decorators
|
||||
|
||||
You can also add a custom decorator function in addition to default `bar.AppendCompleted()` and `bar.PrependElapsed()` decorators. The below example tracks the current step for an application deploy progress. Source code for the below example is available at [example/full/full.go](example/full/full.go)
|
||||
|
||||
```go
|
||||
var steps = []string{"downloading source", "installing deps", "compiling", "packaging", "seeding database", "deploying", "staring servers"}
|
||||
bar := uiprogress.AddBar(len(steps))
|
||||
|
||||
// prepend the current step to the bar
|
||||
bar.PrependFunc(func(b *uiprogress.Bar) string {
|
||||
return "app: " + steps[b.Current()-1]
|
||||
})
|
||||
|
||||
for bar.Incr() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Multiple bars
|
||||
|
||||
You can add multiple bars using `uiprogress.AddBar(n)`. The below example demonstrates updating multiple bars concurrently and adding a new bar later in the pipeline. Source for this example is available at [example/multi/multi.go](example/multi/multi.go)
|
||||
|
||||
```go
|
||||
waitTime := time.Millisecond * 100
|
||||
uiprogress.Start()
|
||||
|
||||
// start the progress bars in go routines
|
||||
var wg sync.WaitGroup
|
||||
|
||||
bar1 := uiprogress.AddBar(20).AppendCompleted().PrependElapsed()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for bar1.Incr() {
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}()
|
||||
|
||||
bar2 := uiprogress.AddBar(40).AppendCompleted().PrependElapsed()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for bar2.Incr() {
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
bar3 := uiprogress.AddBar(20).PrependElapsed().AppendCompleted()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 1; i <= bar3.Total; i++ {
|
||||
bar3.Set(i)
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for all the go routines to finish
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
This will produce
|
||||
|
||||

|
||||
|
||||
### `Incr` counter
|
||||
|
||||
[Bar.Incr()](https://godoc.org/github.com/gosuri/uiprogress#Bar.Incr) is an atomic counter and can be used as a general tracker, making it ideal for tracking progress of work fanned out to a lots of go routines. The source code for the below example is available at [example/incr/incr.go](example/incr/incr.go)
|
||||
|
||||
```go
|
||||
runtime.GOMAXPROCS(runtime.NumCPU()) // use all available cpu cores
|
||||
|
||||
// create a new bar and prepend the task progress to the bar and fanout into 1k go routines
|
||||
count := 1000
|
||||
bar := uiprogress.AddBar(count).AppendCompleted().PrependElapsed()
|
||||
bar.PrependFunc(func(b *uiprogress.Bar) string {
|
||||
return fmt.Sprintf("Task (%d/%d)", b.Current(), count)
|
||||
})
|
||||
|
||||
uiprogress.Start()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// fanout into go routines
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
|
||||
bar.Incr()
|
||||
}()
|
||||
}
|
||||
time.Sleep(time.Second) // wait for a second for all the go routines to finish
|
||||
wg.Wait()
|
||||
uiprogress.Stop()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ go get -v github.com/gosuri/uiprogress
|
||||
```
|
||||
## Todos
|
||||
|
||||
- [ ] Resize bars and decorators by auto detecting window's dimensions
|
||||
- [ ] Handle more progress bars than vertical screen allows
|
||||
|
||||
## License
|
||||
|
||||
uiprogress is released under the MIT License. See [LICENSE](https://github.com/gosuri/uiprogress/blob/master/LICENSE).
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package uiprogress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gosuri/uiprogress/util/strutil"
|
||||
)
|
||||
|
||||
var (
|
||||
// Fill is the default character representing completed progress
|
||||
Fill byte = '='
|
||||
|
||||
// Head is the default character that moves when progress is updated
|
||||
Head byte = '>'
|
||||
|
||||
// Empty is the default character that represents the empty progress
|
||||
Empty byte = '-'
|
||||
|
||||
// LeftEnd is the default character in the left most part of the progress indicator
|
||||
LeftEnd byte = '['
|
||||
|
||||
// RightEnd is the default character in the right most part of the progress indicator
|
||||
RightEnd byte = ']'
|
||||
|
||||
// Width is the default width of the progress bar
|
||||
Width = 70
|
||||
|
||||
// ErrMaxCurrentReached is error when trying to set current value that exceeds the total value
|
||||
ErrMaxCurrentReached = errors.New("errors: current value is greater total value")
|
||||
)
|
||||
|
||||
// Bar represents a progress bar
|
||||
type Bar struct {
|
||||
// Total of the total for the progress bar
|
||||
Total int
|
||||
|
||||
// LeftEnd is character in the left most part of the progress indicator. Defaults to '['
|
||||
LeftEnd byte
|
||||
|
||||
// RightEnd is character in the right most part of the progress indicator. Defaults to ']'
|
||||
RightEnd byte
|
||||
|
||||
// Fill is the character representing completed progress. Defaults to '='
|
||||
Fill byte
|
||||
|
||||
// Head is the character that moves when progress is updated. Defaults to '>'
|
||||
Head byte
|
||||
|
||||
// Empty is the character that represents the empty progress. Default is '-'
|
||||
Empty byte
|
||||
|
||||
// TimeStated is time progress began
|
||||
TimeStarted time.Time
|
||||
|
||||
// Width is the width of the progress bar
|
||||
Width int
|
||||
|
||||
// timeElased is the time elapsed for the progress
|
||||
timeElapsed time.Duration
|
||||
current int
|
||||
|
||||
mtx *sync.RWMutex
|
||||
|
||||
appendFuncs []DecoratorFunc
|
||||
prependFuncs []DecoratorFunc
|
||||
}
|
||||
|
||||
// DecoratorFunc is a function that can be prepended and appended to the progress bar
|
||||
type DecoratorFunc func(b *Bar) string
|
||||
|
||||
// NewBar returns a new progress bar
|
||||
func NewBar(total int) *Bar {
|
||||
return &Bar{
|
||||
Total: total,
|
||||
Width: Width,
|
||||
LeftEnd: LeftEnd,
|
||||
RightEnd: RightEnd,
|
||||
Head: Head,
|
||||
Fill: Fill,
|
||||
Empty: Empty,
|
||||
|
||||
mtx: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current count of the bar. It returns ErrMaxCurrentReached when trying n exceeds the total value. This is atomic operation and concurancy safe.
|
||||
func (b *Bar) Set(n int) error {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
if n > b.Total {
|
||||
return ErrMaxCurrentReached
|
||||
}
|
||||
b.current = n
|
||||
return nil
|
||||
}
|
||||
|
||||
// Incr increments the current value by 1, time elapsed to current time and returns true. It returns false if the cursor has reached or exceeds total value.
|
||||
func (b *Bar) Incr() bool {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
n := b.current + 1
|
||||
if n > b.Total {
|
||||
return false
|
||||
}
|
||||
var t time.Time
|
||||
if b.TimeStarted == t {
|
||||
b.TimeStarted = time.Now()
|
||||
}
|
||||
b.timeElapsed = time.Since(b.TimeStarted)
|
||||
b.current = n
|
||||
return true
|
||||
}
|
||||
|
||||
// Current returns the current progress of the bar
|
||||
func (b *Bar) Current() int {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
return b.current
|
||||
}
|
||||
|
||||
// AppendFunc runs the decorator function and renders the output on the right of the progress bar
|
||||
func (b *Bar) AppendFunc(f DecoratorFunc) *Bar {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
b.appendFuncs = append(b.appendFuncs, f)
|
||||
return b
|
||||
}
|
||||
|
||||
// AppendCompleted appends the completion percent to the progress bar
|
||||
func (b *Bar) AppendCompleted() *Bar {
|
||||
b.AppendFunc(func(b *Bar) string {
|
||||
return b.CompletedPercentString()
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// AppendElapsed appends the time elapsed the be progress bar
|
||||
func (b *Bar) AppendElapsed() *Bar {
|
||||
b.AppendFunc(func(b *Bar) string {
|
||||
return strutil.PadLeft(b.TimeElapsedString(), 5, ' ')
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// PrependFunc runs decorator function and render the output left the progress bar
|
||||
func (b *Bar) PrependFunc(f DecoratorFunc) *Bar {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
b.prependFuncs = append(b.prependFuncs, f)
|
||||
return b
|
||||
}
|
||||
|
||||
// PrependCompleted prepends the precent completed to the progress bar
|
||||
func (b *Bar) PrependCompleted() *Bar {
|
||||
b.PrependFunc(func(b *Bar) string {
|
||||
return b.CompletedPercentString()
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// PrependElapsed prepends the time elapsed to the begining of the bar
|
||||
func (b *Bar) PrependElapsed() *Bar {
|
||||
b.PrependFunc(func(b *Bar) string {
|
||||
return strutil.PadLeft(b.TimeElapsedString(), 5, ' ')
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Bytes returns the byte presentation of the progress bar
|
||||
func (b *Bar) Bytes() []byte {
|
||||
completedWidth := int(float64(b.Width) * (b.CompletedPercent() / 100.00))
|
||||
|
||||
// add fill and empty bits
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < completedWidth; i++ {
|
||||
buf.WriteByte(b.Fill)
|
||||
}
|
||||
for i := 0; i < b.Width-completedWidth; i++ {
|
||||
buf.WriteByte(b.Empty)
|
||||
}
|
||||
|
||||
// set head bit
|
||||
pb := buf.Bytes()
|
||||
if completedWidth > 0 && completedWidth < b.Width {
|
||||
pb[completedWidth-1] = b.Head
|
||||
}
|
||||
|
||||
// set left and right ends bits
|
||||
pb[0], pb[len(pb)-1] = b.LeftEnd, b.RightEnd
|
||||
|
||||
// render append functions to the right of the bar
|
||||
for _, f := range b.appendFuncs {
|
||||
pb = append(pb, ' ')
|
||||
pb = append(pb, []byte(f(b))...)
|
||||
}
|
||||
|
||||
// render prepend functions to the left of the bar
|
||||
for _, f := range b.prependFuncs {
|
||||
args := []byte(f(b))
|
||||
args = append(args, ' ')
|
||||
pb = append(args, pb...)
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// String returns the string representation of the bar
|
||||
func (b *Bar) String() string {
|
||||
return string(b.Bytes())
|
||||
}
|
||||
|
||||
// CompletedPercent return the percent completed
|
||||
func (b *Bar) CompletedPercent() float64 {
|
||||
return (float64(b.Current()) / float64(b.Total)) * 100.00
|
||||
}
|
||||
|
||||
// CompletedPercentString returns the formatted string representation of the completed percent
|
||||
func (b *Bar) CompletedPercentString() string {
|
||||
return fmt.Sprintf("%3.f%%", b.CompletedPercent())
|
||||
}
|
||||
|
||||
// TimeElapsed returns the time elapsed
|
||||
func (b *Bar) TimeElapsed() time.Duration {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
return b.timeElapsed
|
||||
}
|
||||
|
||||
// TimeElapsedString returns the formatted string represenation of the time elapsed
|
||||
func (b *Bar) TimeElapsedString() string {
|
||||
return strutil.PrettyTime(b.TimeElapsed())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue