commit 95726d9e455455cf77b9344bad80c2003a2fb913 Author: TZGyn Date: Mon Jul 20 10:12:46 2026 +0800 Initial commit: minimal relay server and CLI structure Relay pairs connections by room name and pipes bytes between them, with no password/PAKE handshake at the relay layer — codephrase-based encryption is left to the client side of the transfer. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e907f3a --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/tzgyn/share + +go 1.25.0 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 + github.com/kalafut/imohash v1.1.1 + github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b + github.com/minio/highwayhash v1.0.4 + github.com/schollz/cli/v2 v2.2.1 + github.com/schollz/logger v1.2.0 + github.com/schollz/pake/v3 v3.1.1 + github.com/schollz/progressbar/v3 v3.19.1 + golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect + github.com/twmb/murmur3 v1.1.8 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0d56269 --- /dev/null +++ b/go.sum @@ -0,0 +1,58 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kalafut/imohash v1.1.1 h1:G/HYtKgteQSVU96LidSJEbUGoZOMiBcuXYxbeb2W9e4= +github.com/kalafut/imohash v1.1.1/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4= +github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI= +github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/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/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU= +github.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s= +github.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho= +github.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM= +github.com/schollz/pake/v3 v3.1.1 h1:lyoU5uNQ3thfjEzrahgxWWBm6+pbI1F2KAZ3gs6LIV8= +github.com/schollz/pake/v3 v3.1.1/go.mod h1:420+m3AakXcS0n7Uwc7eRs2CosQ2YfE/vKcIkilvqZc= +github.com/schollz/progressbar/v3 v3.19.1 h1:iv8BgwOvdML/S3p84uBpy/IMigv4U9594vPZYa2EdrU= +github.com/schollz/progressbar/v3 v3.19.1/go.mod h1:LFL7jqimKxfhero4K1eCkUr/6R39AgQeiPCJtlTWIW8= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U= +github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw= +github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= +github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8411801 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/tzgyn/share/src/cli" + "github.com/tzgyn/share/src/utils" +) + +func main() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := cli.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } + utils.RemoveMarkedFiles() + os.Exit(0) + }() + + <-sigs + utils.RemoveMarkedFiles() + os.Exit(0) +} diff --git a/share b/share new file mode 100755 index 0000000..e746ea0 Binary files /dev/null and b/share differ diff --git a/src/cli/cli.go b/src/cli/cli.go new file mode 100644 index 0000000..8d226cf --- /dev/null +++ b/src/cli/cli.go @@ -0,0 +1,194 @@ +package cli + +import ( + "fmt" + "os" + "runtime" + "strconv" + "strings" + "time" + + "github.com/schollz/cli/v2" + "github.com/tzgyn/share/src/models" + "github.com/tzgyn/share/src/tcp" + log "github.com/schollz/logger" +) + +var Version string + +func Run() (err error) { + runtime.GOMAXPROCS(runtime.NumCPU()) + + app := cli.NewApp() + app.Name = "share" + if Version == "" { + Version = "v0.1.0" + } + app.Version = Version + app.Compiled = time.Now() + app.Usage = "easily and securely transfer stuff from one computer to another" + app.UsageText = `share [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder] + + USAGE EXAMPLES: + Send a file: + share send file.txt + + Send multiple files: + share send file1.txt file2.txt file3.txt + + Send everything in a folder: + share send example-folder-name + + Send a file with a custom code: + share send --code secret-code file.txt + + Receive a file using code: + share secret-code` + app.Commands = []*cli.Command{ + { + Name: "send", + Usage: "send file(s), or folder (see options with share 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 (at least 6 characters)"}, + &cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5, highway)"}, + &cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"}, + &cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"}, + &cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"}, + &cli.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"}, + &cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"}, + &cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"}, + &cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show receive code as a qrcode"}, + &cli.StringFlag{Name: "exclude", Value: "", Usage: "exclude files if they contain any of the comma separated strings"}, + &cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}}, + &cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}}, + }, + HelpName: "share send", + Action: send, + }, + { + Name: "relay", + Usage: "start your own relay (optional)", + Description: "start relay", + HelpName: "share 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", EnvVars: []string{"SHARE_PORTS"}}, + &cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay", EnvVars: []string{"SHARE_PORT"}}, + &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.BoolFlag{Name: "quiet", Usage: "disable all output"}, + &cli.BoolFlag{Name: "disable-clipboard", Usage: "disable copy to clipboard"}, + &cli.BoolFlag{Name: "extended-clipboard", Usage: "copy full command with secret as env variable to clipboard"}, + &cli.StringFlag{Name: "multicast", Value: "239.255.255.250", Usage: "multicast address to use for local discovery"}, + &cli.StringFlag{Name: "curve", Value: "p256", Usage: "choose an encryption curve"}, + &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{"SHARE_RELAY"}}, + &cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"SHARE_RELAY6"}}, + &cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"}, + &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 { + return receive(c) + } + + return app.Run(os.Args) +} + +func setDebugLevel(c *cli.Context) { + if c.Bool("quiet") { + log.SetLevel("error") + } else if c.Bool("debug") { + log.SetLevel("debug") + log.Debug("debug mode on") + } else { + log.SetLevel("info") + } +} + +func parseRelayPorts(portsFlag string) []string { + var ports []string + for _, p := range strings.Split(portsFlag, ",") { + if p = strings.TrimSpace(p); p != "" { + ports = append(ports, p) + } + } + return ports +} + +func send(c *cli.Context) (err error) { + setDebugLevel(c) + fmt.Println("Send functionality not yet implemented in this fork.") + fmt.Println("This is a placeholder for the send command.") + return nil +} + +func receive(c *cli.Context) (err error) { + setDebugLevel(c) + fmt.Println("Receive functionality not yet implemented in this fork.") + fmt.Println("This is a placeholder for the receive command.") + return nil +} + +func relay(c *cli.Context) (err error) { + log.Infof("starting share relay version %v", Version) + debugString := "info" + if c.Bool("debug") { + debugString = "debug" + } + host := c.String("host") + var ports []string + + if c.IsSet("ports") { + ports = parseRelayPorts(c.String("ports")) + } else { + portString := c.Int("port") + if portString == 0 { + portString = 9009 + } + transfersString := c.Int("transfers") + if transfersString == 0 { + transfersString = 4 + } + ports = make([]string, transfersString) + for i := range ports { + ports[i] = strconv.Itoa(portString + i) + } + } + if len(ports) < 2 { + return fmt.Errorf("relay requires at least two ports; specify --ports with two or more ports or set --transfers to 2+") + } + + for _, port := range ports[1:] { + go func(portStr string) { + err := tcp.Run(debugString, host, portStr) + if err != nil { + panic(err) + } + }(port) + } + return tcp.Run(debugString, host, ports[0]) +} diff --git a/src/comm/comm.go b/src/comm/comm.go new file mode 100644 index 0000000..1dd157b --- /dev/null +++ b/src/comm/comm.go @@ -0,0 +1,200 @@ +package comm + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/url" + "strings" + "time" + + "github.com/magisterquis/connectproxy" + "github.com/tzgyn/share/src/utils" + log "github.com/schollz/logger" + "golang.org/x/net/proxy" +) + +var Socks5Proxy = "" +var HttpProxy = "" + +var MAGIC_BYTES = []byte("croc") + +const maxReadMessageSize = 64 * 1024 * 1024 + +const messageBodyReadTimeout = 10 * time.Minute + +type Comm struct { + connection net.Conn +} + +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 + 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 + 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 +} + +func New(c net.Conn) *Comm { + if err := c.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil { + log.Warnf("error setting read deadline: %v", err) + } + if err := c.SetDeadline(time.Now().Add(3 * time.Hour)); err != nil { + log.Warnf("error setting overall deadline: %v", err) + } + if err := c.SetWriteDeadline(time.Now().Add(3 * time.Hour)); err != nil { + log.Errorf("error setting write deadline: %v", err) + } + comm := new(Comm) + comm.connection = c + return comm +} + +func (c *Comm) Connection() net.Conn { + return c.connection +} + +func (c *Comm) Close() { + if err := c.connection.Close(); err != nil { + if errors.Is(err, net.ErrClosed) { + return + } + 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) { + if err = c.connection.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil { + log.Warnf("error setting read deadline: %v", err) + } + if err := c.connection.SetDeadline(time.Time{}); err != nil { + log.Warnf("failed to clear deadline: %v", err) + } + + 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 + } + + header = make([]byte, 4) + _, err = io.ReadFull(c.connection, header) + if err != nil { + log.Debugf("initial read error: %v", err) + return + } + + var numBytesUint32 uint32 + rbuf := bytes.NewReader(header) + err = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32) + if err != nil { + err = fmt.Errorf("binary.Read failed: %w", err) + log.Debug(err.Error()) + return + } + if numBytesUint32 > uint32(maxReadMessageSize) { + err = fmt.Errorf("message too large: %d > %d", numBytesUint32, maxReadMessageSize) + log.Debug(err.Error()) + return + } + numBytes = int(numBytesUint32) + + if err = c.connection.SetReadDeadline(time.Now().Add(messageBodyReadTimeout)); 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 +} + +func (c *Comm) Send(message []byte) (err error) { + _, err = c.Write(message) + return +} + +func (c *Comm) Receive() (b []byte, err error) { + b, _, _, err = c.Read() + return +} diff --git a/src/crypt/crypt.go b/src/crypt/crypt.go new file mode 100644 index 0000000..e03511f --- /dev/null +++ b/src/crypt/crypt.go @@ -0,0 +1,102 @@ +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" +) + +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) + 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 +} + +func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) { + 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 +} + +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 +} + +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) + 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 +} + +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) + } + encrypted = aead.Seal(nonce, nonce, plaintext, nil) + return +} + +func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []byte, err error) { + if len(encryptedMsg) < aead.NonceSize() { + err = fmt.Errorf("ciphertext too short") + return + } + nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():] + plaintext, err = aead.Open(nil, nonce, ciphertext, nil) + return +} diff --git a/src/mnemonicode/mnemonicode.go b/src/mnemonicode/mnemonicode.go new file mode 100644 index 0000000..86518e0 --- /dev/null +++ b/src/mnemonicode/mnemonicode.go @@ -0,0 +1,49 @@ +package mnemonicode + +const base = 1626 + +func WordsRequired(length int) int { + return ((length + 1) * 3) / 4 +} + +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 +} diff --git a/src/mnemonicode/wordlist.go b/src/mnemonicode/wordlist.go new file mode 100644 index 0000000..2be8b93 --- /dev/null +++ b/src/mnemonicode/wordlist.go @@ -0,0 +1,289 @@ +package mnemonicode + +const WordListVersion = "0.7" + +var wordMap = make(map[string]int, len(WordList)) + +func init() { + for i, w := range WordList { + wordMap[w] = i + } +} + +const longestWord = 7 + +var WordList = []string{ + "academy", "acrobat", "active", "actor", "adam", "admiral", + "adrian", "africa", "agenda", "agent", "airline", "airport", + "aladdin", "alarm", "alaska", "albert", "albino", "album", + "alcohol", "alex", "algebra", "alibi", "alice", "alien", + "alpha", "alpine", "amadeus", "amanda", "amazon", "amber", + "america", "amigo", "analog", "anatomy", "angel", "animal", + "antenna", "antonio", "apollo", "april", "archive", "arctic", + "arizona", "arnold", "aroma", "arthur", "artist", "asia", + "aspect", "aspirin", "athena", "athlete", "atlas", "audio", + "august", "austria", "axiom", "aztec", "balance", "ballad", + "banana", "bandit", "banjo", "barcode", "baron", "basic", + "battery", "belgium", "berlin", "bermuda", "bernard", "bikini", + "binary", "bingo", "biology", "block", "blonde", "bonus", + "boris", "boston", "boxer", "brandy", "bravo", "brazil", + "bronze", "brown", "bruce", "bruno", "burger", "burma", + "cabinet", "cactus", "cafe", "cairo", "cake", "calypso", + "camel", "camera", "campus", "canada", "canal", "cannon", + "canoe", "cantina", "canvas", "canyon", "capital", "caramel", + "caravan", "carbon", "cargo", "carlo", "carol", "carpet", + "cartel", "casino", "castle", "castro", "catalog", "caviar", + "cecilia", "cement", "center", "century", "ceramic", "chamber", + "chance", "change", "chaos", "charlie", "charm", "charter", + "chef", "chemist", "cherry", "chess", "chicago", "chicken", + "chief", "china", "cigar", "cinema", "circus", "citizen", + "city", "clara", "classic", "claudia", "clean", "client", + "climax", "clinic", "clock", "club", "cobra", "coconut", + "cola", "collect", "colombo", "colony", "color", "combat", + "comedy", "comet", "command", "compact", "company", "complex", + "concept", "concert", "connect", "consul", "contact", "context", + "contour", "control", "convert", "copy", "corner", "corona", + "correct", "cosmos", "couple", "courage", "cowboy", "craft", + "crash", "credit", "cricket", "critic", "crown", "crystal", + "cuba", "culture", "dallas", "dance", "daniel", "david", + "decade", "decimal", "deliver", "delta", "deluxe", "demand", + "demo", "denmark", "derby", "design", "detect", "develop", + "diagram", "dialog", "diamond", "diana", "diego", "diesel", + "diet", "digital", "dilemma", "diploma", "direct", "disco", + "disney", "distant", "doctor", "dollar", "dominic", "domino", + "donald", "dragon", "drama", "dublin", "duet", "dynamic", + "east", "ecology", "economy", "edgar", "egypt", "elastic", + "elegant", "element", "elite", "elvis", "email", "energy", + "engine", "english", "episode", "equator", "escort", "ethnic", + "europe", "everest", "evident", "exact", "example", "exit", + "exotic", "export", "express", "extra", "fabric", "factor", + "falcon", "family", "fantasy", "fashion", "fiber", "fiction", + "fidel", "fiesta", "figure", "film", "filter", "final", + "finance", "finish", "finland", "flash", "florida", "flower", + "fluid", "flute", "focus", "ford", "forest", "formal", + "format", "formula", "fortune", "forum", "fragile", "france", + "frank", "friend", "frozen", "future", "gabriel", "galaxy", + "gallery", "gamma", "garage", "garden", "garlic", "gemini", + "general", "genetic", "genius", "germany", "global", "gloria", + "golf", "gondola", "gong", "good", "gordon", "gorilla", + "grand", "granite", "graph", "green", "group", "guide", + "guitar", "guru", "hand", "happy", "harbor", "harmony", + "harvard", "havana", "hawaii", "helena", "hello", "henry", + "hilton", "history", "horizon", "hotel", "human", "humor", + "icon", "idea", "igloo", "igor", "image", "impact", + "import", "index", "india", "indigo", "input", "insect", + "instant", "iris", "italian", "jacket", "jacob", "jaguar", + "janet", "japan", "jargon", "jazz", "jeep", "john", + "joker", "jordan", "jumbo", "june", "jungle", "junior", + "jupiter", "karate", "karma", "kayak", "kermit", "kilo", + "king", "koala", "korea", "labor", "lady", "lagoon", + "laptop", "laser", "latin", "lava", "lecture", "left", + "legal", "lemon", "level", "lexicon", "liberal", "libra", + "limbo", "limit", "linda", "linear", "lion", "liquid", + "liter", "little", "llama", "lobby", "lobster", "local", + "logic", "logo", "lola", "london", "lotus", "lucas", + "lunar", "machine", "macro", "madam", "madonna", "madrid", + "maestro", "magic", "magnet", "magnum", "major", "mama", + "mambo", "manager", "mango", "manila", "marco", "marina", + "market", "mars", "martin", "marvin", "master", "matrix", + "maximum", "media", "medical", "mega", "melody", "melon", + "memo", "mental", "mentor", "menu", "mercury", "message", + "metal", "meteor", "meter", "method", "metro", "mexico", + "miami", "micro", "million", "mineral", "minimum", "minus", + "minute", "miracle", "mirage", "miranda", "mister", "mixer", + "mobile", "model", "modem", "modern", "modular", "moment", + "monaco", "monica", "monitor", "mono", "monster", "montana", + "morgan", "motel", "motif", "motor", "mozart", "multi", + "museum", "music", "mustang", "natural", "neon", "nepal", + "neptune", "nerve", "neutral", "nevada", "news", "ninja", + "nirvana", "normal", "nova", "novel", "nuclear", "numeric", + "nylon", "oasis", "object", "observe", "ocean", "octopus", + "olivia", "olympic", "omega", "opera", "optic", "optimal", + "orange", "orbit", "organic", "orient", "origin", "orlando", + "oscar", "oxford", "oxygen", "ozone", "pablo", "pacific", + "pagoda", "palace", "pamela", "panama", "panda", "panel", + "panic", "paradox", "pardon", "paris", "parker", "parking", + "parody", "partner", "passage", "passive", "pasta", "pastel", + "patent", "patriot", "patrol", "patron", "pegasus", "pelican", + "penguin", "pepper", "percent", "perfect", "perfume", "period", + "permit", "person", "peru", "phone", "photo", "piano", + "picasso", "picnic", "picture", "pigment", "pilgrim", "pilot", + "pirate", "pixel", "pizza", "planet", "plasma", "plaster", + "plastic", "plaza", "pocket", "poem", "poetic", "poker", + "polaris", "police", "politic", "polo", "polygon", "pony", + "popcorn", "popular", "postage", "postal", "precise", "prefix", + "premium", "present", "price", "prince", "printer", "prism", + "private", "product", "profile", "program", "project", "protect", + "proton", "public", "pulse", "puma", "pyramid", "queen", + "radar", "radio", "random", "rapid", "rebel", "record", + "recycle", "reflex", "reform", "regard", "regular", "relax", + "report", "reptile", "reverse", "ricardo", "ringo", "ritual", + "robert", "robot", "rocket", "rodeo", "romeo", "royal", + "russian", "safari", "salad", "salami", "salmon", "salon", + "salute", "samba", "sandra", "santana", "sardine", "school", + "screen", "script", "second", "secret", "section", "segment", + "select", "seminar", "senator", "senior", "sensor", "serial", + "service", "sheriff", "shock", "sierra", "signal", "silicon", + "silver", "similar", "simon", "single", "siren", "slogan", + "social", "soda", "solar", "solid", "solo", "sonic", + "soviet", "special", "speed", "spiral", "spirit", "sport", + "static", "station", "status", "stereo", "stone", "stop", + "street", "strong", "student", "studio", "style", "subject", + "sultan", "super", "susan", "sushi", "suzuki", "switch", + "symbol", "system", "tactic", "tahiti", "talent", "tango", + "tarzan", "taxi", "telex", "tempo", "tennis", "texas", + "textile", "theory", "thermos", "tiger", "titanic", "tokyo", + "tomato", "topic", "tornado", "toronto", "torpedo", "total", + "totem", "tourist", "tractor", "traffic", "transit", "trapeze", + "travel", "tribal", "trick", "trident", "trilogy", "tripod", + "tropic", "trumpet", "tulip", "tuna", "turbo", "twist", + "ultra", "uniform", "union", "uranium", "vacuum", "valid", + "vampire", "vanilla", "vatican", "velvet", "ventura", "venus", + "vertigo", "veteran", "victor", "video", "vienna", "viking", + "village", "vincent", "violet", "violin", "virtual", "virus", + "visa", "vision", "visitor", "visual", "vitamin", "viva", + "vocal", "vodka", "volcano", "voltage", "volume", "voyage", + "water", "weekend", "welcome", "western", "window", "winter", + "wizard", "wolf", "world", "xray", "yankee", "yoga", + "yogurt", "yoyo", "zebra", "zero", "zigzag", "zipper", + "zodiac", "zoom", "abraham", "action", "address", "alabama", + "alfred", "almond", "ammonia", "analyze", "annual", "answer", + "apple", "arena", "armada", "arsenal", "atlanta", "atomic", + "avenue", "average", "bagel", "baker", "ballet", "bambino", + "bamboo", "barbara", "basket", "bazaar", "benefit", "bicycle", + "bishop", "blitz", "bonjour", "bottle", "bridge", "british", + "brother", "brush", "budget", "cabaret", "cadet", "candle", + "capitan", "capsule", "career", "cartoon", "channel", "chapter", + "cheese", "circle", "cobalt", "cockpit", "college", "compass", + "comrade", "condor", "crimson", "cyclone", "darwin", "declare", + "degree", "delete", "delphi", "denver", "desert", "divide", + "dolby", "domain", "domingo", "double", "drink", "driver", + "eagle", "earth", "echo", "eclipse", "editor", "educate", + "edward", "effect", "electra", "emerald", "emotion", "empire", + "empty", "escape", "eternal", "evening", "exhibit", "expand", + "explore", "extreme", "ferrari", "first", "flag", "folio", + "forget", "forward", "freedom", "fresh", "friday", "fuji", + "galileo", "garcia", "genesis", "gold", "gravity", "habitat", + "hamlet", "harlem", "helium", "holiday", "house", "hunter", + "ibiza", "iceberg", "imagine", "infant", "isotope", "jackson", + "jamaica", "jasmine", "java", "jessica", "judo", "kitchen", + "lazarus", "letter", "license", "lithium", "loyal", "lucky", + "magenta", "mailbox", "manual", "marble", "mary", "maxwell", + "mayor", "milk", "monarch", "monday", "money", "morning", + "mother", "mystery", "native", "nectar", "nelson", "network", + "next", "nikita", "nobel", "nobody", "nominal", "norway", + "nothing", "number", "october", "office", "oliver", "opinion", + "option", "order", "outside", "package", "pancake", "pandora", + "panther", "papa", "patient", "pattern", "pedro", "pencil", + "people", "phantom", "philips", "pioneer", "pluto", "podium", + "portal", "potato", "prize", "process", "protein", "proxy", + "pump", "pupil", "python", "quality", "quarter", "quiet", + "rabbit", "radical", "radius", "rainbow", "ralph", "ramirez", + "ravioli", "raymond", "respect", "respond", "result", "resume", + "retro", "richard", "right", "risk", "river", "roger", + "roman", "rondo", "sabrina", "salary", "salsa", "sample", + "samuel", "saturn", "savage", "scarlet", "scoop", "scorpio", + "scratch", "scroll", "sector", "serpent", "shadow", "shampoo", + "sharon", "sharp", "short", "shrink", "silence", "silk", + "simple", "slang", "smart", "smoke", "snake", "society", + "sonar", "sonata", "soprano", "source", "sparta", "sphere", + "spider", "sponsor", "spring", "acid", "adios", "agatha", + "alamo", "alert", "almanac", "aloha", "andrea", "anita", + "arcade", "aurora", "avalon", "baby", "baggage", "balloon", + "bank", "basil", "begin", "biscuit", "blue", "bombay", + "brain", "brenda", "brigade", "cable", "carmen", "cello", + "celtic", "chariot", "chrome", "citrus", "civil", "cloud", + "common", "compare", "cool", "copper", "coral", "crater", + "cubic", "cupid", "cycle", "depend", "door", "dream", + "dynasty", "edison", "edition", "enigma", "equal", "eric", + "event", "evita", "exodus", "extend", "famous", "farmer", + "food", "fossil", "frog", "fruit", "geneva", "gentle", + "george", "giant", "gilbert", "gossip", "gram", "greek", + "grille", "hammer", "harvest", "hazard", "heaven", "herbert", + "heroic", "hexagon", "husband", "immune", "inca", "inch", + "initial", "isabel", "ivory", "jason", "jerome", "joel", + "joshua", "journal", "judge", "juliet", "jump", "justice", + "kimono", "kinetic", "leonid", "lima", "maze", "medusa", + "member", "memphis", "michael", "miguel", "milan", "mile", + "miller", "mimic", "mimosa", "mission", "monkey", "moral", + "moses", "mouse", "nancy", "natasha", "nebula", "nickel", + "nina", "noise", "orchid", "oregano", "origami", "orinoco", + "orion", "othello", "paper", "paprika", "prelude", "prepare", + "pretend", "profit", "promise", "provide", "puzzle", "remote", + "repair", "reply", "rival", "riviera", "robin", "rose", + "rover", "rudolf", "saga", "sahara", "scholar", "shelter", + "ship", "shoe", "sigma", "sister", "sleep", "smile", + "spain", "spark", "split", "spray", "square", "stadium", + "star", "storm", "story", "strange", "stretch", "stuart", + "subway", "sugar", "sulfur", "summer", "survive", "sweet", + "swim", "table", "taboo", "target", "teacher", "telecom", + "temple", "tibet", "ticket", "tina", "today", "toga", + "tommy", "tower", "trivial", "tunnel", "turtle", "twin", + "uncle", "unicorn", "unique", "update", "valery", "vega", + "version", "voodoo", "warning", "william", "wonder", "year", + "yellow", "young", "absent", "absorb", "accent", "alfonso", + "alias", "ambient", "andy", "anvil", "appear", "apropos", + "archer", "ariel", "armor", "arrow", "austin", "avatar", + "axis", "baboon", "bahama", "bali", "balsa", "bazooka", + "beach", "beast", "beatles", "beauty", "before", "benny", + "betty", "between", "beyond", "billy", "bison", "blast", + "bless", "bogart", "bonanza", "book", "border", "brave", + "bread", "break", "broken", "bucket", "buenos", "buffalo", + "bundle", "button", "buzzer", "byte", "caesar", "camilla", + "canary", "candid", "carrot", "cave", "chant", "child", + "choice", "chris", "cipher", "clarion", "clark", "clever", + "cliff", "clone", "conan", "conduct", "congo", "content", + "costume", "cotton", "cover", "crack", "current", "danube", + "data", "decide", "desire", "detail", "dexter", "dinner", + "dispute", "donor", "druid", "drum", "easy", "eddie", + "enjoy", "enrico", "epoxy", "erosion", "except", "exile", + "explain", "fame", "fast", "father", "felix", "field", + "fiona", "fire", "fish", "flame", "flex", "flipper", + "float", "flood", "floor", "forbid", "forever", "fractal", + "frame", "freddie", "front", "fuel", "gallop", "game", + "garbo", "gate", "gibson", "ginger", "giraffe", "gizmo", + "glass", "goblin", "gopher", "grace", "gray", "gregory", + "grid", "griffin", "ground", "guest", "gustav", "gyro", + "hair", "halt", "harris", "heart", "heavy", "herman", + "hippie", "hobby", "honey", "hope", "horse", "hostel", + "hydro", "imitate", "info", "ingrid", "inside", "invent", + "invest", "invite", "iron", "ivan", "james", "jester", + "jimmy", "join", "joseph", "juice", "julius", "july", + "justin", "kansas", "karl", "kevin", "kiwi", "ladder", + "lake", "laura", "learn", "legacy", "legend", "lesson", + "life", "light", "list", "locate", "lopez", "lorenzo", + "love", "lunch", "malta", "mammal", "margo", "marion", + "mask", "match", "mayday", "meaning", "mercy", "middle", + "mike", "mirror", "modest", "morph", "morris", "nadia", + "nato", "navy", "needle", "neuron", "never", "newton", + "nice", "night", "nissan", "nitro", "nixon", "north", + "oberon", "octavia", "ohio", "olga", "open", "opus", + "orca", "oval", "owner", "page", "paint", "palma", + "parade", "parent", "parole", "paul", "peace", "pearl", + "perform", "phoenix", "phrase", "pierre", "pinball", "place", + "plate", "plato", "plume", "pogo", "point", "polite", + "polka", "poncho", "powder", "prague", "press", "presto", + "pretty", "prime", "promo", "quasi", "quest", "quick", + "quiz", "quota", "race", "rachel", "raja", "ranger", + "region", "remark", "rent", "reward", "rhino", "ribbon", + "rider", "road", "rodent", "round", "rubber", "ruby", + "rufus", "sabine", "saddle", "sailor", "saint", "salt", + "satire", "scale", "scuba", "season", "secure", "shake", + "shallow", "shannon", "shave", "shelf", "sherman", "shine", + "shirt", "side", "sinatra", "sincere", "size", "slalom", + "slow", "small", "snow", "sofia", "song", "sound", + "south", "speech", "spell", "spend", "spoon", "stage", + "stamp", "stand", "state", "stella", "stick", "sting", + "stock", "store", "sunday", "sunset", "support", "sweden", + "swing", "tape", "think", "thomas", "tictac", "time", + "toast", "tobacco", "tonight", "torch", "torso", "touch", + "toyota", "trade", "tribune", "trinity", "triton", "truck", + "trust", "type", "under", "unit", "urban", "urgent", + "user", "value", "vendor", "venice", "verona", "vibrate", + "virgo", "visible", "vista", "vital", "voice", "vortex", + "waiter", "watch", "wave", "weather", "wedding", "wheel", + "whiskey", "wisdom", "deal", "null", "nurse", "quebec", + "reserve", "reunion", "roof", "singer", "verbal", "amen", + "ego", "fax", "jet", "job", "rio", "ski", + "yes", +} diff --git a/src/models/constants.go b/src/models/constants.go new file mode 100644 index 0000000..e10e2d4 --- /dev/null +++ b/src/models/constants.go @@ -0,0 +1,161 @@ +package models + +import ( + "context" + "fmt" + "net" + "os" + "path" + "time" + + "github.com/tzgyn/share/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" + 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 { + 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) +} + +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 +} + +func localLookupIP(address string) (ipaddress string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + r := &net.Resolver{} + ip, err := r.LookupHost(ctx, address) + if err != nil { + return + } + ipaddress = ip[0] + return +} + +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 +} diff --git a/src/tcp/ctx.go b/src/tcp/ctx.go new file mode 100644 index 0000000..073ee0e --- /dev/null +++ b/src/tcp/ctx.go @@ -0,0 +1,60 @@ +package tcp + +import ( + "context" + "errors" + "net" + "sync" + + log "github.com/schollz/logger" +) + +type stop struct { + ctx context.Context + cancel context.CancelFunc + server net.Listener + wg sync.WaitGroup + gui bool +} + +func newStop(ctx context.Context) *stop { + s := &stop{} + if ctx == nil { + ctx = context.Background() + } + s.ctx, s.cancel = context.WithCancel(ctx) + return s +} + +func (s *stop) Cancel() { + log.Trace("tcp Cancel") + if s.cancel != nil { + s.cancel() + s.cancel = nil + } +} + +func RunCtx(ctx context.Context, debugLevel, host, port string) error { + return RunWithOptionsAsync(host, port, WithLogLevel(debugLevel), WithCtx(ctx)) +} + +func WithCtx(ctx context.Context) serverOptsFunc { + return func(s *server) error { + if s.stop.cancel != nil { + s.stop.cancel() + } + s.stop = newStop(ctx) + s.stop.gui = true + return nil + } +} + +func Ignore(err error) error { + if err != nil && (errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) || + errors.Is(err, net.ErrClosed)) { + log.Tracef("ignored: %v", err) + return nil + } + return err +} diff --git a/src/tcp/defaults.go b/src/tcp/defaults.go new file mode 100644 index 0000000..27e23c8 --- /dev/null +++ b/src/tcp/defaults.go @@ -0,0 +1,9 @@ +package tcp + +import "time" + +const ( + DEFAULT_LOG_LEVEL = "debug" + DEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute + DEFAULT_ROOM_TTL = 3 * time.Hour +) diff --git a/src/tcp/options.go b/src/tcp/options.go new file mode 100644 index 0000000..bfa1be9 --- /dev/null +++ b/src/tcp/options.go @@ -0,0 +1,43 @@ +package tcp + +import ( + "fmt" + "time" +) + +var availableLogLevels = []string{"info", "error", "warn", "debug", "trace"} + +type serverOptsFunc func(s *server) error + +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 +} diff --git a/src/tcp/tcp.go b/src/tcp/tcp.go new file mode 100644 index 0000000..4456182 --- /dev/null +++ b/src/tcp/tcp.go @@ -0,0 +1,451 @@ +package tcp + +import ( + "bytes" + "context" + "fmt" + "net" + "strings" + "sync" + "time" + + log "github.com/schollz/logger" + + "github.com/tzgyn/share/src/comm" + "github.com/tzgyn/share/src/models" +) + +// server is a minimal relay: it has no notion of a password. Its only job +// is to pair two connections that show up with the same room name and then +// pipe raw bytes between them. Encrypting what flows through that pipe is +// the clients' responsibility, not the relay's. +type server struct { + host string + port string + debugLevel string + rooms roomMap + + roomCleanupInterval time.Duration + roomTTL time.Duration + + *stop +} + +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" + +func newDefaultServer() *server { + s := new(server) + s.roomCleanupInterval = DEFAULT_ROOM_CLEANUP_INTERVAL + s.roomTTL = DEFAULT_ROOM_TTL + s.debugLevel = DEFAULT_LOG_LEVEL + s.stop = newStop(context.Background()) + return s +} + +// RunWithOptionsAsync asynchronously starts a TCP listener. +func RunWithOptionsAsync(host, port string, opts ...serverOptsFunc) error { + s := newDefaultServer() + s.host = host + s.port = port + for _, opt := range opts { + if err := opt(s); 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 string) (err error) { + return RunWithOptionsAsync(host, port, WithLogLevel(debugLevel)) +} + +func (s *server) start() (err error) { + log.SetLevel(s.debugLevel) + + s.rooms.Lock() + s.rooms.rooms = make(map[string]roomInfo) + s.rooms.Unlock() + + s.stop.wg.Add(1) + go func() { + defer s.stop.wg.Done() + s.deleteOldRooms() + }() + defer s.stop.Cancel() + if s.stop.gui { + defer s.stop.wg.Wait() + } + + err = s.run() + err = Ignore(err) + 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 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 %s", addr) + lc := net.ListenConfig{} + s.stop.server, err = lc.Listen(s.stop.ctx, network, addr) + if err != nil { + return fmt.Errorf("error listening on %s: %w", addr, err) + } + defer s.stop.server.Close() + + go func() { + dc := &net.Dialer{ + Timeout: 100 * time.Millisecond, + } + if conn, err := dc.DialContext(s.stop.ctx, network, addr); err == nil { + log.Debugf("started TCP server on %s", addr) + conn.Close() + } else { + log.Errorf("started TCP server on %s : %v", addr, err) + s.stop.Cancel() + } + }() + + // spawn a new goroutine whenever a client connects + for { + connection, err := s.stop.server.Accept() + if err != nil { + return fmt.Errorf("problem accepting connection: %w", err) + } + log.Debugf("client %s connected", connection.RemoteAddr().String()) + s.stop.wg.Add(1) + go func(connection net.Conn) { + defer s.stop.wg.Done() + c := comm.New(connection) + room, errCommunication := s.clientCommunication(c) + 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 + } + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + // check connection + deleteIt := false + s.rooms.Lock() + roomData, ok := s.rooms.rooms[room] + if !ok { + log.Debug("room is gone") + s.rooms.Unlock() + return + } + if roomData.first != nil && roomData.second != nil { + log.Debug("rooms ready") + s.rooms.Unlock() + break + } + if roomData.first != nil { + if errSend := roomData.first.Send([]byte{1}); errSend != nil { + log.Debug(errSend) + deleteIt = true + } + } + s.rooms.Unlock() + if deleteIt { + s.deleteRoom(room) + break + } + select { + case <-s.stop.ctx.Done(): + s.deleteRoom(room) + return + case <-ticker.C: + } + } + }(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) + defer func() { + ticker.Stop() + log.Debug("room cleanup stopped") + }() + for next := true; next; { + roomsToDelete := []string{} + select { + case <-ticker.C: + s.rooms.Lock() + for room, roomData := range s.rooms.rooms { + if time.Since(roomData.opened) > s.roomTTL { + roomsToDelete = append(roomsToDelete, room) + } + } + s.rooms.Unlock() + case <-s.stop.ctx.Done(): + if s.server != nil { + log.Debugf("stop TCP server on %s", s.server.Addr()) + s.server.Close() + time.Sleep(time.Millisecond) + } + log.Debug("stop room cleanup fired") + s.rooms.Lock() + for room := range s.rooms.rooms { + roomsToDelete = append(roomsToDelete, room) + } + s.rooms.Unlock() + next = false + } + for _, room := range roomsToDelete { + s.deleteRoom(room) + log.Debugf("room cleaned up: %s", room) + } + } +} + +// clientCommunication is the entire relay protocol: a client sends the room +// name it wants to join (or "ping" to check liveness), and the relay either +// parks it as the first occupant or, once a second client shows up, wires +// the two connections together and gets out of the way. +func (s *server) clientCommunication(c *comm.Comm) (room string, err error) { + roomBytes, err := c.Receive() + if err != nil { + return + } + if bytes.Equal(roomBytes, []byte("ping")) { + room = pingRoom + log.Debug("sending back pong") + c.Send([]byte("pong")) + 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() + err = c.Send([]byte("ok|||" + c.Connection().RemoteAddr().String())) + 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() + if err = c.Send([]byte("room full")); err != nil { + log.Error(err) + } + 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 joiner everything is ready + err = c.Send([]byte("ok|||" + c.Connection().RemoteAddr().String())) + 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() + roomData, ok := s.rooms.rooms[room] + if !ok { + return + } + log.Debugf("deleting room: %s", room) + if roomData.first != nil { + roomData.first.Close() + } + if roomData.second != nil { + roomData.second.Close() + } + 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 and join the given room, no password required. Once the room +// pairs up, the returned *comm.Comm is a raw pipe to the other peer. +func ConnectToTCPServer(address, room string, timelimit ...time.Duration) (c *comm.Comm, 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 + } + + log.Debugf("joining room: %s", room) + if err = c.Send([]byte(room)); err != nil { + log.Debug(err) + return + } + + data, err := c.Receive() + if err != nil { + log.Debug(err) + return + } + if !bytes.HasPrefix(data, []byte("ok")) { + err = fmt.Errorf("got bad response: %s", data) + log.Debug(err) + return + } + if parts := strings.SplitN(string(data), "|||", 2); len(parts) == 2 { + ipaddr = parts[1] + } + log.Debug("all set") + return +} diff --git a/src/utils/utils.go b/src/utils/utils.go new file mode 100644 index 0000000..1c616e2 --- /dev/null +++ b/src/utils/utils.go @@ -0,0 +1,827 @@ +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/v2" + "github.com/kalafut/imohash" + "github.com/minio/highwayhash" + "github.com/tzgyn/share/src/mnemonicode" + log "github.com/schollz/logger" + "github.com/schollz/progressbar/v3" +) + +const NbPinNumbers = 4 +const NbBytesWords = 4 + +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, "share") + } else { + homedir, err = os.UserHomeDir() + if err != nil { + if !requireValidPath { + err = nil + homedir = "" + } + return + } + homedir = path.Join(homedir, ".config", "share") + } + + if requireValidPath { + if _, err = os.Stat(homedir); os.IsNotExist(err) { + err = os.MkdirAll(homedir, 0o700) + } + } + return +} + +func Exists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +func GetInput(prompt string) string { + reader := bufio.NewReader(os.Stdin) + fmt.Fprintf(os.Stderr, "%s", prompt) + text, _ := reader.ReadString('\n') + return strings.TrimSpace(text) +} + +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 +} + +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(), + progressbar.OptionFullWidth(), + ) + 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 +} + +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(), + progressbar.OptionFullWidth(), + ) + 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) + +func IMOHashFile(fname string) (hash []byte, err error) { + b, err := imopartial.SumFile(fname) + hash = b[:] + return +} + +func IMOHashFileFull(fname string) (hash []byte, err error) { + b, err := imofull.SumFile(fname) + hash = b[:] + return +} + +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(), + progressbar.OptionFullWidth(), + ) + 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 +} + +func SHA256(s string) string { + sha := sha256.New() + sha.Write([]byte(s)) + return hex.EncodeToString(sha.Sum(nil)) +} + +func PublicIP() (ip string, err error) { + resp, err := http.Get("http://ipv4.icanhazip.com") + if err != nil { + return + } + defer resp.Body.Close() + + buf := new(bytes.Buffer) + buf.ReadFrom(resp.Body) + ip = strings.TrimSpace(buf.String()) + + return +} + +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 range NbPinNumbers { + v, err := rand.Int(rand.Reader, max) + if err != nil { + panic(err) + } + s += fmt.Sprintf("%d", v) + } + return s +} + +func GetRandomName() string { + var result []string + bs := make([]byte, NbBytesWords) + rand.Read(bs) + result = mnemonicode.EncodeWordList(result, bs) + return GenerateRandomPin() + "-" + strings.Join(result, "-") +} + +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]) +} + +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 + } + + var bar *progressbar.ProgressBar + showProgress := fsize > 10*1024*1024 + if showProgress { + fnameShort := path.Base(fname) + if len(fnameShort) > 20 { + fnameShort = fnameShort[:20] + "..." + } + bar = progressbar.NewOptions64(fsize, + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionShowBytes(true), + progressbar.OptionSetDescription(fmt.Sprintf("Checking %s", fnameShort)), + progressbar.OptionClearOnFinish(), + progressbar.OptionFullWidth(), + progressbar.OptionThrottle(100*time.Millisecond), + ) + } + + 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 showProgress && bar != nil { + bar.Add(bytesread) + } + } + if showProgress && bar != nil { + bar.Finish() + } + 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 +} + +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 +} + +func GetLocalIPs() (ips []string, err error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + if ip := LocalIP(); ip != "" { + return []string{ip}, nil + } + return + } + ips = []string{} + for _, address := range addrs { + 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(".", "share-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 +} + +var privateIPBlocks []*net.IPNet + +func init() { + for _, cidr := range []string{ + "127.0.0.0/8", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16", + "::1/128", + "fe80::/10", + "fc00::/7", + } { + _, 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, ignoredPaths map[string]bool, exclusions []string) (err error) { + if _, err = os.Stat(destination); err == nil { + log.Errorf("%s file already exists!\n", destination) + return fmt.Errorf("file already exists: %s", destination) + } + + if _, err := os.Stat(source); os.IsNotExist(err) { + log.Errorf("Source directory does not exist: %s", source) + return fmt.Errorf("source directory does not exist: %s", source) + } + + sourceAbs, err := filepath.Abs(source) + if err != nil { + return fmt.Errorf("failed to resolve source path: %w", err) + } + if resolvedSourceAbs, resolveErr := filepath.EvalSymlinks(sourceAbs); resolveErr == nil { + sourceAbs = resolvedSourceAbs + } + destinationAbs, err := filepath.Abs(destination) + if err != nil { + return fmt.Errorf("failed to resolve zip destination path: %w", err) + } + + fmt.Fprintf(os.Stderr, "Zipping %s to %s\n", source, destination) + file, err := os.Create(destination) + if err != nil { + log.Error(err) + return fmt.Errorf("failed to create zip file: %w", err) + } + defer file.Close() + if resolvedDestinationAbs, resolveErr := filepath.EvalSymlinks(destinationAbs); resolveErr == nil { + destinationAbs = resolvedDestinationAbs + } + skipDestination := pathWithin(sourceAbs, destinationAbs) + writer := zip.NewWriter(file) + writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { + return flate.NewWriter(out, flate.NoCompression) + }) + defer writer.Close() + + baseName := strings.TrimSuffix(filepath.Base(destination), ".zip") + + rootInfo, err := os.Stat(source) + if err == nil && rootInfo.IsDir() { + header, err := zip.FileInfoHeader(rootInfo) + if err != nil { + log.Error(err) + } else { + header.Name = baseName + "/" + header.Method = zip.Store + header.Modified = rootInfo.ModTime() + + _, err = writer.CreateHeader(header) + if err != nil { + log.Error(err) + } else { + fmt.Fprintf(os.Stderr, "\r\033[2K") + fmt.Fprintf(os.Stderr, "\rAdding %s", baseName+"/") + } + } + } + + err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Error(err) + return nil + } + + if path == source { + return nil + } + + if skipDestination { + absPath, absErr := filepath.Abs(path) + if absErr == nil { + if resolvedAbsPath, resolveErr := filepath.EvalSymlinks(absPath); resolveErr == nil { + absPath = resolvedAbsPath + } + if filepath.Clean(absPath) == filepath.Clean(destinationAbs) { + return nil + } + } + } + + if len(ignoredPaths) > 0 { + absPath, absErr := filepath.Abs(path) + if absErr == nil && ignoredPaths[absPath] { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + + relPath, err := filepath.Rel(source, path) + if err != nil { + log.Error(err) + return nil + } + + zipPath := filepath.Join(baseName, relPath) + zipPath = filepath.ToSlash(zipPath) + + if len(exclusions) > 0 { + zipPathLower := strings.ToLower(zipPath) + for _, exclusion := range exclusions { + if strings.Contains(zipPathLower, exclusion) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + } + + if info.IsDir() { + header, err := zip.FileInfoHeader(info) + if err != nil { + log.Error(err) + return nil + } + header.Name = zipPath + "/" + header.Method = zip.Store + header.Modified = info.ModTime() + + _, err = writer.CreateHeader(header) + if err != nil { + log.Error(err) + return nil + } + + fmt.Fprintf(os.Stderr, "\r\033[2K") + fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath+"/") + return nil + } + + if info.Mode().IsRegular() { + f1, err := os.Open(path) + if err != nil { + log.Error(err) + return nil + } + defer f1.Close() + + header, err := zip.FileInfoHeader(info) + if err != nil { + log.Error(err) + return nil + } + header.Name = zipPath + header.Method = zip.Deflate + + w1, err := writer.CreateHeader(header) + if err != nil { + log.Error(err) + return nil + } + + if _, err := io.Copy(w1, f1); err != nil { + log.Error(err) + return nil + } + + fmt.Fprintf(os.Stderr, "\r\033[2K") + fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath) + } + return nil + }) + + if err != nil { + log.Error(err) + return fmt.Errorf("error during directory walk: %w", err) + } + fmt.Fprintf(os.Stderr, "\n") + return nil +} + +func pathWithin(parent string, child string) bool { + rel, err := filepath.Rel(filepath.Clean(parent), filepath.Clean(child)) + if err != nil { + return false + } + return rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) +} + +func UnzipDirectory(destination string, source string) error { + archive, err := zip.OpenReader(source) + if err != nil { + log.Error(err) + return fmt.Errorf("failed to open zip file: %w", err) + } + defer archive.Close() + + filePaths := make([]string, len(archive.File)) + for i, f := range archive.File { + filePath, pathErr := resolveUnzipPath(destination, f.Name) + if pathErr != nil { + log.Errorf("Invalid file path %s: %v\n", f.Name, pathErr) + return fmt.Errorf("invalid file path in zip entry %q: %w", f.Name, pathErr) + } + filePaths[i] = filePath + } + + modTimes := make(map[string]time.Time) + + for i, f := range archive.File { + filePath := filePaths[i] + fmt.Fprintf(os.Stderr, "\r\033[2K") + fmt.Fprintf(os.Stderr, "\rUnzipping file %s", filePath) + + modifiedTime := f.Modified + if modifiedTime.IsZero() { + modifiedTime = f.FileHeader.Modified + } + if !modifiedTime.IsZero() { + modTimes[filePath] = modifiedTime + } + + if f.FileInfo().IsDir() { + if err := os.MkdirAll(filePath, os.ModePerm); err != nil { + log.Error(err) + } + continue + } + + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + log.Error(err) + continue + } + + 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) + continue + } + + fileInArchive, err := f.Open() + if err != nil { + log.Error(err) + dstFile.Close() + continue + } + + if _, err := io.Copy(dstFile, fileInArchive); err != nil { + log.Error(err) + } + + dstFile.Close() + fileInArchive.Close() + } + + for path, modTime := range modTimes { + if err := os.Chtimes(path, modTime, modTime); err != nil { + log.Errorf("Failed to set modification time for %s: %v", path, err) + } else { + fi, err := os.Lstat(path) + if err != nil || + !modTime.UTC().Equal(fi.ModTime().UTC()) { + log.Errorf("Failed to set modification time for %s: %v", path, err) + fmt.Fprintf(os.Stderr, "Failed to set modification time %s %v: %v\n", path, modTime, err) + } + } + } + + fmt.Fprintf(os.Stderr, "\n") + return nil +} + +func resolveUnzipPath(destination string, entryName string) (string, error) { + if filepath.IsAbs(entryName) || filepath.VolumeName(entryName) != "" { + return "", fmt.Errorf("path escapes destination") + } + + destinationAbs, err := filepath.Abs(destination) + if err != nil { + return "", fmt.Errorf("failed to resolve destination: %w", err) + } + destinationAbs = filepath.Clean(destinationAbs) + + filePath := filepath.Clean(filepath.Join(destinationAbs, entryName)) + relativePath, err := filepath.Rel(destinationAbs, filePath) + if err != nil { + return "", fmt.Errorf("failed to resolve path %q: %w", entryName, err) + } + if relativePath == ".." || strings.HasPrefix(relativePath, ".."+string(os.PathSeparator)) { + return "", fmt.Errorf("path escapes destination") + } + + return filePath, nil +} + +func ValidFileName(fname string) (err error) { + 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 + } + } + _, basename := filepath.Split(fname) + if strings.Contains(basename, string(os.PathSeparator)) { + err = fmt.Errorf("basename cannot contain path separators: '%s'", basename) + return + } + if filepath.IsAbs(fname) { + err = fmt.Errorf("filename cannot be an absolute path: '%s'", fname) + return + } + if !filepath.IsLocal(fname) { + err = fmt.Errorf("filename must be a local path: '%s'", fname) + return + } + return +} + +const shareRemovalFile = "share-marked-files.txt" + +func MarkFileForRemoval(fname string) { + f, err := os.OpenFile(shareRemovalFile, 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) { + f, err := os.Open(shareRemovalFile) + if err != nil { + return + } + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fname := scanner.Text() + err = os.Remove(fname) + if err == nil { + log.Tracef("Removed %s", fname) + } + } + f.Close() + os.Remove(shareRemovalFile) + return +}