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.
This commit is contained in:
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+194
@@ -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])
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+451
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user