mirror of https://github.com/schollz/croc.git
470 lines
10 KiB
Go
470 lines
10 KiB
Go
package utils
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"compress/flate"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cespare/xxhash"
|
|
"github.com/kalafut/imohash"
|
|
"github.com/schollz/mnemonicode"
|
|
)
|
|
|
|
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) (hash256 []byte, err error) {
|
|
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)
|
|
case "xxhash":
|
|
return XXHashFile(fname)
|
|
}
|
|
err = fmt.Errorf("unspecified algorithm")
|
|
return
|
|
}
|
|
|
|
// MD5HashFile returns MD5 hash
|
|
func MD5HashFile(fname string) (hash256 []byte, err error) {
|
|
f, err := os.Open(fname)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
h := md5.New()
|
|
if _, err = io.Copy(h, f); err != nil {
|
|
return
|
|
}
|
|
|
|
hash256 = h.Sum(nil)
|
|
return
|
|
}
|
|
|
|
// IMOHashFile returns imohash
|
|
func IMOHashFile(fname string) (hash []byte, err error) {
|
|
b, err := imohash.SumFile(fname)
|
|
hash = b[:]
|
|
return
|
|
}
|
|
|
|
var imofull = imohash.NewCustom(0, 0)
|
|
|
|
// 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) (hash256 []byte, err error) {
|
|
f, err := os.Open(fname)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
h := xxhash.New()
|
|
if _, err = io.Copy(h, f); err != nil {
|
|
return
|
|
}
|
|
|
|
hash256 = h.Sum(nil)
|
|
return
|
|
}
|
|
|
|
// SHA256 returns sha256 sum
|
|
func SHA256(s string) string {
|
|
sha := sha256.New()
|
|
sha.Write([]byte(s))
|
|
return hex.EncodeToString(sha.Sum(nil))
|
|
}
|
|
|
|
// PublicIP returns public ip address
|
|
func PublicIP() (ip string, err error) {
|
|
resp, err := http.Get("https://canhazip.com")
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ip = strings.TrimSpace(string(bodyBytes))
|
|
}
|
|
return
|
|
}
|
|
|
|
// LocalIP returns local ip address
|
|
func LocalIP() string {
|
|
conn, err := net.Dial("udp", "8.8.8.8:80")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
|
|
|
return localAddr.IP.String()
|
|
}
|
|
|
|
func 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.Fatalf("%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.Fatalln(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.Fatalln(err)
|
|
}
|
|
if info.Mode().IsRegular() {
|
|
f1, err := os.Open(path)
|
|
if err != nil {
|
|
log.Fatalln(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.Fatalln(err)
|
|
}
|
|
if _, err := io.Copy(w1, f1); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "\r\033[2K")
|
|
fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "\n")
|
|
return nil
|
|
}
|
|
|
|
func UnzipDirectory(destination string, source string) error {
|
|
archive, err := zip.OpenReader(source)
|
|
if err != nil {
|
|
log.Fatalln(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)
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(filePath, os.ModePerm)
|
|
continue
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
fileInArchive, err := f.Open()
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
dstFile.Close()
|
|
fileInArchive.Close()
|
|
}
|
|
fmt.Fprintf(os.Stderr, "\n")
|
|
return nil
|
|
}
|