pax_global_header00006660000000000000000000000064143105301440014505gustar00rootroot0000000000000052 comment=05a9ad28c57885fb75565225ff19debe5e366685 wormhole-william-1.0.6/000077500000000000000000000000001431053014400150015ustar00rootroot00000000000000wormhole-william-1.0.6/.github/000077500000000000000000000000001431053014400163415ustar00rootroot00000000000000wormhole-william-1.0.6/.github/workflows/000077500000000000000000000000001431053014400203765ustar00rootroot00000000000000wormhole-william-1.0.6/.github/workflows/go.yml000066400000000000000000000017131431053014400215300ustar00rootroot00000000000000name: Go on: [push, pull_request] jobs: build: name: Build/Test runs-on: ${{ matrix.os }} timeout-minutes: 3 strategy: matrix: go-version: [1.16.12, 1.17.5, 1.18beta1] os: [ubuntu-latest, windows-latest] steps: - name: Set up Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Build run: go build -v ./... - name: Test run: go test -v ./... --timeout 60s - name: Meta Tests run: go test -v -tags ci ./ci --timeout 60s if: ${{ runner.os == 'Linux' }} - name: Cross test for i386 run: env GOOS=linux GOARCH=386 go test -v ./... --timeout 60s if: ${{ runner.os == 'Linux' }} - name: Cross compile for arm (RPI) run: env GOOS=linux GOARCH=arm GOARM=5 go build -v ./... if: ${{ runner.os == 'Linux' }} wormhole-william-1.0.6/.github/workflows/release.yml000066400000000000000000000026211431053014400225420ustar00rootroot00000000000000name: Create Release on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: build: name: Create Release runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v2 with: go-version: "1.15.2" - name: Checkout code uses: actions/checkout@v2 - name: Build Artifacts run: go run build_release.go - name: Create Release uses: actions/github-script@v2 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | const fs = require('fs').promises; console.log('environment', process.versions); const { repo: { owner, repo }, sha } = context; const release = await github.repos.createRelease({ owner, repo, tag_name: process.env.GITHUB_REF, name: process.env.GITHUB_REF.split('/')[2], draft: true, target_commitish: sha }); console.log('created release', { release }); for (let file of await fs.readdir('./release')) { console.log('uploading', file); await github.repos.uploadReleaseAsset({ owner, repo, release_id: release.data.id, name: file, data: await fs.readFile(`./release/${file}`) }); } wormhole-william-1.0.6/.gitignore000066400000000000000000000000401431053014400167630ustar00rootroot00000000000000release/ wormhole-william *.exe wormhole-william-1.0.6/LICENSE000066400000000000000000000020701431053014400160050ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2019 Peter Sanford Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wormhole-william-1.0.6/README.md000066400000000000000000000071021431053014400162600ustar00rootroot00000000000000# wormhole-william wormhole-william is a Go (golang) implementation of [magic wormhole](https://magic-wormhole.readthedocs.io/en/latest/). It provides secure end-to-end encrypted file transfers between computers. The endpoints are connected using the same "wormhole code". wormhole-william is compatible with the official [python magic wormhole cli tool](https://github.com/warner/magic-wormhole). Currently, wormhole-william supports: - sending and receiving text over the wormhole protocol - sending and receiving files over the transit protocol - sending and receiving directories over the transit protocol ## Docs https://pkg.go.dev/github.com/psanford/wormhole-william/wormhole?tab=doc ## CLI Usage ``` $ wormhole-william send --help Send a text message, file, or directory... Usage: wormhole-william send [WHAT] [flags] Flags: --code string human-generated code phrase -c, --code-length int length of code (in bytes/words) -h, --help help for send --hide-progress suppress progress-bar display -v, --verify display verification string (and wait for approval) Global Flags: --relay-url string rendezvous relay to use $ wormhole-william receive --help Receive a text message, file, or directory... Usage: wormhole-william receive [code] [flags] Aliases: receive, recv Flags: -h, --help help for receive --hide-progress suppress progress-bar display -v, --verify display verification string (and wait for approval) Global Flags: --relay-url string rendezvous relay to use ``` ### CLI tab completion The wormhole-william CLI supports shell completion, including completing the receive code. To enable shell completion follow the instructions from `wormhole-william shell-completion -h`. ## Building the CLI tool wormhole-william uses go modules so it requires a version of the go tool chain >= 1.11. If you are using a version of go that supports modules you can clone the repo outside of your GOPATH and do a `go build` in the top level directory. To just install via the go tool run: ``` go install github.com/psanford/wormhole-william@latest ``` ## API Usage Sending text: ```go package main import ( "context" "fmt" "io/ioutil" "log" "github.com/psanford/wormhole-william/wormhole" ) func sendText() { var c wormhole.Client msg := "Dillinger-entertainer" ctx := context.Background() code, status, err := c.SendText(ctx, msg) if err != nil { log.Fatal(err) } fmt.Println("On the other computer, please run: wormhole receive") fmt.Printf("Wormhole code is: %s\n", code) s := <-status if s.OK { fmt.Println("OK!") } else { log.Fatalf("Send error: %s", s.Error) } } func recvText(code string) { var c wormhole.Client ctx := context.Background() msg, err := c.Receive(ctx, code) if err != nil { log.Fatal(err) } if msg.Type != wormhole.TransferText { log.Fatalf("Expected a text message but got type %s", msg.Type) } msgBody, err := ioutil.ReadAll(msg) if err != nil { log.Fatal(err) } fmt.Println("got message:") fmt.Println(msgBody) } ``` See the [cli tool](https://github.com/psanford/wormhole-william/tree/master/cmd) and [examples](https://github.com/psanford/wormhole-william/tree/master/examples) directory for working examples of how to use the API to send and receive text, files and directories. ## Third Party Users of Wormhole William - [wormhole-gui](https://github.com/Jacalz/wormhole-gui): A Magic Wormhole graphical user interface - [wormhole-william-mobile](https://github.com/psanford/wormhole-william-mobile): Android wormhole-william app wormhole-william-1.0.6/build_release.go000066400000000000000000000043661431053014400201400ustar00rootroot00000000000000//go:build ignore // +build ignore // This is a tool to assist with building release artifacts. // It is run automatically from github actions to produce the // artifacts. // // Instructions for cutting a new release: // - Update version/version.go // - Make new git tag (e.g. v1.0.x) // - Push tag to github // - Github release.yml action will `go run build_release.go` at that tag package main import ( "bytes" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "github.com/psanford/wormhole-william/version" ) var ignoreTagMismatch = flag.Bool("ignore-tag-mismatch", false, "Don't check if current tag matches in code version") func main() { flag.Parse() if !*ignoreTagMismatch { checkTagMatchesVersion() } os.MkdirAll("release", 0777) for _, t := range targets { cmd := exec.Command("go", "build", "-trimpath", "-o", filepath.Join("release", t.binaryName())) env := []string{"GOOS=" + t.goos, "GOARCH=" + t.garch, "GO111MODULE=on"} if t.goarm != "" { env = append(env, "GOARM="+t.goarm) } cmd.Env = append(os.Environ(), env...) fmt.Printf("run: %s %s %s\n", strings.Join(env, " "), cmd.Path, strings.Join(cmd.Args[1:], " ")) out, err := cmd.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "%s %s err: %s, out: %s\n", t.goos, t.garch, err, out) os.Exit(1) } } } func checkTagMatchesVersion() { codeVersion := version.AgentVersion headSha := gitCmd("rev-parse", "HEAD") tagSha := gitCmd("rev-parse", codeVersion) if headSha != tagSha { log.Fatalf("Tag for %s does not match HEAD ref: HEAD:%s TAG:%s", codeVersion, headSha, tagSha) } } func gitCmd(args ...string) string { out, err := exec.Command("git", args...).Output() if err != nil { log.Fatalf("git %s failed: %s", args, err) } return string(bytes.TrimSpace(out)) } type target struct { goos string garch string goarm string } func (t *target) binaryName() string { ext := "" if t.goos == "windows" { ext = ".exe" } tmpl := "wormhole-william-%s-%s%s%s" return fmt.Sprintf(tmpl, t.goos, t.garch, t.goarm, ext) } var targets = []target{ {"linux", "amd64", ""}, {"linux", "arm64", ""}, {"linux", "arm", "5"}, {"linux", "arm", "6"}, {"linux", "arm", "7"}, {"darwin", "amd64", ""}, {"windows", "386", ""}, {"freebsd", "amd64", ""}, } wormhole-william-1.0.6/ci/000077500000000000000000000000001431053014400153745ustar00rootroot00000000000000wormhole-william-1.0.6/ci/gofmt_test.go000066400000000000000000000020321431053014400200730ustar00rootroot00000000000000//go:build ci // +build ci package ci import ( "bytes" "go/format" "io/ioutil" "os" "path/filepath" "strings" "testing" ) func TestGofmt(t *testing.T) { var ( needsFormatting []string checkedFiles int ) err := filepath.Walk("..", func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if path == ".git" { return filepath.SkipDir } if info.IsDir() || !strings.HasSuffix(path, ".go") { return nil } content, err := ioutil.ReadFile(path) if err != nil { return err } formatted, err := format.Source(content) if err != nil { return err } if !bytes.Equal(content, formatted) { needsFormatting = append(needsFormatting, strings.TrimPrefix(path, "..")) } checkedFiles++ return nil }) if err != nil { t.Error(err) } if len(needsFormatting) > 0 { t.Fatalf("The following files are not properlery gofmt'ed: %v", needsFormatting) } if checkedFiles < 20 { t.Fatalf("Expected to check at least 20 files but only checked %d", checkedFiles) } } wormhole-william-1.0.6/ci/gomodtidy_test.go000066400000000000000000000022321431053014400207600ustar00rootroot00000000000000//go:build ci // +build ci package ci import ( "bytes" "fmt" "os/exec" "strings" "testing" ) func TestGoModTidy(t *testing.T) { modified, err := modifiedFiles() if err != nil { t.Fatal(err) } if len(modified) > 0 { t.Fatalf("Modified files detected before running `go mod tidy`: \n%s", strings.Join(modified, "\n")) } output, err := exec.Command("go", "mod", "tidy").CombinedOutput() if err != nil { t.Fatalf("go mod tidy err: %s %s", err, output) } modified, err = modifiedFiles() if err != nil { t.Fatal(err) } if len(modified) > 0 { t.Fatalf("Modified files detected from running `go mod tidy`: %v", modified) } } func modifiedFiles() ([]string, error) { var modifiedFiles []string status, err := exec.Command("git", "status", "-s").CombinedOutput() if err != nil { fmt.Printf("%s\n", status) return nil, err } lines := bytes.Split(status, []byte("\n")) for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } if bytes.HasPrefix(line, []byte("??")) { // ignore untracked files continue } modifiedFiles = append(modifiedFiles, string(line)) } return modifiedFiles, nil } wormhole-william-1.0.6/cmd/000077500000000000000000000000001431053014400155445ustar00rootroot00000000000000wormhole-william-1.0.6/cmd/cmd.go000066400000000000000000000016021431053014400166350ustar00rootroot00000000000000package cmd import ( "os" "github.com/psanford/wormhole-william/version" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "wormhole-william", Short: "Create a wormhole and transfer files through it.", Version: version.AgentVersion, Long: `Create a (magic) Wormhole and communicate through it. Wormholes are created by speaking the same magic CODE in two different places at the same time. Wormholes are secure against anyone who doesn't use the same code.`, } var ( relayURL string verify bool hideProgressBar bool ) func Execute() error { rootCmd.PersistentFlags().StringVar(&relayURL, "relay-url", "", "rendezvous relay to use") if relayURL == "" { relayURL = os.Getenv("WORMHOLE_RELAY_URL") } rootCmd.AddCommand(recvCommand()) rootCmd.AddCommand(sendCommand()) rootCmd.AddCommand(completionCommand()) return rootCmd.Execute() } wormhole-william-1.0.6/cmd/completion.go000066400000000000000000000073531431053014400202540ustar00rootroot00000000000000package cmd import ( "context" "os" "strings" "time" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous" "github.com/psanford/wormhole-william/wordlist" "github.com/psanford/wormhole-william/wormhole" "github.com/spf13/cobra" ) func completionCommand() *cobra.Command { cmd := &cobra.Command{ Use: "shell-completion [bash|zsh|fish|powershell]", Short: "Generate shell completion script", Long: `To load completions: Bash: $ source <(wormhole-william shell-completion bash) # To configure your bash shell to load completions for each session add to your bashrc # ~/.bashrc or ~/.profile if which wormhole-william &>/dev/null ; then . <(wormhole-william shell-completion bash) fi Zsh: # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ wormhole-william shell-completion zsh > "${fpath[1]}/_wormhole-william" # You will need to start a new shell for this setup to take effect. fish: $ wormhole-william shell-completion fish | source # To load completions for each session, execute once: $ wormhole-william shell-completion fish > ~/.config/fish/completions/wormhole-william.fish PowerShell: PS> wormhole-william shell-completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> wormhole-william shell-completion powershell > wormhole-william.ps1 # and source this file from your PowerShell profile. `, DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { switch args[0] { case "bash": cmd.Root().GenBashCompletion(os.Stdout) case "zsh": cmd.Root().GenZshCompletion(os.Stdout) case "fish": cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) } }, } return cmd } func recvCodeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { flags := cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace parts := strings.Split(toComplete, "-") if len(parts) < 2 { nameplates, err := activeNameplates() if err != nil { return nil, flags } if len(parts) == 0 { return nameplates, flags } var candidates []string for _, nameplate := range nameplates { if strings.HasPrefix(nameplate, parts[0]) { candidates = append(candidates, nameplate+"-") } } return candidates, flags } currentCompletion := parts[len(parts)-1] prefix := parts[:len(parts)-1] // even odd is based on just the number of words so slice off the mailbox parts = parts[1:] even := len(parts)%2 == 0 var candidates []string for _, pair := range wordlist.RawWords { var candidateWord string if even { candidateWord = pair.Even } else { candidateWord = pair.Odd } if strings.HasPrefix(candidateWord, currentCompletion) { guessParts := append(prefix, candidateWord) candidates = append(candidates, strings.Join(guessParts, "-")) } } return candidates, flags } func activeNameplates() ([]string, error) { url := wormhole.DefaultRendezvousURL sideID := crypto.RandSideID() appID := wormhole.WormholeCLIAppID ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() client := rendezvous.NewClient(url, sideID, appID) mood := rendezvous.Happy defer client.Close(ctx, mood) _, err := client.Connect(ctx) if err != nil { return nil, err } return client.ListNameplates(ctx) } wormhole-william-1.0.6/cmd/recv.go000066400000000000000000000146351431053014400170430ustar00rootroot00000000000000package cmd import ( "bufio" "context" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/cheggaaa/pb/v3" "github.com/klauspost/compress/zip" "github.com/psanford/wormhole-william/wormhole" "github.com/spf13/cobra" ) func recvCommand() *cobra.Command { cmd := cobra.Command{ Use: "receive [OPTIONS] [CODE]...", Aliases: []string{"recv"}, Short: "Receive a text message, file, or directory...", Run: recvAction, } cmd.Flags().BoolVarP(&verify, "verify", "v", false, "display verification string (and wait for approval)") cmd.Flags().BoolVar(&hideProgressBar, "hide-progress", false, "suppress progress-bar display") cmd.ValidArgsFunction = recvCodeCompletion return &cmd } func recvAction(cmd *cobra.Command, args []string) { var ( code string c = newClient() ctx = context.Background() ) if len(args) > 0 { code = args[0] } if code == "" { reader := bufio.NewReader(os.Stdin) fmt.Print("Enter receive wormhole code: ") line, err := reader.ReadString('\n') if err != nil { errf("Error reading from stdin: %s\n", err) } code = strings.TrimSpace(line) } if verify { c.VerifierOk = func(code string) bool { fmt.Printf("Verifier %s.\n", code) return true } } msg, err := c.Receive(ctx, code) if err != nil { log.Fatal(err) } switch msg.Type { case wormhole.TransferText: body, err := ioutil.ReadAll(msg) if err != nil { log.Fatal(err) } fmt.Println(string(body)) case wormhole.TransferFile: var acceptFile bool if _, err := os.Stat(msg.Name); err == nil { msg.Reject() errf("Error refusing to overwrite existing '%s'", msg.Name) } else if !os.IsNotExist(err) { msg.Reject() errf("Error stat'ing existing '%s'\n", msg.Name) } else { reader := bufio.NewReader(os.Stdin) fmt.Printf("Receiving file (%s) into: %s\n", formatBytes(msg.TransferBytes64), msg.Name) fmt.Print("ok? (y/N):") line, err := reader.ReadString('\n') if err != nil { errf("Error reading from stdin: %s\n", err) } line = strings.TrimSpace(line) if line == "y" { acceptFile = true } if !acceptFile { msg.Reject() bail("transfer rejected") } else { wd, err := os.Getwd() if err != nil { bail("Failed to get working directory: %s", err) } f, err := ioutil.TempFile(wd, fmt.Sprintf("%s.tmp", msg.Name)) if err != nil { bail("Failed to create tempfile: %s", err) } proxyReader := pbProxyReader(msg, msg.TransferBytes64) _, err = io.Copy(f, proxyReader) if err != nil { os.Remove(f.Name()) bail("Receive file error: %s", err) } proxyReader.Close() tmpName := f.Name() f.Close() err = os.Rename(tmpName, msg.Name) if err != nil { bail("Rename %s to %s failed: %s", tmpName, msg.Name, err) } } } case wormhole.TransferDirectory: var acceptDir bool wd, err := os.Getwd() if err != nil { bail("Failed to get working directory: %s", err) } dirName := msg.Name dirName, err = filepath.Abs(dirName) if err != nil { bail("Failed to get abs directory: %s", err) } if filepath.Dir(dirName) != wd { bail("Bad Directory name %s", msg.Name) } if _, err := os.Stat(dirName); err == nil { errf("Error refusing to overwrite existing '%s'", msg.Name) } else if !os.IsNotExist(err) { errf("Error stat'ing existing '%s'\n", msg.Name) } else { reader := bufio.NewReader(os.Stdin) fmt.Printf("Receiving directory (%s) into: %s\n", formatBytes(msg.TransferBytes64), msg.Name) fmt.Printf("%d files, %s (uncompressed)\n", msg.FileCount, formatBytes(msg.UncompressedBytes64)) fmt.Print("ok? (y/N):") line, err := reader.ReadString('\n') if err != nil { errf("Error reading from stdin: %s\n", err) } line = strings.TrimSpace(line) if line == "y" { acceptDir = true } if !acceptDir { msg.Reject() bail("transfer rejected") } else { err = os.Mkdir(msg.Name, 0777) if err != nil { bail("Mkdir error for %s: %s\n", msg.Name, err) } tmpFile, err := ioutil.TempFile(wd, msg.Name+".zip.tmp") if err != nil { bail("Failed to create tempfile: %s", err) } defer tmpFile.Close() defer os.Remove(tmpFile.Name()) proxyReader := pbProxyReader(msg, msg.TransferBytes64) n, err := io.Copy(tmpFile, proxyReader) if err != nil { os.Remove(tmpFile.Name()) bail("Receive file error: %s", err) } zr, err := zip.NewReader(tmpFile, n) if err != nil { bail("Read zip error: %s", err) } for _, zf := range zr.File { p, err := filepath.Abs(filepath.Join(dirName, zf.Name)) if err != nil { bail("Failes to calculate file path ABS: %s", err) } if !strings.HasPrefix(p, dirName) { bail("Dangerous filename detected: %s", zf.Name) } rc, err := zf.Open() if err != nil { bail("Failed to open file in zip: %s %s", zf.Name, err) } dir := filepath.Dir(p) err = os.MkdirAll(dir, 0777) if err != nil { bail("Failed to mkdirall %s: %s", dir, err) } f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zf.Mode()) if err != nil { bail("Failed to open %s: %s", p, err) } _, err = io.Copy(f, rc) if err != nil { bail("Failed to write to %s: %s", p, err) } err = f.Close() if err != nil { bail("Error closing %s: %s", p, err) } rc.Close() } proxyReader.Close() } } } } func errf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg, args...) if !strings.HasSuffix("\n", msg) { fmt.Fprint(os.Stderr, "\n") } } func bail(msg string, args ...interface{}) { errf(msg, args...) os.Exit(1) } func formatBytes(b int64) string { const unit = 1000 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]) } type proxyReadCloser struct { *pb.Reader bar *pb.ProgressBar } func (p *proxyReadCloser) Close() error { p.bar.Finish() return nil } func pbProxyReader(r io.Reader, size int64) io.ReadCloser { if hideProgressBar { return ioutil.NopCloser(r) } else { progressBar := pb.Full.Start64(size) progressBar.Set(pb.Bytes, true) progressBar.Set(pb.SIBytesPrefix, true) proxyReader := progressBar.NewProxyReader(r) return &proxyReadCloser{ Reader: proxyReader, bar: progressBar, } } } wormhole-william-1.0.6/cmd/send.go000066400000000000000000000117161431053014400170320ustar00rootroot00000000000000package cmd import ( "bufio" "context" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/cheggaaa/pb/v3" qrterminal "github.com/mdp/qrterminal/v3" "github.com/psanford/wormhole-william/wormhole" "github.com/spf13/cobra" ) var ( codeLen int codeFlag string sendTextFlag string showQRCode bool ) func sendCommand() *cobra.Command { cmd := cobra.Command{ Use: "send [WHAT]", Short: "Send a text message, file, or directory...", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { sendText() return } else if len(args) > 1 { bail("Too many arguments") } stat, err := os.Stat(args[0]) if err != nil { bail("Failed to read %s: %s", args[0], err) } if stat.IsDir() { sendDir(args[0]) return } else { sendFile(args[0]) return } }, } cmd.Flags().BoolVarP(&verify, "verify", "v", false, "display verification string (and wait for approval)") cmd.Flags().IntVarP(&codeLen, "code-length", "c", 0, "length of code (in bytes/words)") cmd.Flags().StringVar(&codeFlag, "code", "", "human-generated code phrase") cmd.Flags().StringVar(&sendTextFlag, "text", "", "text message to send, instead of a file.\nUse '-' to read from stdin") cmd.Flags().BoolVar(&hideProgressBar, "hide-progress", false, "suppress progress-bar display") cmd.Flags().BoolVar(&showQRCode, "qr", false, "display code as QR code (experimental)") return &cmd } func newClient() wormhole.Client { if showQRCode && codeLen == 0 { codeLen = 4 } c := wormhole.Client{ RendezvousURL: relayURL, PassPhraseComponentLength: codeLen, } if verify { c.VerifierOk = func(code string) bool { reader := bufio.NewReader(os.Stdin) fmt.Printf("Verifier %s. ok? (yes/no): ", code) yn, _ := reader.ReadString('\n') yn = strings.TrimSpace(yn) return yn == "yes" } } return c } func printInstructions(code string) { mwCmd := "wormhole receive" wwCmd := "wormhole-william recv" if verify { mwCmd = mwCmd + " --verify" wwCmd = wwCmd + " --verify" } fmt.Printf("On the other computer, please run: %s (or %s)\n", mwCmd, wwCmd) fmt.Printf("Wormhole code is: %s\n", code) if showQRCode { url := relayURL if url == "" { url = wormhole.DefaultRendezvousURL } content := fmt.Sprintf("wormhole:%s?code=%s", url, code) qrterminal.Generate(content, qrterminal.L, os.Stdout) } } func sendFile(filename string) { f, err := os.Open(filename) if err != nil { bail("Failed to open %s: %s", filename, err) } c := newClient() ctx := context.Background() var bar *pb.ProgressBar args := []wormhole.SendOption{ wormhole.WithCode(codeFlag), } if !hideProgressBar { args = append(args, wormhole.WithProgress(func(sentBytes int64, totalBytes int64) { if bar == nil { bar = pb.Full.Start64(totalBytes) bar.Set(pb.Bytes, true) bar.Set(pb.SIBytesPrefix, true) } bar.SetCurrent(sentBytes) if sentBytes == totalBytes { bar.Finish() } })) } code, status, err := c.SendFile(ctx, filepath.Base(filename), f, args...) if err != nil { bail("Error sending message: %s", err) } printInstructions(code) s := <-status if s.OK { fmt.Println("file sent") } else { bail("Send error: %s", s.Error) } } func sendDir(dirpath string) { dirpath = strings.TrimSuffix(dirpath, "/") stat, err := os.Stat(dirpath) if err != nil { log.Fatal(err) } if !stat.IsDir() { log.Fatalf("%s is not a directory", dirpath) } prefix, dirname := filepath.Split(dirpath) var entries []wormhole.DirectoryEntry filepath.Walk(dirpath, func(path string, info os.FileInfo, err error) error { if info.IsDir() { return nil } if !info.Mode().IsRegular() { return nil } relPath := strings.TrimPrefix(path, prefix) entries = append(entries, wormhole.DirectoryEntry{ Path: relPath, Mode: info.Mode(), Reader: func() (io.ReadCloser, error) { return os.Open(path) }, }) return nil }) c := newClient() ctx := context.Background() code, status, err := c.SendDirectory(ctx, dirname, entries, wormhole.WithCode(codeFlag)) if err != nil { log.Fatal(err) } printInstructions(code) s := <-status if s.OK { fmt.Println("directory sent") } else { bail("Send error: %s", s.Error) } } func sendText() { c := newClient() var msg string if sendTextFlag == "-" { data, err := ioutil.ReadAll(os.Stdin) if err != nil { bail("Read stdin err: %s", err) } msg = string(data) } else if sendTextFlag != "" { msg = sendTextFlag } else { reader := bufio.NewReader(os.Stdin) fmt.Print("Text to send: ") msg, _ = reader.ReadString('\n') msg = strings.TrimSpace(msg) } ctx := context.Background() code, status, err := c.SendText(ctx, msg, wormhole.WithCode(codeFlag)) if err != nil { log.Fatal(err) } printInstructions(code) s := <-status if s.Error != nil { log.Fatalf("Send error: %s", s.Error) } else if s.OK { fmt.Println("text message sent") } else { log.Fatalf("Hmm not ok but also not error") } } wormhole-william-1.0.6/examples/000077500000000000000000000000001431053014400166175ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-recv-dir/000077500000000000000000000000001431053014400236205ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-recv-dir/wormhole-william-recv-dir.go000066400000000000000000000034011431053014400311460ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/klauspost/compress/zip" "github.com/psanford/wormhole-william/wormhole" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) os.Exit(1) } code := os.Args[1] var c wormhole.Client ctx := context.Background() msg, err := c.Receive(ctx, code) if err != nil { log.Fatal(err) } log.Printf("got msg: %+v\n", msg) wd, err := os.Getwd() if err != nil { log.Fatal(err) } tmpFile, err := ioutil.TempFile(wd, msg.Name+".zip.tmp") if err != nil { log.Fatal(err) } defer tmpFile.Close() defer os.Remove(tmpFile.Name()) n, err := io.Copy(tmpFile, msg) if err != nil { log.Fatal("readfull error", err) } zr, err := zip.NewReader(tmpFile, n) if err != nil { log.Fatalf("Read zip error: %s", err) } dirName := filepath.Join(wd, msg.Name) for _, zf := range zr.File { p, err := filepath.Abs(filepath.Join(dirName, zf.Name)) if err != nil { log.Fatalf("Failes to calculate file path ABS: %s", err) } if !strings.HasPrefix(p, dirName) { log.Fatalf("Dangerous filename detected: %s", zf.Name) } rc, err := zf.Open() if err != nil { log.Fatalf("Failed to open file in zip: %s %s", zf.Name, err) } dir := filepath.Dir(p) err = os.MkdirAll(dir, 0777) if err != nil { log.Fatalf("Failed to mkdirall %s: %s", dir, err) } f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zf.Mode()) if err != nil { log.Fatalf("Failed to open %s: %s", p, err) } _, err = io.Copy(f, rc) if err != nil { log.Fatalf("Failed to write to %s: %s", p, err) } err = f.Close() if err != nil { log.Fatalf("Error closing %s: %s", p, err) } rc.Close() } } wormhole-william-1.0.6/examples/wormhole-william-recv-file/000077500000000000000000000000001431053014400237615ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-recv-file/wormhole-william-recv-file.go000066400000000000000000000010031431053014400314440ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "os" "github.com/psanford/wormhole-william/wormhole" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) os.Exit(1) } code := os.Args[1] var c wormhole.Client ctx := context.Background() fileInfo, err := c.Receive(ctx, code) if err != nil { log.Fatal(err) } log.Printf("got msg: %+v\n", fileInfo) _, err = io.Copy(os.Stdout, fileInfo) if err != nil { log.Fatal("readfull error", err) } } wormhole-william-1.0.6/examples/wormhole-william-recv-text/000077500000000000000000000000001431053014400240265ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-recv-text/wormhole-william-recv.go000066400000000000000000000011511431053014400306000ustar00rootroot00000000000000package main import ( "context" "fmt" "io/ioutil" "log" "os" "github.com/psanford/wormhole-william/wormhole" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) os.Exit(1) } code := os.Args[1] var c wormhole.Client ctx := context.Background() msg, err := c.Receive(ctx, code) if err != nil { log.Fatal(err) } if msg.Type != wormhole.TransferText { log.Fatalf("Expected a text message but got type %s", msg.Type) } msgBody, err := ioutil.ReadAll(msg) if err != nil { log.Fatal(err) } fmt.Println("got message:") fmt.Println(msgBody) } wormhole-william-1.0.6/examples/wormhole-william-send-dir/000077500000000000000000000000001431053014400236125ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-send-dir/wormhole-william-send-dir.go000066400000000000000000000025721431053014400311420ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "os" "path/filepath" "strings" "github.com/psanford/wormhole-william/wormhole" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) os.Exit(1) } dirpath := os.Args[1] dirpath = strings.TrimSuffix(dirpath, "/") stat, err := os.Stat(dirpath) if err != nil { log.Fatal(err) } if !stat.IsDir() { log.Fatalf("%s is not a directory", dirpath) } prefix, dirname := filepath.Split(dirpath) var entries []wormhole.DirectoryEntry filepath.Walk(dirpath, func(path string, info os.FileInfo, err error) error { if info.IsDir() { return nil } if !info.Mode().IsRegular() { return nil } relPath := strings.TrimPrefix(path, prefix) entries = append(entries, wormhole.DirectoryEntry{ Path: relPath, Mode: info.Mode(), Reader: func() (io.ReadCloser, error) { return os.Open(path) }, }) return nil }) var c wormhole.Client ctx := context.Background() code, status, err := c.SendDirectory(ctx, dirname, entries) if err != nil { log.Fatal(err) } fmt.Println("On the other computer, please run: wormhole receive") fmt.Printf("Wormhole code is: %s\n", code) s := <-status if s.Error != nil { log.Fatalf("Send error: %s", s.Error) } else if s.OK { fmt.Println("OK!") } else { log.Fatalf("Hmm not ok but also not error") } } wormhole-william-1.0.6/examples/wormhole-william-send-file/000077500000000000000000000000001431053014400237535ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-send-file/wormhole-william-send-file.go000066400000000000000000000013501431053014400314350ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "github.com/psanford/wormhole-william/wormhole" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) os.Exit(1) } filename := os.Args[1] f, err := os.Open(filename) if err != nil { log.Fatal(err) } var c wormhole.Client ctx := context.Background() code, status, err := c.SendFile(ctx, filename, f) if err != nil { log.Fatal(err) } fmt.Println("On the other computer, please run: wormhole receive") fmt.Printf("Wormhole code is: %s\n", code) s := <-status if s.Error != nil { log.Fatalf("Send error: %s", s.Error) } else if s.OK { fmt.Println("OK!") } else { log.Fatalf("Hmm not ok but also not error") } } wormhole-william-1.0.6/examples/wormhole-william-send-text/000077500000000000000000000000001431053014400240205ustar00rootroot00000000000000wormhole-william-1.0.6/examples/wormhole-william-send-text/wormhole-william-send-text.go000066400000000000000000000012511431053014400315470ustar00rootroot00000000000000package main import ( "bufio" "context" "fmt" "log" "os" "github.com/psanford/wormhole-william/wormhole" ) func main() { var c wormhole.Client reader := bufio.NewReader(os.Stdin) fmt.Print("Text to send: ") msg, _ := reader.ReadString('\n') msg = msg[:len(msg)-1] ctx := context.Background() code, status, err := c.SendText(ctx, msg) if err != nil { log.Fatal(err) } fmt.Println("On the other computer, please run: wormhole receive") fmt.Printf("Wormhole code is: %s\n", code) s := <-status if s.Error != nil { log.Fatalf("Send error: %s", s.Error) } else if s.OK { fmt.Println("OK!") } else { log.Fatalf("Hmm not ok but also not error") } } wormhole-william-1.0.6/go.mod000066400000000000000000000007201431053014400161060ustar00rootroot00000000000000module github.com/psanford/wormhole-william go 1.12 require ( github.com/cheggaaa/pb/v3 v3.0.8 github.com/gorilla/websocket v1.4.2 github.com/klauspost/compress v1.11.13 github.com/mdp/qrterminal/v3 v3.0.0 github.com/spf13/cobra v1.1.3 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect nhooyr.io/websocket v1.8.6 salsa.debian.org/vasudev/gospake2 v0.0.0-20180813171123-adcc69dd31d5 ) wormhole-william-1.0.6/go.sum000066400000000000000000001070771431053014400161500ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= salsa.debian.org/vasudev/gospake2 v0.0.0-20180813171123-adcc69dd31d5 h1:j+F9fFxAFNdrO85XnERJSYS5QGfPUWUy9IP4s9BkV6A= salsa.debian.org/vasudev/gospake2 v0.0.0-20180813171123-adcc69dd31d5/go.mod h1:soKzqXBAtqHTODjyA0VzH2iERtpzN1w65eZUfetn2cQ= wormhole-william-1.0.6/internal/000077500000000000000000000000001431053014400166155ustar00rootroot00000000000000wormhole-william-1.0.6/internal/crypto/000077500000000000000000000000001431053014400201355ustar00rootroot00000000000000wormhole-william-1.0.6/internal/crypto/crypto.go000066400000000000000000000012131431053014400220010ustar00rootroot00000000000000package crypto import ( "crypto/rand" "fmt" "io" ) // RandSideID returns a string appropate for use // as the Side ID for a client. func RandSideID() string { return RandHex(5) } // RandHex generates secure random bytes of byteCount long // and returns that in hex encoded string format func RandHex(byteCount int) string { buf := make([]byte, byteCount) _, err := io.ReadFull(rand.Reader, buf) if err != nil { panic(err) } return fmt.Sprintf("%x", buf) } func RandNonce() [NonceSize]byte { var nonce [NonceSize]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { panic(err) } return nonce } const NonceSize = 24 wormhole-william-1.0.6/main.go000066400000000000000000000003101431053014400162460ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/psanford/wormhole-william/cmd" ) func main() { err := cmd.Execute() if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } } wormhole-william-1.0.6/rendezvous/000077500000000000000000000000001431053014400172055ustar00rootroot00000000000000wormhole-william-1.0.6/rendezvous/client.go000066400000000000000000000343701431053014400210210ustar00rootroot00000000000000// Package rendezvous provides a client for magic wormhole rendezvous servers. package rendezvous import ( "context" "encoding/json" "errors" "fmt" "reflect" "sync" "sync/atomic" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous/internal/msgs" "github.com/psanford/wormhole-william/version" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" ) // NewClient returns a Rendezvous client. URL is the websocket // url of Rendezvous server. SideID is the id for the client to // use to distinguish messages in a mailbox from the other client. // AppID is the application identity string of the client. // // Two clients can only communicate if they have the same AppID. func NewClient(url, sideID, appID string, opts ...ClientOption) *Client { c := &Client{ url: url, sideID: sideID, appID: appID, pendingMsgs: make([]pendingMsg, 0, 2), mailboxMsgs: make([]MailboxEvent, 0), pendingMailboxWaiters: make(map[uint32]chan int), pendingMsgWaiters: make(map[uint32]chan uint32), } for _, opt := range opts { opt.setValue(c) } return c } type pendingMsg struct { // id will be monotonically increasing for each received // message so waiters can know if they have seen all the // pending messages or not id uint32 msgType string raw []byte } type Client struct { url string appID string sideID string mailboxID string nameplate string agentString string agentVersion string wsClient *websocket.Conn mailboxMsgs []MailboxEvent pendingMailboxWaiters map[uint32]chan int pendingMsgIDCntr uint32 pendingMsgWaiterCntr uint32 sendCmdMu sync.Mutex pendingMsgMu sync.Mutex pendingMsgs []pendingMsg pendingMsgWaiters map[uint32]chan uint32 clientState clientState err error } type MailboxEvent struct { // Error will be non nil if an error occurred // while waiting for messages Error error Side string Phase string Body string } type clientState int32 const ( statePending clientState = iota stateOpen stateError stateClosed ) func (c clientState) String() string { switch c { case statePending: return "Pending" case stateOpen: return "Open" case stateError: return "Error" case stateClosed: return "Closed" default: return fmt.Sprintf("Unknown client state: %d", c) } } func (c *Client) closeWithError(err error) { atomic.StoreInt32((*int32)(&c.clientState), int32(stateError)) c.err = err } type ConnectInfo struct { MOTD string CurrentCLIVersion string } // Connect opens a connection and binds to the rendezvous server. It // returns the Welcome information the server responds with. func (c *Client) Connect(ctx context.Context) (*ConnectInfo, error) { swapped := atomic.CompareAndSwapInt32((*int32)(&c.clientState), int32(statePending), int32(stateOpen)) if !swapped { return nil, fmt.Errorf("current client state %s != pending, cannot connect", c.clientState) } var err error c.wsClient, _, err = websocket.Dial(ctx, c.url, nil) if err != nil { wrappedErr := fmt.Errorf("dial %s: %s", c.url, err) c.closeWithError(wrappedErr) return nil, wrappedErr } go c.readMessages(ctx) var welcome msgs.Welcome err = c.readMsg(ctx, &welcome) if err != nil { c.closeWithError(err) return nil, err } if welcome.Welcome.Error != "" { err := fmt.Errorf("server error: %s", err) c.closeWithError(err) return nil, err } if err := c.bind(ctx, c.sideID, c.appID); err != nil { c.closeWithError(err) return nil, err } info := ConnectInfo{ MOTD: welcome.Welcome.MOTD, CurrentCLIVersion: welcome.Welcome.CurrentCLIVersion, } return &info, nil } func (c *Client) searchPendingMsgs(ctx context.Context, msgType string) *pendingMsg { c.pendingMsgMu.Lock() defer c.pendingMsgMu.Unlock() for i, pending := range c.pendingMsgs { if pending.msgType == msgType { copyMsg := pending orig := c.pendingMsgs c.pendingMsgs = c.pendingMsgs[:i] c.pendingMsgs = append(c.pendingMsgs, orig[i+1:]...) return ©Msg } } return nil } func (c *Client) registerWaiter() (uint32, <-chan uint32) { nextID := atomic.AddUint32(&c.pendingMsgWaiterCntr, 1) ch := make(chan uint32, 1) c.pendingMsgMu.Lock() defer c.pendingMsgMu.Unlock() c.pendingMsgWaiters[nextID] = ch if len(c.pendingMsgs) > 0 { ch <- c.pendingMsgs[len(c.pendingMsgs)-1].id } return nextID, ch } func (c *Client) deregisterWaiter(id uint32) { c.pendingMsgMu.Lock() defer c.pendingMsgMu.Unlock() delete(c.pendingMsgWaiters, id) } func (c *Client) readMsg(ctx context.Context, m interface{}) error { expectMsgType := msgType(m) waiterID, ch := c.registerWaiter() defer c.deregisterWaiter(waiterID) for { select { case <-ch: case <-ctx.Done(): return ctx.Err() } msg := c.searchPendingMsgs(ctx, expectMsgType) if msg != nil { err := json.Unmarshal(msg.raw, m) if err != nil { wrappedErr := fmt.Errorf("JSON unmarshal: %s", err) return wrappedErr } return nil } } } func msgType(msg interface{}) string { ptr := reflect.TypeOf(msg) if ptr.Kind() != reflect.Ptr { panic("msg must be a pointer") } structType := ptr.Elem() for i := 0; i < structType.NumField(); i++ { field := structType.Field(i) if field.Tag.Get("json") == "type" { return field.Tag.Get("rendezvous_value") } } return "" } // CreateMailbox allocates a nameplate, claims it, and then opens // the associated mailbox. It returns the nameplate id string. func (c *Client) CreateMailbox(ctx context.Context) (string, error) { nameplateResp, err := c.allocateNameplate(ctx) if err != nil { c.closeWithError(err) return "", err } claimed, err := c.claimNameplate(ctx, nameplateResp.Nameplate) if err != nil { c.closeWithError(err) return "", err } c.nameplate = nameplateResp.Nameplate err = c.openMailbox(ctx, claimed.Mailbox) if err != nil { c.closeWithError(err) return "", err } return nameplateResp.Nameplate, nil } // AttachMailbox opens an existing mailbox and releases the associated // nameplate. func (c *Client) AttachMailbox(ctx context.Context, nameplate string) error { claimed, err := c.claimNameplate(ctx, nameplate) if err != nil { c.closeWithError(err) return err } c.nameplate = nameplate err = c.openMailbox(ctx, claimed.Mailbox) if err != nil { c.closeWithError(err) return err } return nil } // ListNameplates returns a list of active nameplates on the // rendezvous server. func (c *Client) ListNameplates(ctx context.Context) ([]string, error) { var ( nameplatesResp msgs.Nameplates listReq msgs.List ) _, err := c.sendAndWait(ctx, &listReq) if err != nil { return nil, err } err = c.readMsg(ctx, &nameplatesResp) if err != nil { return nil, err } outNameplates := make([]string, len(nameplatesResp.Nameplates)) for i, np := range nameplatesResp.Nameplates { outNameplates[i] = np.ID } return outNameplates, nil } // AddMessage adds a message to the opened mailbox. This must be called after // either CreateMailbox or AttachMailbox. func (c *Client) AddMessage(ctx context.Context, phase, body string) error { addReq := msgs.Add{ Phase: phase, Body: body, } _, err := c.sendAndWait(ctx, &addReq) return err } // MsgChan returns a channel of Mailbox message events. // Each message from the other side will be published to this channel. func (c *Client) MsgChan(ctx context.Context) <-chan MailboxEvent { resultChan := make(chan MailboxEvent) go c.recvMailboxMsgs(ctx, resultChan) return resultChan } func (c *Client) recvMailboxMsgs(ctx context.Context, outCh chan MailboxEvent) { id, notified := c.registerMailboxWaiter() defer c.deregisterMailboxWaiter(id) nextOffset := 0 var nextMsg *MailboxEvent OUTER: for { // loop over all pending messages we haven't sent // to outCh yet for { c.pendingMsgMu.Lock() if len(c.mailboxMsgs)-1 >= nextOffset { nextMsg = &c.mailboxMsgs[nextOffset] } c.pendingMsgMu.Unlock() if nextMsg == nil { break } nextOffset++ if nextMsg.Side != c.sideID { // Only send messages from the other side outCh <- *nextMsg if c.nameplate != "" { // release the nameplate when we get a response from the other side c.releaseNameplate(ctx, c.nameplate) c.nameplate = "" } } nextMsg = nil } // wait for any new mailbox messages _, ok := <-notified if !ok { break OUTER } } close(outCh) } func (c *Client) registerMailboxWaiter() (uint32, <-chan int) { nextID := atomic.AddUint32(&c.pendingMsgWaiterCntr, 1) ch := make(chan int, 1) c.pendingMsgMu.Lock() defer c.pendingMsgMu.Unlock() c.pendingMailboxWaiters[nextID] = ch return nextID, ch } func (c *Client) deregisterMailboxWaiter(id uint32) { c.pendingMsgMu.Lock() defer c.pendingMsgMu.Unlock() delete(c.pendingMailboxWaiters, id) } type Mood string const ( Happy Mood = "happy" Lonely Mood = "lonely" Scary Mood = "scary" Errory Mood = "errory" ) // Close sends mood to server and then tears down the connection. func (c *Client) Close(ctx context.Context, mood Mood) error { if mood == "" { mood = Happy } if c.wsClient == nil { return errors.New("Close called on non-open rendezvous connection") } defer func() { if c.wsClient != nil { c.wsClient.Close(websocket.StatusNormalClosure, "") c.wsClient = nil } }() var closedResp msgs.ClosedResp closeReq := msgs.Close{ Mood: string(mood), Mailbox: c.mailboxID, } _, err := c.sendAndWait(ctx, &closeReq) if err != nil { return err } err = c.readMsg(ctx, &closedResp) return err } // sendAndWait sends a message to the rendezvous server and waits // for an ack response. func (c *Client) sendAndWait(ctx context.Context, msg interface{}) (*msgs.Ack, error) { id, err := c.prepareMsg(msg) if err != nil { return nil, err } c.sendCmdMu.Lock() err = wsjson.Write(ctx, c.wsClient, msg) if err != nil { c.sendCmdMu.Unlock() return nil, err } var ack msgs.Ack err = c.readMsg(ctx, &ack) if err != nil { c.sendCmdMu.Unlock() return nil, err } c.sendCmdMu.Unlock() if ack.ID != id { return nil, fmt.Errorf("got ack for different message. got %s send: %+v", ack.ID, msg) } return &ack, nil } // prepareMsg populates the ID and Type fields for a message. // It returns the ID string or an error. func (c *Client) prepareMsg(msg interface{}) (string, error) { id := crypto.RandHex(2) ptr := reflect.TypeOf(msg) if ptr.Kind() != reflect.Ptr { return "", errors.New("msg must be a pointer") } st := ptr.Elem() val := reflect.ValueOf(msg).Elem() var ( foundType bool foundID bool ) for i := 0; i < st.NumField(); i++ { field := st.Field(i) if field.Name == "Type" { msgType, _ := field.Tag.Lookup("rendezvous_value") if msgType == "" { return "", errors.New("type filed missing rendezvous_value struct tag") } ff := val.Field(i) ff.SetString(msgType) foundType = true } else if field.Name == "ID" { ff := val.Field(i) ff.SetString(id) foundID = true } } if !foundID || !foundType { return id, errors.New("msg type missing required field(s): Type and/or ID") } return id, nil } func (c *Client) agentID() (string, string) { agent := c.agentString if agent == "" { agent = version.AgentString } v := c.agentVersion if v == "" { v = version.AgentVersion } return agent, v } func (c *Client) bind(ctx context.Context, side, appID string) error { agent, version := c.agentID() bind := msgs.Bind{ Side: side, AppID: appID, ClientVersion: []string{agent, version}, } _, err := c.sendAndWait(ctx, &bind) return err } func (c *Client) allocateNameplate(ctx context.Context) (*msgs.AllocatedResp, error) { var ( allocReq msgs.Allocate allocedResp msgs.AllocatedResp ) _, err := c.sendAndWait(ctx, &allocReq) if err != nil { return nil, err } err = c.readMsg(ctx, &allocedResp) if err != nil { return nil, err } return &allocedResp, nil } func (c *Client) claimNameplate(ctx context.Context, nameplate string) (*msgs.ClaimedResp, error) { var claimResp msgs.ClaimedResp claimReq := msgs.Claim{ Nameplate: nameplate, } _, err := c.sendAndWait(ctx, &claimReq) if err != nil { return nil, err } err = c.readMsg(ctx, &claimResp) if err != nil { return nil, err } return &claimResp, nil } func (c *Client) releaseNameplate(ctx context.Context, nameplate string) error { var releasedResp msgs.ReleasedResp releaseReq := msgs.Release{ Nameplate: nameplate, } _, err := c.sendAndWait(ctx, &releaseReq) if err != nil { return err } err = c.readMsg(ctx, &releasedResp) if err != nil { return err } return nil } func (c *Client) openMailbox(ctx context.Context, mailbox string) error { c.pendingMsgMu.Lock() c.mailboxID = mailbox c.pendingMsgMu.Unlock() open := msgs.Open{ Mailbox: mailbox, } _, err := c.sendAndWait(ctx, &open) return err } // readMessages reads off the websocket and dispatches messages // to either pendingMsg or pendingMailboxMsg. func (c *Client) readMessages(ctx context.Context) { for { if err := ctx.Err(); err != nil { c.closeWithError(err) break } _, msg, err := c.wsClient.Read(ctx) if err != nil { wrappedErr := fmt.Errorf("WS Read: %s", err) c.closeWithError(wrappedErr) break } var genericMsg msgs.GenericServerMsg err = json.Unmarshal(msg, &genericMsg) if err != nil { wrappedErr := fmt.Errorf("JSON unmarshal: %s", err) c.closeWithError(wrappedErr) break } if genericMsg.Type == "message" { var mm msgs.Message err := json.Unmarshal(msg, &mm) if err != nil { wrappedErr := fmt.Errorf("JSON unmarshal: %s", err) c.closeWithError(wrappedErr) break } mboxMsg := MailboxEvent{ Side: mm.Side, Phase: mm.Phase, Body: mm.Body, } c.pendingMsgMu.Lock() c.mailboxMsgs = append(c.mailboxMsgs, mboxMsg) maxOffset := len(c.mailboxMsgs) - 1 for _, waiter := range c.pendingMailboxWaiters { select { case waiter <- maxOffset: default: } } c.pendingMsgMu.Unlock() } else { nextID := atomic.AddUint32(&c.pendingMsgIDCntr, 1) c.pendingMsgMu.Lock() c.pendingMsgs = append(c.pendingMsgs, pendingMsg{ id: nextID, msgType: genericMsg.Type, raw: msg, }) for _, waiter := range c.pendingMsgWaiters { select { case waiter <- nextID: default: } } c.pendingMsgMu.Unlock() } } } wormhole-william-1.0.6/rendezvous/client_test.go000066400000000000000000000054321431053014400220550ustar00rootroot00000000000000package rendezvous import ( "context" "reflect" "testing" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous/rendezvousservertest" "github.com/psanford/wormhole-william/version" ) func TestBasicClient(t *testing.T) { ts := rendezvousservertest.NewServer() defer ts.Close() side0 := crypto.RandSideID() side1 := crypto.RandSideID() appID := "superlatively-abbeys" c0 := NewClient(ts.WebSocketURL(), side0, appID) ctx := context.Background() info, err := c0.Connect(ctx) if err != nil { t.Fatal(err) } if info.MOTD != rendezvousservertest.TestMotd { t.Fatalf("MOTD got=%s expected=%s", info.MOTD, rendezvousservertest.TestMotd) } gotAgents := ts.Agents() expectAgents := [][]string{ {version.AgentString, version.AgentVersion}, } if !reflect.DeepEqual(gotAgents, expectAgents) { t.Fatalf("got agents=%v expected=%v", gotAgents, expectAgents) } nameplate, err := c0.CreateMailbox(ctx) if err != nil { t.Fatal(err) } c1 := NewClient(ts.WebSocketURL(), side1, appID) _, err = c1.Connect(ctx) if err != nil { t.Fatal(err) } err = c1.AttachMailbox(ctx, nameplate) if err != nil { t.Fatal(err) } phase0 := "seacoasts-demonstrator" body0 := "Roquefort-Gilligan" err = c0.AddMessage(ctx, phase0, body0) if err != nil { t.Fatal(err) } c0Msgs := c0.MsgChan(ctx) c1Msgs := c1.MsgChan(ctx) msg := <-c1Msgs expectMsg := MailboxEvent{ Side: side0, Phase: phase0, Body: body0, } if !reflect.DeepEqual(expectMsg, msg) { t.Fatalf("Message mismatch got=%+v, expect=%+v", msg, expectMsg) } select { case m := <-c0Msgs: t.Fatalf("c0 got message when it wasn't expecting one: %+v", m) default: } phase1 := "fundamentalists-potluck" body1 := "sanitarium-seasonings" err = c1.AddMessage(ctx, phase1, body1) if err != nil { t.Fatal(err) } msg = <-c0Msgs expectMsg = MailboxEvent{ Side: side1, Phase: phase1, Body: body1, } if !reflect.DeepEqual(expectMsg, msg) { t.Fatalf("Message mismatch got=%+v, expect=%+v", msg, expectMsg) } select { case m := <-c1Msgs: t.Fatalf("c1 got message when it wasn't expecting one: %+v", m) default: } } func TestCustomUserAgent(t *testing.T) { ts := rendezvousservertest.NewServer() defer ts.Close() side0 := crypto.RandSideID() appID := "nightclubs-reasonableness" agentString := "deafening-buttermilk" agentVersion := "v1.2.3" c0 := NewClient(ts.WebSocketURL(), side0, appID, WithVersion(agentString, agentVersion)) ctx := context.Background() _, err := c0.Connect(ctx) if err != nil { t.Fatal(err) } gotAgents := ts.Agents() expectAgents := [][]string{ {agentString, agentVersion}, } if !reflect.DeepEqual(gotAgents, expectAgents) { t.Fatalf("got agents=%v expected=%v", gotAgents, expectAgents) } c0.Close(ctx, "") } wormhole-william-1.0.6/rendezvous/internal/000077500000000000000000000000001431053014400210215ustar00rootroot00000000000000wormhole-william-1.0.6/rendezvous/internal/msgs/000077500000000000000000000000001431053014400217725ustar00rootroot00000000000000wormhole-william-1.0.6/rendezvous/internal/msgs/msgs.go000066400000000000000000000104401431053014400232710ustar00rootroot00000000000000package msgs // Server sent wecome message type Welcome struct { Type string `json:"type" rendezvous_value:"welcome"` Welcome WelcomeServerInfo `json:"welcome"` ServerTX float64 `json:"server_tx"` } type WelcomeServerInfo struct { MOTD string `json:"motd"` CurrentCLIVersion string `json:"current_cli_version"` Error string `json:"error"` } // Client sent bind message type Bind struct { Type string `json:"type" rendezvous_value:"bind"` ID string `json:"id"` Side string `json:"side"` AppID string `json:"appid"` // ClientVersion is by convention a two value array // of [client_id, version] ClientVersion []string `json:"client_version"` } // Client sent aollocate message type Allocate struct { Type string `json:"type" rendezvous_value:"allocate"` ID string `json:"id"` } // Server sent ack message type Ack struct { Type string `json:"type" rendezvous_value:"ack"` ID string `json:"id"` ServerTX float64 `json:"server_tx"` } // Server sent allocated message type AllocatedResp struct { Type string `json:"type" rendezvous_value:"allocated"` Nameplate string `json:"nameplate"` ServerTX float64 `json:"server_tx"` } // Client sent claim message type Claim struct { Type string `json:"type" rendezvous_value:"claim"` ID string `json:"id"` Nameplate string `json:"nameplate"` } // Server sent claimed message type ClaimedResp struct { Type string `json:"type" rendezvous_value:"claimed"` Mailbox string `json:"mailbox"` ServerTX float64 `json:"server_tx"` } // Client sent open message type Open struct { Type string `json:"type" rendezvous_value:"open"` ID string `json:"id"` Mailbox string `json:"mailbox"` } // Client sent add message to add a message to a mailbox. type Add struct { Type string `json:"type" rendezvous_value:"add"` ID string `json:"id"` Phase string `json:"phase"` // Body is a hex string encoded json submessage Body string `json:"body"` } // Server sent message message type Message struct { Type string `json:"type" rendezvous_value:"message"` ID string `json:"id"` Side string `json:"side"` Phase string `json:"phase"` // Body is a hex string encoded json submessage Body string `json:"body"` ServerRX float64 `json:"server_rx"` ServerTX float64 `json:"server_tx"` } // Client sent list message to list nameplates. type List struct { Type string `json:"type" rendezvous_value:"list"` ID string `json:"id"` } // Server sent nameplates message. // The server sends this in response to ListMsg. // It contains the list of active nameplates. type Nameplates struct { Type string `json:"type" rendezvous_value:"nameplates"` Nameplates []struct { ID string `json:"id"` } `json:"nameplates"` ServerTX float64 `json:"server_tx"` } // Client sent release message to release a nameplate. type Release struct { Type string `json:"type" rendezvous_value:"release"` ID string `json:"id"` Nameplate string `json:"nameplate"` } // Server sent response to release request. type ReleasedResp struct { Type string `json:"type" rendezvous_value:"released"` ServerTX float64 `json:"server_tx"` } // Server sent error message type Error struct { Type string `json:"type" rendezvous_value:"error"` Error string `json:"error"` Orig interface{} `json:"orig"` ServerTx float64 `json:"server_tx"` } type Close struct { Type string `json:"type" rendezvous_value:"close"` ID string `json:"id"` Mailbox string `json:"mailbox"` Mood string `json:"mood"` } type ClosedResp struct { Type string `json:"type" rendezvous_value:"closed"` ServerTx float64 `json:"server_tx"` } type GenericServerMsg struct { Type string `json:"type"` ServerTX float64 `json:"server_tx"` ID string `json:"id"` Error string `json:"error"` } var MsgMap = map[string]interface{}{ "welcome": Welcome{}, "bind": Bind{}, "allocate": Allocate{}, "ack": Ack{}, "allocated": AllocatedResp{}, "claim": Claim{}, "claimed": ClaimedResp{}, "open": Open{}, "add": Add{}, "message": Message{}, "list": List{}, "nameplates": Nameplates{}, "release": Release{}, "released": ReleasedResp{}, "error": Error{}, "close": Close{}, "closed": ClosedResp{}, } wormhole-william-1.0.6/rendezvous/internal/msgs/msgs_test.go000066400000000000000000000007001431053014400243260ustar00rootroot00000000000000package msgs import ( "reflect" "testing" ) func TestStructTags(t *testing.T) { for n, iface := range MsgMap { st := reflect.TypeOf(iface) for i := 0; i < st.NumField(); i++ { field := st.Field(i) if field.Name == "Type" { tagVal, _ := field.Tag.Lookup("rendezvous_value") if tagVal != n { t.Errorf("msgMap key / Type struct tag rendezvous_value mismatch: key=%s tag=%s struct=%T", n, tagVal, iface) } } } } } wormhole-william-1.0.6/rendezvous/options.go000066400000000000000000000010231431053014400212230ustar00rootroot00000000000000package rendezvous type ClientOption interface { setValue(*Client) } type versionOption struct { agentString string agentVersion string } func (o *versionOption) setValue(c *Client) { c.agentString = o.agentString c.agentVersion = o.agentVersion } // WithVersion returns a ClientOption to override the default client // identifier and version reported to the rendezvous server. func WithVersion(agentID string, version string) ClientOption { return &versionOption{ agentString: agentID, agentVersion: version, } } wormhole-william-1.0.6/rendezvous/rendezvousservertest/000077500000000000000000000000001431053014400235405ustar00rootroot00000000000000wormhole-william-1.0.6/rendezvous/rendezvousservertest/rendezvousservertest.go000066400000000000000000000172641431053014400304340ustar00rootroot00000000000000package rendezvousservertest import ( "encoding/json" "errors" "fmt" "io" "log" "math" "net/http" "net/http/httptest" "net/url" "reflect" "strconv" "sync" "time" "github.com/gorilla/websocket" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous/internal/msgs" ) type TestServer struct { *httptest.Server mu sync.Mutex mailboxes map[string]*mailbox nameplates map[int16]string agents [][]string } func NewServer() *TestServer { ts := &TestServer{ mailboxes: make(map[string]*mailbox), nameplates: make(map[int16]string), } smux := http.NewServeMux() smux.HandleFunc("/ws", ts.handleWS) ts.Server = httptest.NewServer(smux) return ts } func (ts *TestServer) Agents() [][]string { ts.mu.Lock() defer ts.mu.Unlock() return ts.agents } func (ts *TestServer) WebSocketURL() string { u, err := url.Parse(ts.URL) if err != nil { panic(err) } u.Scheme = "ws" u.Path = "/ws" return u.String() } type mailbox struct { sync.Mutex claimCount int msgs []mboxMsg clients map[string]chan mboxMsg } func newMailbox() *mailbox { return &mailbox{ msgs: make([]mboxMsg, 0, 4), clients: make(map[string]chan mboxMsg), } } func (m *mailbox) Add(side string, addMsg *msgs.Add) { m.Lock() defer m.Unlock() msg := mboxMsg{ side: side, phase: addMsg.Phase, body: addMsg.Body, } m.msgs = append(m.msgs, msg) for side, c := range m.clients { select { case c <- msg: case <-time.After(1 * time.Second): log.Printf("Send to %s timed out", side) } } } type mboxMsg struct { side string phase string body string } var wsUpgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func prepareServerMsg(msg interface{}) { ptr := reflect.TypeOf(msg) if ptr.Kind() != reflect.Ptr { panic(fmt.Sprintf("msg must be a pointer to a struct, got %T", msg)) } st := ptr.Elem() if st.Kind() != reflect.Struct { panic(fmt.Sprintf("msg must be a pointer to a struct, got %T", msg)) } val := reflect.ValueOf(msg).Elem() for i := 0; i < st.NumField(); i++ { field := st.Field(i) jsonName := field.Tag.Get("json") if jsonName == "type" { msgType := field.Tag.Get("rendezvous_value") if msgType == "" { panic("Type filed missing rendezvous_value struct tag") } ff := val.Field(i) ff.SetString(msgType) } else if jsonName == "ServerTX" { ff := val.Field(i) ff.SetFloat(float64(time.Now().UnixNano()) / float64(time.Second)) } } } func serverUnmarshal(m []byte) (interface{}, error) { var genericMsg msgs.GenericServerMsg err := json.Unmarshal(m, &genericMsg) if err != nil { return nil, err } protoType, found := msgs.MsgMap[genericMsg.Type] if !found { return nil, fmt.Errorf("unknown msg type: %s %v %s", genericMsg.Type, genericMsg, m) } t := reflect.TypeOf(protoType) val := reflect.New(t) resultPtr := val.Interface() err = json.Unmarshal(m, resultPtr) if err != nil { return nil, err } return resultPtr, nil } var TestMotd = "ordure-posts" func (ts *TestServer) handleWS(w http.ResponseWriter, r *http.Request) { c, err := wsUpgrader.Upgrade(w, r, nil) if err != nil { panic(err) } defer c.Close() var sendMu sync.Mutex sendMsg := func(msg interface{}) { prepareServerMsg(msg) sendMu.Lock() defer sendMu.Unlock() err = c.WriteJSON(msg) if err != nil { panic(err) } } welcome := &msgs.Welcome{ Welcome: msgs.WelcomeServerInfo{ MOTD: TestMotd, }, } sendMsg(welcome) ackMsg := func(id string) { ack := &msgs.Ack{ ID: id, } sendMsg(ack) } errMsg := func(id string, orig interface{}, reason error) { errPacket := &msgs.Error{ Error: reason.Error(), Orig: orig, } sendMsg(errPacket) } var sideID string var openMailbox *mailbox defer func() { if sideID != "" && openMailbox != nil { openMailbox.Lock() delete(openMailbox.clients, sideID) openMailbox.Unlock() } }() for { _, msgBytes, err := c.ReadMessage() if _, isCloseErr := err.(*websocket.CloseError); err == io.EOF || isCloseErr { break } else if err != nil { log.Printf("rendezvousservertest recv err: %s", err) break } msg, err := serverUnmarshal(msgBytes) if err != nil { panic(fmt.Sprintf("err: %s msg: %s", err, msgBytes)) } switch m := msg.(type) { case *msgs.Bind: if sideID != "" { ackMsg(m.ID) errMsg(m.ID, m, fmt.Errorf("already bound")) continue } if m.Side == "" { ackMsg(m.ID) errMsg(m.ID, m, fmt.Errorf("bind requires 'side'")) continue } ts.mu.Lock() ts.agents = append(ts.agents, m.ClientVersion) ts.mu.Unlock() sideID = m.Side ackMsg(m.ID) case *msgs.Allocate: ackMsg(m.ID) var nameplate int16 ts.mu.Lock() for i := int16(1); i < math.MaxInt16; i++ { mboxID := ts.nameplates[i] if mboxID == "" { mboxID = crypto.RandHex(20) mbox := newMailbox() ts.mailboxes[mboxID] = mbox ts.nameplates[i] = mboxID nameplate = i break } } ts.mu.Unlock() if nameplate < 1 { errMsg(m.ID, m, fmt.Errorf("failed to allocate nameplate")) continue } resp := &msgs.AllocatedResp{ Nameplate: fmt.Sprintf("%d", nameplate), } sendMsg(resp) case *msgs.Claim: ackMsg(m.ID) nameplate, err := strconv.Atoi(m.Nameplate) if err != nil { panic(fmt.Sprintf("nameplate %s is not an int", m.Nameplate)) } ts.mu.Lock() mboxID := ts.nameplates[int16(nameplate)] if mboxID == "" { mboxID = crypto.RandHex(20) mbox := newMailbox() ts.mailboxes[mboxID] = mbox ts.nameplates[int16(nameplate)] = mboxID } ts.mu.Unlock() ts.mu.Lock() mbox := ts.mailboxes[mboxID] ts.mu.Unlock() if mbox == nil { errMsg(m.ID, m, fmt.Errorf("no mailbox found associated to nameplate %s", m.Nameplate)) continue } var crowded bool mbox.Lock() if mbox.claimCount > 1 { crowded = true } else { mbox.claimCount++ } mbox.Unlock() if crowded { errMsg(m.ID, m, errors.New("crowded")) continue } resp := &msgs.ClaimedResp{ Mailbox: mboxID, } sendMsg(resp) case *msgs.Open: ackMsg(m.ID) if openMailbox != nil { errMsg(m.ID, m, errors.New("only one open per connection")) continue } ts.mu.Lock() mbox := ts.mailboxes[m.Mailbox] ts.mu.Unlock() if mbox == nil { errMsg(m.ID, m, errors.New("mailbox not found")) continue } msgChan := make(chan mboxMsg) mbox.Lock() mbox.clients[sideID] = msgChan pendingMsgs := make([]mboxMsg, len(mbox.msgs)) copy(pendingMsgs, mbox.msgs) mbox.Unlock() for _, mboxMsg := range pendingMsgs { msg := &msgs.Message{ Side: mboxMsg.side, Phase: mboxMsg.phase, Body: mboxMsg.body, } sendMsg(msg) } go func() { for mboxMsg := range msgChan { msg := &msgs.Message{ Side: mboxMsg.side, Phase: mboxMsg.phase, Body: mboxMsg.body, } sendMsg(msg) } }() openMailbox = mbox case *msgs.Release: ackMsg(m.ID) nameplate, err := strconv.Atoi(m.Nameplate) if err != nil { errMsg(m.ID, m, errors.New("no nameplate found")) continue } ts.mu.Lock() delete(ts.nameplates, int16(nameplate)) ts.mu.Unlock() sendMsg(&msgs.ReleasedResp{}) case *msgs.Add: ackMsg(m.ID) openMailbox.Add(sideID, m) case *msgs.Close: ackMsg(m.ID) sendMsg(&msgs.ClosedResp{}) case *msgs.List: ackMsg(m.ID) var resp msgs.Nameplates ts.mu.Lock() for n := range ts.nameplates { resp.Nameplates = append(resp.Nameplates, struct { ID string `json:"id"` }{ strconv.Itoa(int(n)), }) } ts.mu.Unlock() sendMsg(&resp) default: log.Printf("Test server got unexpected message: %v", msg) } } } wormhole-william-1.0.6/tag_version.go000066400000000000000000000037471431053014400176630ustar00rootroot00000000000000//go:build ignore // +build ignore // This is a tool to assist with tagging versions correctly. // It updates version/version.go and produces the commands // to run for git. // // To run: go run tag_version.go package main import ( "flag" "fmt" "log" "os/exec" "strconv" "strings" "github.com/psanford/wormhole-william/version" ) var updateMajor = flag.Bool("major", false, "update major component") var updateMinor = flag.Bool("minor", false, "update minor component") var updatePatch = flag.Bool("patch", true, "update patch component") func main() { flag.Parse() err := exec.Command("git", "diff", "--quiet").Run() if err != nil { log.Fatalf("Cannot run tag_version with pending changes to your working directory: %s", err) } v := version.AgentVersion parts := strings.Split(v, ".") if len(parts) != 3 { log.Fatalf("Unexpected version format %s", v) } majorStr := parts[0] if majorStr[0] != 'v' { log.Fatalf("Unexpected version format (major) %s", v) } major, err := strconv.Atoi(majorStr[1:]) if err != nil { panic(err) } minorStr := parts[1] minor, err := strconv.Atoi(minorStr) if err != nil { panic(err) } patchStr := parts[2] patch, err := strconv.Atoi(patchStr) if err != nil { panic(err) } if *updateMajor { major++ minor = 0 patch = 0 } else if *updateMinor { minor++ patch = 0 } else if *updatePatch { patch++ } else { log.Fatal("No update flag specified") } newVersion := fmt.Sprintf("v%d.%d.%d", major, minor, patch) fmt.Printf("newVersion: %s\n", newVersion) out, err := exec.Command("gofmt", "-w", "-r", fmt.Sprintf("\"%s\" -> \"%s\"", v, newVersion), "version/version.go").CombinedOutput() if err != nil { log.Fatalf("Failed to gofmt: %s, %s", err, out) } fmt.Println("Run:\n") fmt.Println("go test ./... &&\\") fmt.Printf("git add version/version.go && git commit -m \"Bump version %s => %s\" &&\\\n", v, newVersion) fmt.Printf("git tag %s\n", newVersion) fmt.Println("\nThen:\n") fmt.Println("git push --tags") } wormhole-william-1.0.6/version/000077500000000000000000000000001431053014400164665ustar00rootroot00000000000000wormhole-william-1.0.6/version/version.go000066400000000000000000000002711431053014400205020ustar00rootroot00000000000000package version var ( // Default Agent identifier sent to rendezvous server AgentString = "go-william" // Default Agent version sent to rendezvous server AgentVersion = "v1.0.6" ) wormhole-william-1.0.6/wordlist/000077500000000000000000000000001431053014400166505ustar00rootroot00000000000000wormhole-william-1.0.6/wordlist/wordlist.go000066400000000000000000000210371431053014400210510ustar00rootroot00000000000000package wordlist import ( "crypto/rand" "strings" ) type WordPair struct { Even string Odd string } var RawWords = map[byte]WordPair{ 0x00: {"aardvark", "adroitness"}, 0x01: {"absurd", "adviser"}, 0x02: {"accrue", "aftermath"}, 0x03: {"acme", "aggregate"}, 0x04: {"adrift", "alkali"}, 0x05: {"adult", "almighty"}, 0x06: {"afflict", "amulet"}, 0x07: {"ahead", "amusement"}, 0x08: {"aimless", "antenna"}, 0x09: {"algol", "applicant"}, 0x0A: {"allow", "apollo"}, 0x0B: {"alone", "armistice"}, 0x0C: {"ammo", "article"}, 0x0D: {"ancient", "asteroid"}, 0x0E: {"apple", "atlantic"}, 0x0F: {"artist", "atmosphere"}, 0x10: {"assume", "autopsy"}, 0x11: {"athens", "babylon"}, 0x12: {"atlas", "backwater"}, 0x13: {"aztec", "barbecue"}, 0x14: {"baboon", "belowground"}, 0x15: {"backfield", "bifocals"}, 0x16: {"backward", "bodyguard"}, 0x17: {"banjo", "bookseller"}, 0x18: {"beaming", "borderline"}, 0x19: {"bedlamp", "bottomless"}, 0x1A: {"beehive", "bradbury"}, 0x1B: {"beeswax", "bravado"}, 0x1C: {"befriend", "brazilian"}, 0x1D: {"belfast", "breakaway"}, 0x1E: {"berserk", "burlington"}, 0x1F: {"billiard", "businessman"}, 0x20: {"bison", "butterfat"}, 0x21: {"blackjack", "camelot"}, 0x22: {"blockade", "candidate"}, 0x23: {"blowtorch", "cannonball"}, 0x24: {"bluebird", "capricorn"}, 0x25: {"bombast", "caravan"}, 0x26: {"bookshelf", "caretaker"}, 0x27: {"brackish", "celebrate"}, 0x28: {"breadline", "cellulose"}, 0x29: {"breakup", "certify"}, 0x2A: {"brickyard", "chambermaid"}, 0x2B: {"briefcase", "cherokee"}, 0x2C: {"burbank", "chicago"}, 0x2D: {"button", "clergyman"}, 0x2E: {"buzzard", "coherence"}, 0x2F: {"cement", "combustion"}, 0x30: {"chairlift", "commando"}, 0x31: {"chatter", "company"}, 0x32: {"checkup", "component"}, 0x33: {"chisel", "concurrent"}, 0x34: {"choking", "confidence"}, 0x35: {"chopper", "conformist"}, 0x36: {"christmas", "congregate"}, 0x37: {"clamshell", "consensus"}, 0x38: {"classic", "consulting"}, 0x39: {"classroom", "corporate"}, 0x3A: {"cleanup", "corrosion"}, 0x3B: {"clockwork", "councilman"}, 0x3C: {"cobra", "crossover"}, 0x3D: {"commence", "crucifix"}, 0x3E: {"concert", "cumbersome"}, 0x3F: {"cowbell", "customer"}, 0x40: {"crackdown", "dakota"}, 0x41: {"cranky", "decadence"}, 0x42: {"crowfoot", "december"}, 0x43: {"crucial", "decimal"}, 0x44: {"crumpled", "designing"}, 0x45: {"crusade", "detector"}, 0x46: {"cubic", "detergent"}, 0x47: {"dashboard", "determine"}, 0x48: {"deadbolt", "dictator"}, 0x49: {"deckhand", "dinosaur"}, 0x4A: {"dogsled", "direction"}, 0x4B: {"dragnet", "disable"}, 0x4C: {"drainage", "disbelief"}, 0x4D: {"dreadful", "disruptive"}, 0x4E: {"drifter", "distortion"}, 0x4F: {"dropper", "document"}, 0x50: {"drumbeat", "embezzle"}, 0x51: {"drunken", "enchanting"}, 0x52: {"dupont", "enrollment"}, 0x53: {"dwelling", "enterprise"}, 0x54: {"eating", "equation"}, 0x55: {"edict", "equipment"}, 0x56: {"egghead", "escapade"}, 0x57: {"eightball", "eskimo"}, 0x58: {"endorse", "everyday"}, 0x59: {"endow", "examine"}, 0x5A: {"enlist", "existence"}, 0x5B: {"erase", "exodus"}, 0x5C: {"escape", "fascinate"}, 0x5D: {"exceed", "filament"}, 0x5E: {"eyeglass", "finicky"}, 0x5F: {"eyetooth", "forever"}, 0x60: {"facial", "fortitude"}, 0x61: {"fallout", "frequency"}, 0x62: {"flagpole", "gadgetry"}, 0x63: {"flatfoot", "galveston"}, 0x64: {"flytrap", "getaway"}, 0x65: {"fracture", "glossary"}, 0x66: {"framework", "gossamer"}, 0x67: {"freedom", "graduate"}, 0x68: {"frighten", "gravity"}, 0x69: {"gazelle", "guitarist"}, 0x6A: {"geiger", "hamburger"}, 0x6B: {"glitter", "hamilton"}, 0x6C: {"glucose", "handiwork"}, 0x6D: {"goggles", "hazardous"}, 0x6E: {"goldfish", "headwaters"}, 0x6F: {"gremlin", "hemisphere"}, 0x70: {"guidance", "hesitate"}, 0x71: {"hamlet", "hideaway"}, 0x72: {"highchair", "holiness"}, 0x73: {"hockey", "hurricane"}, 0x74: {"indoors", "hydraulic"}, 0x75: {"indulge", "impartial"}, 0x76: {"inverse", "impetus"}, 0x77: {"involve", "inception"}, 0x78: {"island", "indigo"}, 0x79: {"jawbone", "inertia"}, 0x7A: {"keyboard", "infancy"}, 0x7B: {"kickoff", "inferno"}, 0x7C: {"kiwi", "informant"}, 0x7D: {"klaxon", "insincere"}, 0x7E: {"locale", "insurgent"}, 0x7F: {"lockup", "integrate"}, 0x80: {"merit", "intention"}, 0x81: {"minnow", "inventive"}, 0x82: {"miser", "istanbul"}, 0x83: {"mohawk", "jamaica"}, 0x84: {"mural", "jupiter"}, 0x85: {"music", "leprosy"}, 0x86: {"necklace", "letterhead"}, 0x87: {"neptune", "liberty"}, 0x88: {"newborn", "maritime"}, 0x89: {"nightbird", "matchmaker"}, 0x8A: {"oakland", "maverick"}, 0x8B: {"obtuse", "medusa"}, 0x8C: {"offload", "megaton"}, 0x8D: {"optic", "microscope"}, 0x8E: {"orca", "microwave"}, 0x8F: {"payday", "midsummer"}, 0x90: {"peachy", "millionaire"}, 0x91: {"pheasant", "miracle"}, 0x92: {"physique", "misnomer"}, 0x93: {"playhouse", "molasses"}, 0x94: {"pluto", "molecule"}, 0x95: {"preclude", "montana"}, 0x96: {"prefer", "monument"}, 0x97: {"preshrunk", "mosquito"}, 0x98: {"printer", "narrative"}, 0x99: {"prowler", "nebula"}, 0x9A: {"pupil", "newsletter"}, 0x9B: {"puppy", "norwegian"}, 0x9C: {"python", "october"}, 0x9D: {"quadrant", "ohio"}, 0x9E: {"quiver", "onlooker"}, 0x9F: {"quota", "opulent"}, 0xA0: {"ragtime", "orlando"}, 0xA1: {"ratchet", "outfielder"}, 0xA2: {"rebirth", "pacific"}, 0xA3: {"reform", "pandemic"}, 0xA4: {"regain", "pandora"}, 0xA5: {"reindeer", "paperweight"}, 0xA6: {"rematch", "paragon"}, 0xA7: {"repay", "paragraph"}, 0xA8: {"retouch", "paramount"}, 0xA9: {"revenge", "passenger"}, 0xAA: {"reward", "pedigree"}, 0xAB: {"rhythm", "pegasus"}, 0xAC: {"ribcage", "penetrate"}, 0xAD: {"ringbolt", "perceptive"}, 0xAE: {"robust", "performance"}, 0xAF: {"rocker", "pharmacy"}, 0xB0: {"ruffled", "phonetic"}, 0xB1: {"sailboat", "photograph"}, 0xB2: {"sawdust", "pioneer"}, 0xB3: {"scallion", "pocketful"}, 0xB4: {"scenic", "politeness"}, 0xB5: {"scorecard", "positive"}, 0xB6: {"scotland", "potato"}, 0xB7: {"seabird", "processor"}, 0xB8: {"select", "provincial"}, 0xB9: {"sentence", "proximate"}, 0xBA: {"shadow", "puberty"}, 0xBB: {"shamrock", "publisher"}, 0xBC: {"showgirl", "pyramid"}, 0xBD: {"skullcap", "quantity"}, 0xBE: {"skydive", "racketeer"}, 0xBF: {"slingshot", "rebellion"}, 0xC0: {"slowdown", "recipe"}, 0xC1: {"snapline", "recover"}, 0xC2: {"snapshot", "repellent"}, 0xC3: {"snowcap", "replica"}, 0xC4: {"snowslide", "reproduce"}, 0xC5: {"solo", "resistor"}, 0xC6: {"southward", "responsive"}, 0xC7: {"soybean", "retraction"}, 0xC8: {"spaniel", "retrieval"}, 0xC9: {"spearhead", "retrospect"}, 0xCA: {"spellbind", "revenue"}, 0xCB: {"spheroid", "revival"}, 0xCC: {"spigot", "revolver"}, 0xCD: {"spindle", "sandalwood"}, 0xCE: {"spyglass", "sardonic"}, 0xCF: {"stagehand", "saturday"}, 0xD0: {"stagnate", "savagery"}, 0xD1: {"stairway", "scavenger"}, 0xD2: {"standard", "sensation"}, 0xD3: {"stapler", "sociable"}, 0xD4: {"steamship", "souvenir"}, 0xD5: {"sterling", "specialist"}, 0xD6: {"stockman", "speculate"}, 0xD7: {"stopwatch", "stethoscope"}, 0xD8: {"stormy", "stupendous"}, 0xD9: {"sugar", "supportive"}, 0xDA: {"surmount", "surrender"}, 0xDB: {"suspense", "suspicious"}, 0xDC: {"sweatband", "sympathy"}, 0xDD: {"swelter", "tambourine"}, 0xDE: {"tactics", "telephone"}, 0xDF: {"talon", "therapist"}, 0xE0: {"tapeworm", "tobacco"}, 0xE1: {"tempest", "tolerance"}, 0xE2: {"tiger", "tomorrow"}, 0xE3: {"tissue", "torpedo"}, 0xE4: {"tonic", "tradition"}, 0xE5: {"topmost", "travesty"}, 0xE6: {"tracker", "trombonist"}, 0xE7: {"transit", "truncated"}, 0xE8: {"trauma", "typewriter"}, 0xE9: {"treadmill", "ultimate"}, 0xEA: {"trojan", "undaunted"}, 0xEB: {"trouble", "underfoot"}, 0xEC: {"tumor", "unicorn"}, 0xED: {"tunnel", "unify"}, 0xEE: {"tycoon", "universe"}, 0xEF: {"uncut", "unravel"}, 0xF0: {"unearth", "upcoming"}, 0xF1: {"unwind", "vacancy"}, 0xF2: {"uproot", "vagabond"}, 0xF3: {"upset", "vertigo"}, 0xF4: {"upshot", "virginia"}, 0xF5: {"vapor", "visitor"}, 0xF6: {"village", "vocalist"}, 0xF7: {"virus", "voyager"}, 0xF8: {"vulcan", "warranty"}, 0xF9: {"waffle", "waterloo"}, 0xFA: {"wallet", "whimsical"}, 0xFB: {"watchword", "wichita"}, 0xFC: {"wayside", "wilmington"}, 0xFD: {"willow", "wyoming"}, 0xFE: {"woodlark", "yesteryear"}, 0xFF: {"zulu", "yucatan"}, } func ChooseWords(count int) string { words := make([]string, count) b := make([]byte, 1) for i := 0; i < count; i++ { _, err := rand.Read(b) if err != nil { panic(err) } if i%2 == 0 { words[i] = RawWords[b[0]].Odd } else { words[i] = RawWords[b[0]].Even } } return strings.Join(words, "-") } wormhole-william-1.0.6/wormhole/000077500000000000000000000000001431053014400166355ustar00rootroot00000000000000wormhole-william-1.0.6/wormhole/file_transport.go000066400000000000000000000303521431053014400222220ustar00rootroot00000000000000package wormhole import ( "bytes" "context" "crypto/sha256" "crypto/subtle" "encoding/binary" "errors" "fmt" "io" "math" "math/big" "net" "strconv" "time" "github.com/psanford/wormhole-william/internal/crypto" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/secretbox" ) type fileTransportAck struct { Ack string `json:"ack"` SHA256 string `json:"sha256"` } type TransferType int const ( TransferFile TransferType = iota + 1 TransferDirectory TransferText ) func (tt TransferType) String() string { switch tt { case TransferFile: return "TransferFile" case TransferDirectory: return "TransferDirectory" case TransferText: return "TransferText" default: return fmt.Sprintf("TransferTypeUnknown<%d>", tt) } } type transportCryptor struct { conn net.Conn prefixBuf []byte nextReadNonce *big.Int nextWriteNonce uint64 err error readKey [32]byte writeKey [32]byte } func newTransportCryptor(c net.Conn, transitKey []byte, readPurpose, writePurpose string) *transportCryptor { r := hkdf.New(sha256.New, transitKey, nil, []byte(readPurpose)) var readKey [32]byte _, err := io.ReadFull(r, readKey[:]) if err != nil { panic(err) } r = hkdf.New(sha256.New, transitKey, nil, []byte(writePurpose)) var writeKey [32]byte _, err = io.ReadFull(r, writeKey[:]) if err != nil { panic(err) } return &transportCryptor{ conn: c, prefixBuf: make([]byte, 4+crypto.NonceSize), nextReadNonce: big.NewInt(0), readKey: readKey, writeKey: writeKey, } } func (d *transportCryptor) Close() error { return d.conn.Close() } func (d *transportCryptor) readRecord() ([]byte, error) { if d.err != nil { return nil, d.err } _, err := io.ReadFull(d.conn, d.prefixBuf) if err != nil { d.err = err return nil, d.err } l := binary.BigEndian.Uint32(d.prefixBuf[:4]) var nonce [24]byte copy(nonce[:], d.prefixBuf[4:]) var bigNonce big.Int bigNonce.SetBytes(nonce[:]) if bigNonce.Cmp(d.nextReadNonce) != 0 { d.err = errors.New("received out-of-order record") return nil, d.err } d.nextReadNonce.Add(d.nextReadNonce, big.NewInt(1)) sealedMsg := make([]byte, l-crypto.NonceSize) _, err = io.ReadFull(d.conn, sealedMsg) if err != nil { d.err = err return nil, d.err } out, ok := secretbox.Open(nil, sealedMsg, &nonce, &d.readKey) if !ok { d.err = errDecryptFailed return nil, d.err } return out, nil } func (d *transportCryptor) writeRecord(msg []byte) error { var nonce [crypto.NonceSize]byte if d.nextWriteNonce == math.MaxUint64 { panic("Nonce exhaustion") } binary.BigEndian.PutUint64(nonce[crypto.NonceSize-8:], d.nextWriteNonce) d.nextWriteNonce++ sealedMsg := secretbox.Seal(nil, msg, &nonce, &d.writeKey) nonceAndSealedMsg := append(nonce[:], sealedMsg...) // we do an explit cast to int64 to avoid compilation failures // for 32bit systems. nonceAndSealedMsgSize := int64(len(nonceAndSealedMsg)) if nonceAndSealedMsgSize >= math.MaxUint32 { panic(fmt.Sprintf("writeRecord too large: %d", len(nonceAndSealedMsg))) } l := make([]byte, 4) binary.BigEndian.PutUint32(l, uint32(len(nonceAndSealedMsg))) lenNonceAndSealedMsg := append(l, nonceAndSealedMsg...) _, err := d.conn.Write(lenNonceAndSealedMsg) return err } func newFileTransport(transitKey []byte, appID, relayAddr string) *fileTransport { return &fileTransport{ transitKey: transitKey, appID: appID, relayAddr: relayAddr, } } type fileTransport struct { listener net.Listener relayConn net.Conn relayAddr string transitKey []byte appID string } func (t *fileTransport) connectViaRelay(otherTransit *transitMsg) (net.Conn, error) { cancelFuncs := make(map[string]func()) successChan := make(chan net.Conn) failChan := make(chan string) var count int for _, outerHint := range otherTransit.HintsV1 { if outerHint.Type == "relay-v1" { for _, innerHint := range outerHint.Hints { if innerHint.Type == "direct-tcp-v1" { count++ ctx, cancel := context.WithCancel(context.Background()) addr := net.JoinHostPort(innerHint.Hostname, strconv.Itoa(innerHint.Port)) cancelFuncs[addr] = cancel go t.connectToRelay(ctx, addr, successChan, failChan) } } } } var conn net.Conn connectTimeout := time.After(5 * time.Second) for i := 0; i < count; i++ { select { case <-failChan: case conn = <-successChan: case <-connectTimeout: for _, cancel := range cancelFuncs { cancel() } } } return conn, nil } func (t *fileTransport) connectDirect(otherTransit *transitMsg) (net.Conn, error) { cancelFuncs := make(map[string]func()) successChan := make(chan net.Conn) failChan := make(chan string) var count int for _, hint := range otherTransit.HintsV1 { if hint.Type == "direct-tcp-v1" { count++ ctx, cancel := context.WithCancel(context.Background()) addr := net.JoinHostPort(hint.Hostname, strconv.Itoa(hint.Port)) cancelFuncs[addr] = cancel go t.connectToSingleHost(ctx, addr, successChan, failChan) } } var conn net.Conn connectTimeout := time.After(5 * time.Second) for i := 0; i < count; i++ { select { case <-failChan: case conn = <-successChan: case <-connectTimeout: for _, cancel := range cancelFuncs { cancel() } } } return conn, nil } func (t *fileTransport) connectToRelay(ctx context.Context, addr string, successChan chan net.Conn, failChan chan string) { var d net.Dialer conn, err := d.DialContext(ctx, "tcp", addr) if err != nil { failChan <- addr return } _, err = conn.Write(t.relayHandshakeHeader()) if err != nil { failChan <- addr return } gotOk := make([]byte, 3) _, err = io.ReadFull(conn, gotOk) if err != nil { conn.Close() failChan <- addr return } if !bytes.Equal(gotOk, []byte("ok\n")) { conn.Close() failChan <- addr return } t.directRecvHandshake(ctx, addr, conn, successChan, failChan) } func (t *fileTransport) connectToSingleHost(ctx context.Context, addr string, successChan chan net.Conn, failChan chan string) { var d net.Dialer conn, err := d.DialContext(ctx, "tcp", addr) if err != nil { failChan <- addr return } t.directRecvHandshake(ctx, addr, conn, successChan, failChan) } func (t *fileTransport) directRecvHandshake(ctx context.Context, addr string, conn net.Conn, successChan chan net.Conn, failChan chan string) { expectHeader := t.senderHandshakeHeader() gotHeader := make([]byte, len(expectHeader)) _, err := io.ReadFull(conn, gotHeader) if err != nil { conn.Close() failChan <- addr return } if subtle.ConstantTimeCompare(gotHeader, expectHeader) != 1 { conn.Close() failChan <- addr return } _, err = conn.Write(t.receiverHandshakeHeader()) if err != nil { conn.Close() failChan <- addr return } gotGo := make([]byte, 3) _, err = io.ReadFull(conn, gotGo) if err != nil { conn.Close() failChan <- addr return } if !bytes.Equal(gotGo, []byte("go\n")) { conn.Close() failChan <- addr return } successChan <- conn } func (t *fileTransport) makeTransitMsg() (*transitMsg, error) { msg := transitMsg{ AbilitiesV1: []transitAbility{ { Type: "direct-tcp-v1", }, { Type: "relay-v1", }, }, // make a slice so this jsons to [] and not null HintsV1: make([]transitHintsV1, 0), } if t.listener != nil { _, portStr, err := net.SplitHostPort(t.listener.Addr().String()) if err != nil { return nil, err } port, err := strconv.Atoi(portStr) if err != nil { return nil, fmt.Errorf("port isn't an integer? %s", portStr) } addrs := nonLocalhostAddresses() for _, addr := range addrs { msg.HintsV1 = append(msg.HintsV1, transitHintsV1{ Type: "direct-tcp-v1", Priority: 0.0, Hostname: addr, Port: port, }) } } if t.relayConn != nil { relayHost, portStr, err := net.SplitHostPort(t.relayAddr) if err != nil { return nil, err } relayPort, err := strconv.Atoi(portStr) if err != nil { return nil, fmt.Errorf("port isn't an integer? %s", portStr) } msg.HintsV1 = append(msg.HintsV1, transitHintsV1{ Type: "relay-v1", Hints: []transitHintsV1Hint{ { Type: "direct-tcp-v1", Priority: 2.0, Hostname: relayHost, Port: relayPort, }, }, }) } return &msg, nil } func (t *fileTransport) senderHandshakeHeader() []byte { purpose := "transit_sender" r := hkdf.New(sha256.New, t.transitKey, nil, []byte(purpose)) out := make([]byte, 32) _, err := io.ReadFull(r, out) if err != nil { panic(err) } return []byte(fmt.Sprintf("transit sender %x ready\n\n", out)) } func (t *fileTransport) receiverHandshakeHeader() []byte { purpose := "transit_receiver" r := hkdf.New(sha256.New, t.transitKey, nil, []byte(purpose)) out := make([]byte, 32) _, err := io.ReadFull(r, out) if err != nil { panic(err) } return []byte(fmt.Sprintf("transit receiver %x ready\n\n", out)) } func (t *fileTransport) relayHandshakeHeader() []byte { purpose := "transit_relay_token" r := hkdf.New(sha256.New, t.transitKey, nil, []byte(purpose)) out := make([]byte, 32) _, err := io.ReadFull(r, out) if err != nil { panic(err) } sideID := crypto.RandHex(8) return []byte(fmt.Sprintf("please relay %x for side %s\n", out, sideID)) } // Test option to disable local listeners var testDisableLocalListener bool func (t *fileTransport) listen() error { if testDisableLocalListener { return nil } l, err := net.Listen("tcp", ":0") if err != nil { return err } t.listener = l return nil } func (t *fileTransport) listenRelay() error { if t.relayAddr == "" { return nil } conn, err := net.Dial("tcp", t.relayAddr) if err != nil { return err } _, err = conn.Write(t.relayHandshakeHeader()) if err != nil { conn.Close() return err } t.relayConn = conn return nil } func (t *fileTransport) waitForRelayPeer(conn net.Conn, cancelCh chan struct{}) error { okCh := make(chan struct{}) go func() { select { case <-cancelCh: conn.Close() case <-okCh: } }() defer close(okCh) gotOk := make([]byte, 3) _, err := io.ReadFull(conn, gotOk) if err != nil { conn.Close() return err } if !bytes.Equal(gotOk, []byte("ok\n")) { conn.Close() return errors.New("got non ok status from relay server") } return nil } func (t *fileTransport) acceptConnection(ctx context.Context) (net.Conn, error) { readyCh := make(chan net.Conn) cancelCh := make(chan struct{}) acceptErrCh := make(chan error, 1) if t.relayConn != nil { go func() { waitErr := t.waitForRelayPeer(t.relayConn, cancelCh) if waitErr != nil { return } t.handleIncomingConnection(t.relayConn, readyCh, cancelCh) }() } if t.listener != nil { defer t.listener.Close() go func() { for { conn, err := t.listener.Accept() if err == io.EOF { break } else if err != nil { acceptErrCh <- err break } go t.handleIncomingConnection(conn, readyCh, cancelCh) } }() } select { case <-ctx.Done(): close(cancelCh) return nil, ctx.Err() case acceptErr := <-acceptErrCh: close(cancelCh) return nil, acceptErr case conn := <-readyCh: close(cancelCh) _, err := conn.Write([]byte("go\n")) if err != nil { return nil, err } return conn, nil } } func (t *fileTransport) handleIncomingConnection(conn net.Conn, readyCh chan<- net.Conn, cancelCh chan struct{}) { okCh := make(chan struct{}) go func() { select { case <-cancelCh: conn.Close() case <-okCh: } }() _, err := conn.Write(t.senderHandshakeHeader()) if err != nil { conn.Close() close(okCh) return } expectHeader := t.receiverHandshakeHeader() gotHeader := make([]byte, len(expectHeader)) _, err = io.ReadFull(conn, gotHeader) if err != nil { conn.Close() close(okCh) return } if subtle.ConstantTimeCompare(gotHeader, expectHeader) != 1 { conn.Close() close(okCh) return } select { case okCh <- struct{}{}: case <-cancelCh: } select { case <-cancelCh: // One of the other connections won, shut this one down conn.Write([]byte("nevermind\n")) conn.Close() case readyCh <- conn: } } func nonLocalhostAddresses() []string { addrs, err := net.InterfaceAddrs() if err != nil { return nil } var outAddrs []string for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { outAddrs = append(outAddrs, ipnet.IP.String()) } } } return outAddrs } wormhole-william-1.0.6/wormhole/recv.go000066400000000000000000000230511431053014400201240ustar00rootroot00000000000000package wormhole import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "hash" "io" "strings" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous" ) // Receive receives a message sent by a wormhole client. // // It returns an IncomingMessage with metadata about the payload being sent. // To read the contents of the message call IncomingMessage.Read(). func (c *Client) Receive(ctx context.Context, code string) (fr *IncomingMessage, returnErr error) { sideID := crypto.RandSideID() appID := c.appID() rc := rendezvous.NewClient(c.url(), sideID, appID) defer func() { mood := rendezvous.Errory if returnErr == nil { // don't close our connection in this case // wait until the user actually accepts the transfer return } else if returnErr == errDecryptFailed { mood = rendezvous.Scary } rc.Close(ctx, mood) }() _, err := rc.Connect(ctx) if err != nil { return nil, err } nameplate, err := nameplateFromCode(code) if err != nil { return nil, err } err = rc.AttachMailbox(ctx, nameplate) if err != nil { return nil, err } clientProto := newClientProtocol(ctx, rc, sideID, appID) err = clientProto.WritePake(ctx, code) if err != nil { return nil, err } err = clientProto.ReadPake(ctx) if err != nil { return nil, err } err = clientProto.WriteVersion(ctx) if err != nil { return nil, err } _, err = clientProto.ReadVersion() if err != nil { return nil, err } if c.VerifierOk != nil { verifier, err := clientProto.Verifier() if err != nil { return nil, err } if ok := c.VerifierOk(hex.EncodeToString(verifier)); !ok { errMsg := "sender rejected verification check, abandoned transfer" writeErr := clientProto.WriteAppData(ctx, &genericMessage{ Error: &errMsg, }) if writeErr != nil { return nil, writeErr } return nil, errors.New(errMsg) } } collector, err := clientProto.Collect(collectOffer, collectTransit) if err != nil { return nil, err } defer collector.close() var offer offerMsg err = collector.waitFor(&offer) if err != nil { return nil, err } fr = &IncomingMessage{} if offer.Message != nil { answer := genericMessage{ Answer: &answerMsg{ MessageAck: "ok", }, } err = clientProto.WriteAppData(ctx, &answer) if err != nil { return nil, err } rc.Close(ctx, rendezvous.Happy) text := *offer.Message fr.TransferBytes = len(text) fr.TransferBytes64 = int64(fr.TransferBytes) fr.UncompressedBytes = fr.TransferBytes fr.UncompressedBytes64 = fr.TransferBytes64 fr.Type = TransferText fr.textReader = strings.NewReader(text) return fr, nil } else if offer.File != nil { fr.Type = TransferFile fr.Name = offer.File.FileName fr.TransferBytes = int(offer.File.FileSize) fr.TransferBytes64 = offer.File.FileSize fr.UncompressedBytes = int(offer.File.FileSize) fr.UncompressedBytes64 = offer.File.FileSize fr.FileCount = 1 fr.ctx = ctx } else if offer.Directory != nil { fr.Type = TransferDirectory fr.Name = offer.Directory.Dirname fr.TransferBytes = int(offer.Directory.ZipSize) fr.TransferBytes64 = offer.Directory.ZipSize fr.UncompressedBytes = int(offer.Directory.NumBytes) fr.UncompressedBytes64 = offer.Directory.NumBytes fr.FileCount = int(offer.Directory.NumFiles) fr.ctx = ctx } else { return nil, errors.New("got non-file transfer offer") } var gotTransitMsg transitMsg err = collector.waitFor(&gotTransitMsg) if err != nil { return nil, err } transitKey := deriveTransitKey(clientProto.sharedKey, appID) transport := newFileTransport(transitKey, appID, c.relayAddr()) transitMsg, err := transport.makeTransitMsg() if err != nil { return nil, fmt.Errorf("make transit msg error: %s", err) } err = clientProto.WriteAppData(ctx, &genericMessage{ Transit: transitMsg, }) if err != nil { return nil, err } reject := func() (initErr error) { defer func() { mood := rendezvous.Errory if returnErr == nil { mood = rendezvous.Happy } else if returnErr == errDecryptFailed { mood = rendezvous.Scary } rc.Close(ctx, mood) }() var errStr = "transfer rejected" answer := &genericMessage{ Error: &errStr, } ctx := context.Background() err = clientProto.WriteAppData(ctx, answer) if err != nil { return err } return nil } // defer actually sending the "ok" message until // the caller does a read on the IncomingMessage object. acceptAndInitialize := func() (initErr error) { defer func() { mood := rendezvous.Errory if returnErr == nil { mood = rendezvous.Happy } else if returnErr == errDecryptFailed { mood = rendezvous.Scary } rc.Close(ctx, mood) }() answer := &genericMessage{ Answer: &answerMsg{ FileAck: "ok", }, } ctx := context.Background() err = clientProto.WriteAppData(ctx, answer) if err != nil { return err } conn, err := transport.connectDirect(&gotTransitMsg) if err != nil { return err } if conn == nil { conn, err = transport.connectViaRelay(&gotTransitMsg) if err != nil { return err } } if conn == nil { return errors.New("failed to establish connection") } cryptor := newTransportCryptor(conn, transitKey, "transit_record_sender_key", "transit_record_receiver_key") fr.cryptor = cryptor fr.sha256 = sha256.New() return nil } fr.initializeTransfer = acceptAndInitialize fr.rejectTransfer = reject return fr, nil } // A IncomingMessage contains information about a payload sent to this wormhole client. // // The Type field indicates if the sender sent a single file or a directory. // If the Type is TransferDirectory then reading from the IncomingMessage will // read a zip file of the contents of the directory. type IncomingMessage struct { // Name is the name of the file or directory being transferred. Name string // The type of file transfer being offered. Type TransferType // Deprecated: TransferBytes has been replaced with with TransferBytes64 // to allow transfer of >2 GiB files on 32 bit systems TransferBytes int // TransferBytes64 is the offered size of the file transfer from the peer. // This is expected to be the number of bytes sent over the network to // perform the file transfer, however a malicious client could lie about this. // The primary purpose of this field is to allow the user to choose to accept // or reject the transfer if the file size is unexpected. // // For client implementation convenience, TransferBytes64 is also set for text messages. // Note that the message has already been fully transferred by the time this value is known. TransferBytes64 int64 // Deprecated: UncompressedBytes has been replaced with UncompressedBytes64 // to allow transfers of > 2 GiB files on 32 bit systems UncompressedBytes int // UncompressedBytes64 is the offered size of the files on disk post decompression. // This is sent from the peer as part of the offer and a malicious peer could lie // about this. // The primary purpose of this field is to allow the user to choose to accept // or reject the transfer if the file size is unexpected. // // For client implementation convenience, UncompressedBytes64 is also set for text messages. // Note that the message has already been fully transferred by the time this value is known. UncompressedBytes64 int64 // FileCount is the number of files in a TransferDirectory offer. This is sent // as part of the offer from the peer and a malicious peer could lie about this. FileCount int textReader io.Reader transferInitialized bool initializeTransfer func() error rejectTransfer func() error cryptor *transportCryptor buf []byte readCount int64 sha256 hash.Hash readErr error ctx context.Context } // Read the decrypted contents sent to this client. func (f *IncomingMessage) Read(p []byte) (int, error) { if f.readErr != nil { return 0, f.readErr } switch f.Type { case TransferText: return f.readText(p) case TransferFile, TransferDirectory: return f.readCrypt(p) default: return 0, fmt.Errorf("unknown Receiver type %d", f.Type) } } func (f *IncomingMessage) readText(p []byte) (int, error) { return f.textReader.Read(p) } // Reject an incoming file or directory transfer. This must be // called before any calls to Read. This does nothing for // text message transfers. func (f *IncomingMessage) Reject() error { switch f.Type { case TransferFile, TransferDirectory: default: return errors.New("can only reject File and Directory transfers") } if f.readErr != nil { return f.readErr } if f.transferInitialized { return errors.New("cannot Reject after calls to Read") } f.transferInitialized = true f.rejectTransfer() return nil } func (f *IncomingMessage) readCrypt(p []byte) (int, error) { if f.readErr != nil { return 0, f.readErr } if err := f.ctx.Err(); err != nil { f.readErr = err if f.cryptor != nil { f.cryptor.Close() } return 0, err } if !f.transferInitialized { f.transferInitialized = true err := f.initializeTransfer() if err != nil { return 0, err } } if len(f.buf) == 0 { rec, err := f.cryptor.readRecord() if err == io.EOF { f.readErr = io.ErrUnexpectedEOF return 0, f.readErr } else if err != nil { f.readErr = err return 0, err } f.buf = rec } n := copy(p, f.buf) f.buf = f.buf[n:] f.readCount += int64(n) f.sha256.Write(p[:n]) if f.readCount >= f.TransferBytes64 { f.readErr = io.EOF sum := f.sha256.Sum(nil) ack := fileTransportAck{ Ack: "ok", SHA256: fmt.Sprintf("%x", sum), } msg, _ := json.Marshal(ack) f.cryptor.writeRecord(msg) f.cryptor.Close() } return n, nil } wormhole-william-1.0.6/wormhole/send.go000066400000000000000000000336611431053014400201260ustar00rootroot00000000000000package wormhole import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/klauspost/compress/zip" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous" "github.com/psanford/wormhole-william/wordlist" "golang.org/x/crypto/nacl/secretbox" ) // SendText sends a text message via the wormhole protocol. // // It returns the nameplate+passphrase code to give to the receiver, a result chan // that gets written to once the receiver actually attempts to read the message // (either successfully or not). func (c *Client) SendText(ctx context.Context, msg string, opts ...SendOption) (string, chan SendResult, error) { sideID := crypto.RandSideID() appID := c.appID() var options sendOptions for _, opt := range opts { err := opt.setOption(&options) if err != nil { return "", nil, err } } rc := rendezvous.NewClient(c.url(), sideID, appID) _, err := rc.Connect(ctx) if err != nil { return "", nil, err } var pwStr string if options.code == "" { nameplate, err := rc.CreateMailbox(ctx) if err != nil { return "", nil, err } pwStr = nameplate + "-" + wordlist.ChooseWords(c.wordCount()) } else { pwStr = options.code nameplate, err := nameplateFromCode(pwStr) if err != nil { return "", nil, err } err = rc.AttachMailbox(ctx, nameplate) if err != nil { return "", nil, err } } clientProto := newClientProtocol(ctx, rc, sideID, appID) ch := make(chan SendResult, 1) go func() { var returnErr error defer func() { mood := rendezvous.Errory if returnErr == nil { mood = rendezvous.Happy } else if returnErr == errDecryptFailed { mood = rendezvous.Scary } rc.Close(ctx, mood) }() sendErr := func(err error) { ch <- SendResult{ Error: err, } returnErr = err close(ch) } err = clientProto.WritePake(ctx, pwStr) if err != nil { sendErr(err) return } err = clientProto.ReadPake(ctx) if err != nil { sendErr(err) return } err = clientProto.WriteVersion(ctx) if err != nil { sendErr(err) return } _, err = clientProto.ReadVersion() if err != nil { sendErr(err) return } if c.VerifierOk != nil { verifier, err := clientProto.Verifier() if err != nil { sendErr(err) return } if ok := c.VerifierOk(hex.EncodeToString(verifier)); !ok { errMsg := "sender rejected verification check, abandoned transfer" writeErr := clientProto.WriteAppData(ctx, &genericMessage{ Error: &errMsg, }) if writeErr != nil { sendErr(writeErr) return } sendErr(errors.New(errMsg)) return } } offer := &genericMessage{ Offer: &offerMsg{ Message: &msg, }, } err = clientProto.WriteAppData(ctx, offer) if err != nil { sendErr(err) return } collector, err := clientProto.Collect() if err != nil { sendErr(err) return } defer collector.close() var answer answerMsg err = collector.waitFor(&answer) if err != nil { sendErr(err) return } if answer.MessageAck == "ok" { if options.progressFunc != nil { // If called WithProgress, send a single progress update // showing that the transfer is complete. This is to simplify // client implementations that share code between the Send() // and SendText() code paths. msgSize := int64(len(msg)) options.progressFunc(msgSize, msgSize) } ch <- SendResult{ OK: true, } close(ch) return } else { sendErr(fmt.Errorf("unexpected answer")) return } }() return pwStr, ch, nil } func (c *Client) sendFileDirectory(ctx context.Context, offer *offerMsg, r io.Reader, opts ...SendOption) (string, chan SendResult, error) { if err := c.validateRelayAddr(); err != nil { return "", nil, fmt.Errorf("invalid TransitRelayAddress: %s", err) } var options sendOptions for _, opt := range opts { err := opt.setOption(&options) if err != nil { return "", nil, err } } sideID := crypto.RandSideID() appID := c.appID() rc := rendezvous.NewClient(c.url(), sideID, appID) _, err := rc.Connect(ctx) if err != nil { return "", nil, err } var pwStr string if options.code == "" { nameplate, err := rc.CreateMailbox(ctx) if err != nil { return "", nil, err } pwStr = nameplate + "-" + wordlist.ChooseWords(c.wordCount()) } else { pwStr = options.code nameplate, err := nameplateFromCode(pwStr) if err != nil { return "", nil, err } err = rc.AttachMailbox(ctx, nameplate) if err != nil { return "", nil, err } } clientProto := newClientProtocol(ctx, rc, sideID, appID) ch := make(chan SendResult, 1) go func() { var returnErr error defer func() { mood := rendezvous.Errory if returnErr == nil { mood = rendezvous.Happy } else if returnErr == errDecryptFailed { mood = rendezvous.Scary } rc.Close(ctx, mood) }() sendErr := func(err error) { ch <- SendResult{ Error: err, } returnErr = err close(ch) } err = clientProto.WritePake(ctx, pwStr) if err != nil { sendErr(err) return } err = clientProto.ReadPake(ctx) if err != nil { sendErr(err) return } err = clientProto.WriteVersion(ctx) if err != nil { sendErr(err) return } _, err = clientProto.ReadVersion() if err != nil { sendErr(err) return } if c.VerifierOk != nil { verifier, err := clientProto.Verifier() if err != nil { sendErr(err) return } if ok := c.VerifierOk(hex.EncodeToString(verifier)); !ok { errMsg := "sender rejected verification check, abandoned transfer" writeErr := clientProto.WriteAppData(ctx, &genericMessage{ Error: &errMsg, }) if writeErr != nil { sendErr(writeErr) return } sendErr(errors.New(errMsg)) return } } transitKey := deriveTransitKey(clientProto.sharedKey, appID) transport := newFileTransport(transitKey, appID, c.relayAddr()) err = transport.listen() if err != nil { sendErr(err) return } err = transport.listenRelay() if err != nil { sendErr(err) return } transit, err := transport.makeTransitMsg() if err != nil { sendErr(fmt.Errorf("make transit msg error: %s", err)) return } err = clientProto.WriteAppData(ctx, &genericMessage{ Transit: transit, }) if err != nil { sendErr(err) return } gmOffer := &genericMessage{ Offer: offer, } err = clientProto.WriteAppData(ctx, gmOffer) if err != nil { sendErr(err) return } collector, err := clientProto.Collect() if err != nil { sendErr(err) return } defer collector.close() var answer answerMsg err = collector.waitFor(&answer) if err != nil { sendErr(err) return } if answer.FileAck != "ok" { sendErr(fmt.Errorf("unexpected answer")) return } conn, err := transport.acceptConnection(ctx) if err != nil { sendErr(err) return } cryptor := newTransportCryptor(conn, transitKey, "transit_record_receiver_key", "transit_record_sender_key") recordSize := (1 << 14) // chunk recordSlice := make([]byte, recordSize-secretbox.Overhead) hasher := sha256.New() var ( progress int64 totalSize int64 ) if offer.File != nil { totalSize = offer.File.FileSize } else if offer.Directory != nil { totalSize = offer.Directory.ZipSize } var cancel func() ctx, cancel = context.WithCancel(ctx) defer cancel() go func() { <-ctx.Done() conn.Close() }() for { n, err := r.Read(recordSlice) if n > 0 { hasher.Write(recordSlice[:n]) err = cryptor.writeRecord(recordSlice[:n]) if err != nil { sendErr(err) return } progress += int64(n) if options.progressFunc != nil { options.progressFunc(progress, totalSize) } } if err == io.EOF { break } else if err != nil { sendErr(err) return } } respRec, err := cryptor.readRecord() if err != nil { sendErr(err) return } var ack fileTransportAck err = json.Unmarshal(respRec, &ack) if err != nil { sendErr(err) return } if ack.Ack != "ok" { sendErr(errors.New("got non ok final ack from receiver")) return } shaSum := fmt.Sprintf("%x", hasher.Sum(nil)) if strings.ToLower(ack.SHA256) != shaSum { sendErr(fmt.Errorf("receiver sha256 mismatch %s vs %s", ack.SHA256, shaSum)) return } ch <- SendResult{ OK: true, } close(ch) }() return pwStr, ch, nil } // SendFile sends a single file via the wormhole protocol. It returns a nameplate+passhrase code to give to the // receiver, a result channel that will be written to after the receiver attempts to read (either successfully or not) // and an error if one occurred. func (c *Client) SendFile(ctx context.Context, fileName string, r io.ReadSeeker, opts ...SendOption) (string, chan SendResult, error) { if err := c.validateRelayAddr(); err != nil { return "", nil, fmt.Errorf("invalid TransitRelayAddress: %s", err) } size, err := readSeekerSize(r) if err != nil { return "", nil, err } offer := &offerMsg{ File: &offerFile{ FileName: fileName, FileSize: size, }, } return c.sendFileDirectory(ctx, offer, r, opts...) } // A DirectoryEntry represents a single file to be sent by SendDirectory type DirectoryEntry struct { // Path is the relative path to the file from the top level directory. Path string // Mode controls the permission and mode bits for the file. Mode os.FileMode // Reader is a function that returns a ReadCloser for the file's content. Reader func() (io.ReadCloser, error) } // SendDirectory sends a tree of files to a receiving client. // Each DirectoryEntry Path must be prefixed by the directoryName provided to SendDirectory. // // It returns a nameplate+passhrase code to give to the // receiver, a result channel that will be written to after the receiver attempts to read (either successfully or not) // and an error if one occurred. func (c *Client) SendDirectory(ctx context.Context, directoryName string, entries []DirectoryEntry, opts ...SendOption) (string, chan SendResult, error) { zipInfo, err := makeTmpZip(directoryName, entries) if err != nil { return "", nil, err } offer := &offerMsg{ Directory: &offerDirectory{ Dirname: directoryName, Mode: "zipfile/deflated", NumBytes: zipInfo.numBytes, NumFiles: zipInfo.numFiles, ZipSize: zipInfo.zipSize, }, } code, resultCh, err := c.sendFileDirectory(ctx, offer, zipInfo.file, opts...) if err != nil { return "", nil, err } // intercept result chan to close our tmpfile after we are done with it retCh := make(chan SendResult, 1) go func() { r := <-resultCh zipInfo.file.Close() retCh <- r }() return code, retCh, err } type zipResult struct { file *os.File numBytes int64 numFiles int64 zipSize int64 } func makeTmpZip(directoryName string, entries []DirectoryEntry) (*zipResult, error) { f, err := ioutil.TempFile("", "wormhole-william-dir") if err != nil { return nil, err } if len(entries) < 1 { return nil, errors.New("no files provided") } defer os.Remove(f.Name()) if strings.TrimSpace(directoryName) == "" { return nil, errors.New("directoryName must be set") } prefix, _ := filepath.Split(directoryName) if prefix != "" { return nil, errors.New("directoryName must not include sub directories") } w := zip.NewWriter(f) var totalBytes int64 prefixPath := filepath.ToSlash(directoryName) + "/" for _, entry := range entries { entryPath := filepath.ToSlash(entry.Path) if !strings.HasPrefix(entryPath, prefixPath) { return nil, errors.New("each directory entry must be prefixed with the directoryName") } header := &zip.FileHeader{ Name: strings.TrimPrefix(entryPath, prefixPath), Method: zip.Deflate, } header.SetMode(entry.Mode) f, err := w.CreateHeader(header) if err != nil { return nil, err } r, err := entry.Reader() if err != nil { return nil, err } n, err := io.Copy(f, r) if err != nil { return nil, err } totalBytes += n err = r.Close() if err != nil { return nil, err } } err = w.Close() if err != nil { return nil, err } zipSize, err := readSeekerSize(f) if err != nil { return nil, err } result := zipResult{ file: f, numBytes: totalBytes, numFiles: int64(len(entries)), zipSize: zipSize, } return &result, nil } func readSeekerSize(r io.ReadSeeker) (int64, error) { size, err := r.Seek(0, io.SeekEnd) if err != nil { return -1, err } _, err = r.Seek(0, io.SeekStart) if err != nil { return -1, err } return size, nil } type sendOptions struct { code string progressFunc progressFunc } type SendOption interface { setOption(*sendOptions) error } type sendCodeOption struct { code string } func (o sendCodeOption) setOption(opts *sendOptions) error { if err := validateCode(o.code); err != nil { return err } opts.code = o.code return nil } func validateCode(code string) error { if code == "" { return nil } _, err := nameplateFromCode(code) if err != nil { return err } if strings.Contains(code, " ") { return errors.New("code must not contain spaces") } return nil } // WithCode returns a SendOption to use a specific nameplate+code // instead of generating one dynamically. func WithCode(code string) SendOption { return sendCodeOption{code: code} } type progressFunc func(sentBytes int64, totalBytes int64) type progressSendOption struct { progressFunc progressFunc } func (o progressSendOption) setOption(opts *sendOptions) error { opts.progressFunc = o.progressFunc return nil } // WithProgress returns a SendOption to track the progress of the data // transfer. It takes a callback function that will be called for each // chunk of data successfully written. // // WithProgress is only minimally supported in SendText. SendText does // not use the wormhole transit protocol so it is not able to detect // the progress of the receiver. This limitation does not apply to // SendFile or SendDirectory. func WithProgress(f func(sentBytes int64, totalBytes int64)) SendOption { return progressSendOption{f} } wormhole-william-1.0.6/wormhole/wormhole.go000066400000000000000000000350321431053014400210230ustar00rootroot00000000000000// Package wormhole provides a magic wormhole client implementation. package wormhole import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net" "reflect" "strconv" "strings" "sync" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/secretbox" "salsa.debian.org/vasudev/gospake2" ) // A Client is wormhole client. Its zero value is a usable client. type Client struct { // AppID is the identity string of the client sent to the rendezvous // server. Two clients can only communicate if they have the same // AppID. The AppID should be a domain name + path to make it // globally unique. If empty, WormholeCLIAppID will be used. AppID string // RendezvousURL is the url of the Rendezvous server. If empty, // DefaultRendezvousURL will be used. RendezvousURL string // TransitRelayAddress is the host:port address to offer // to use for file transfers where direct connections are unavailable. // If empty, DefaultTransitRelayAddress will be used. TransitRelayAddress string // PassPhraseComponentLength is the number of words to use // when generating a passprase. Any value less than 2 will // default to 2. PassPhraseComponentLength int // VerifierOk specifies an optional hook to be called before // transmitting/receiving the encrypted payload. // // If VerifierOk is non-nil it will be called after the PAKE // hand-shake has succeeded passing in the verifier code. Callers // can then prompt the user to confirm the code matches via an out // of band mechanism before proceeding with the file transmission. // If VerifierOk returns false the transmission will be aborted. VerifierOk func(verifier string) bool } var ( // WormholeCLIAppID is the AppID used by the python magic wormhole // client. In order to interoperate with that client you must use // the same AppID. WormholeCLIAppID = "lothar.com/wormhole/text-or-file-xfer" // DefaultRendezvousURL is the default Rendezvous server to use. DefaultRendezvousURL = "ws://relay.magic-wormhole.io:4000/v1" // DefaultTransitRelayAddress is the default transit server to ues. DefaultTransitRelayAddress = "transit.magic-wormhole.io:4001" ) func (c *Client) url() string { if c.RendezvousURL != "" { return c.RendezvousURL } return DefaultRendezvousURL } func (c *Client) appID() string { if c.AppID != "" { return c.AppID } return WormholeCLIAppID } func (c *Client) wordCount() int { if c.PassPhraseComponentLength > 1 { return c.PassPhraseComponentLength } else { return 2 } } func (c *Client) relayAddr() string { if c.TransitRelayAddress != "" { return c.TransitRelayAddress } return DefaultTransitRelayAddress } func (c *Client) validateRelayAddr() error { if c.relayAddr() == "" { return nil } _, _, err := net.SplitHostPort(c.relayAddr()) return err } // SendResult has information about whether or not a Send command was successful. type SendResult struct { OK bool Error error } var errDecryptFailed = errors.New("decrypt message failed") func openAndUnmarshal(v interface{}, mb rendezvous.MailboxEvent, sharedKey []byte) error { keySlice := derivePhaseKey(string(sharedKey), mb.Side, mb.Phase) nonceAndSealedMsg, err := hex.DecodeString(mb.Body) if err != nil { return err } nonce, sealedMsg := splitNonce(nonceAndSealedMsg) var openKey [32]byte copy(openKey[:], keySlice) out, ok := secretbox.Open(nil, sealedMsg, &nonce, &openKey) if !ok { return errDecryptFailed } return json.Unmarshal(out, v) } func sendEncryptedMessage(ctx context.Context, rc *rendezvous.Client, msg, sharedKey []byte, sideID, phase string) error { var sealKey [32]byte nonce := crypto.RandNonce() msgKey := derivePhaseKey(string(sharedKey), sideID, phase) copy(sealKey[:], msgKey) sealedMsg := secretbox.Seal(nil, msg, &nonce, &sealKey) nonceAndSealedMsg := append(nonce[:], sealedMsg...) hexNonceAndSealedMsg := hex.EncodeToString(nonceAndSealedMsg) return rc.AddMessage(ctx, phase, hexNonceAndSealedMsg) } func jsonHexMarshal(msg interface{}) string { jsonMsg, err := json.Marshal(msg) if err != nil { panic(err) } return hex.EncodeToString(jsonMsg) } func jsonHexUnmarshal(hexStr string, msg interface{}) error { b, err := hex.DecodeString(hexStr) if err != nil { return err } return json.Unmarshal(b, msg) } const secreboxKeySize = 32 func derivePhaseKey(key, side, phase string) []byte { sideSha := sha256.Sum256([]byte(side)) phaseSha := sha256.Sum256([]byte(phase)) purpose := "wormhole:phase:" + string(sideSha[:]) + string(phaseSha[:]) r := hkdf.New(sha256.New, []byte(key), nil, []byte(purpose)) out := make([]byte, secreboxKeySize) _, err := io.ReadFull(r, out) if err != nil { panic(err) } return out } func deriveTransitKey(key []byte, appID string) []byte { purpose := appID + "/transit-key" r := hkdf.New(sha256.New, key, nil, []byte(purpose)) out := make([]byte, secreboxKeySize) _, err := io.ReadFull(r, out) if err != nil { panic(err) } return out } func deriveVerifier(key []byte) []byte { purpose := "wormhole:verifier" r := hkdf.New(sha256.New, key, nil, []byte(purpose)) out := make([]byte, secreboxKeySize) _, err := io.ReadFull(r, out) if err != nil { panic(err) } return out } type pakeMsg struct { Body string `json:"pake_v1"` } type offerMsg struct { Message *string `json:"message,omitempty"` Directory *offerDirectory `json:"directory,omitempty"` File *offerFile `json:"file,omitempty"` } func (m *offerMsg) Type() collectType { return collectOffer } type offerDirectory struct { Dirname string `json:"dirname"` Mode string `json:"mode"` NumBytes int64 `json:"numbytes"` NumFiles int64 `json:"numfiles"` ZipSize int64 `json:"zipsize"` } type offerFile struct { FileName string `json:"filename"` FileSize int64 `json:"filesize"` } type genericMessage struct { Offer *offerMsg `json:"offer,omitempty"` Answer *answerMsg `json:"answer,omitempty"` Transit *transitMsg `json:"transit,omitempty"` AppVersions *appVersionsMsg `json:"app_versions,omitempty"` Error *string `json:"error,omitempty"` } type appVersionsMsg struct { } type answerMsg struct { MessageAck string `json:"message_ack,omitempty"` FileAck string `json:"file_ack,omitempty"` } func (m *answerMsg) Type() collectType { return collectAnswer } type collectable interface { Type() collectType } func splitNonce(sealedMsg []byte) (nonce [24]byte, msg []byte) { copy(nonce[:], sealedMsg[:24]) return nonce, sealedMsg[24:] } type transitAbility struct { Type string `json:"type"` } type transitHintsV1 struct { Hostname string `json:"hostname"` Port int `json:"port"` Priority float64 `json:"priority"` Type string `json:"type"` Hints []transitHintsV1Hint `json:"hints"` } type transitHintsV1Hint struct { Hostname string `json:"hostname"` Port int `json:"port"` Priority float64 `json:"priority"` Type string `json:"type"` } type transitMsg struct { AbilitiesV1 []transitAbility `json:"abilities-v1"` HintsV1 []transitHintsV1 `json:"hints-v1"` } func (m *transitMsg) Type() collectType { return collectTransit } type msgCollector struct { sharedKey []byte collectOffer bool collectTransit bool collectAnswer bool subscribe chan *collectSubscription closeMu sync.Mutex closed bool done chan error } func newMsgCollector(sharedKey []byte) *msgCollector { return &msgCollector{ sharedKey: sharedKey, subscribe: make(chan *collectSubscription), done: make(chan error, 1), } } func (c *msgCollector) close() { c.closeMu.Lock() defer c.closeMu.Unlock() if !c.closed { c.closed = true close(c.done) } } func (c *msgCollector) closeWithErr(err error) { c.closeMu.Lock() defer c.closeMu.Unlock() if !c.closed { c.closed = true c.done <- err close(c.done) } } func (c *msgCollector) waitFor(msg collectable) error { if reflect.ValueOf(msg).Kind() != reflect.Ptr { return errors.New("you must pass waitFor a pointer to a struct") } sub := collectSubscription{ collectMsg: msg, result: make(chan collectResult, 1), } select { case err := <-c.done: if err != nil { return err } return errors.New("msgCollector closed") case c.subscribe <- &sub: } result := <-sub.result if result.err != nil { return result.err } dst := reflect.ValueOf(msg).Elem() src := reflect.ValueOf(result.result).Elem() dst.Set(src) return nil } type collectResult struct { err error result collectable } type collectSubscription struct { collectMsg collectable result chan collectResult } func (c *msgCollector) collect(ch <-chan rendezvous.MailboxEvent) { pendingMsgs := make(map[collectType]collectable) waiters := make(map[collectType]*collectSubscription) errorResult := func(e error) { c.closeWithErr(e) for t, waiter := range waiters { waiter.result <- collectResult{ err: e, } delete(waiters, t) } } for { select { case <-c.done: return case sub := <-c.subscribe: collectType := sub.collectMsg.Type() if m := pendingMsgs[collectType]; m != nil { sub.result <- collectResult{ result: m, } delete(pendingMsgs, collectType) } else { if waiters[collectType] != nil { sub.result <- collectResult{ err: errors.New("there is already a pending collect request for this type"), } } else { waiters[collectType] = sub } } case gotMsg, ok := <-ch: if !ok { c.close() return } if gotMsg.Error != nil { errorResult(gotMsg.Error) return } if _, err := strconv.Atoi(gotMsg.Phase); err != nil { errorResult(fmt.Errorf("got unexpected phase: %s", gotMsg.Phase)) return } var msg genericMessage err := openAndUnmarshal(&msg, gotMsg, c.sharedKey) if err != nil { errorResult(err) return } var resultMsg collectable var t collectType if msg.Offer != nil { t = collectOffer resultMsg = msg.Offer } else if msg.Transit != nil { t = collectTransit resultMsg = msg.Transit } else if msg.Answer != nil { t = collectAnswer resultMsg = msg.Answer } else if msg.Error != nil { errorResult(fmt.Errorf("TransferError: %s", *msg.Error)) return } else { continue } if sub := waiters[t]; sub != nil { sub.result <- collectResult{ result: resultMsg, } delete(waiters, t) } else { if pendingMsgs[t] != nil { errorResult(fmt.Errorf("got multiple messages of type %s", t)) return } pendingMsgs[t] = resultMsg } } } } type clientProtocol struct { sharedKey []byte phaseCounter int ch <-chan rendezvous.MailboxEvent rc *rendezvous.Client spake *gospake2.SPAKE2 sideID string appID string } func newClientProtocol(ctx context.Context, rc *rendezvous.Client, sideID, appID string) *clientProtocol { recvChan := rc.MsgChan(ctx) return &clientProtocol{ ch: recvChan, rc: rc, sideID: sideID, appID: appID, } } func (cc *clientProtocol) WritePake(ctx context.Context, code string) error { pw := gospake2.NewPassword(code) spake := gospake2.SPAKE2Symmetric(pw, gospake2.NewIdentityS(cc.appID)) cc.spake = &spake pakeMsgBody := cc.spake.Start() pm := pakeMsg{ Body: hex.EncodeToString(pakeMsgBody), } return cc.rc.AddMessage(ctx, "pake", jsonHexMarshal(pm)) } func (cc *clientProtocol) ReadPake(ctx context.Context) error { var pake pakeMsg err := cc.readPlaintext(ctx, "pake", &pake) if err != nil { return err } otherSidesMsg, err := hex.DecodeString(pake.Body) if err != nil { return err } sharedKey, err := cc.spake.Finish(otherSidesMsg) if err != nil { return err } cc.sharedKey = sharedKey return nil } func (cc *clientProtocol) Verifier() ([]byte, error) { if cc.sharedKey == nil { return nil, errors.New("shared key not established yet") } return deriveVerifier(cc.sharedKey), nil } func (cc *clientProtocol) WriteVersion(ctx context.Context) error { phase := "version" verInfo := genericMessage{ AppVersions: &appVersionsMsg{}, } jsonOut, err := json.Marshal(verInfo) if err != nil { return err } err = sendEncryptedMessage(ctx, cc.rc, jsonOut, cc.sharedKey, cc.sideID, phase) return err } func (cc *clientProtocol) ReadVersion() (*appVersionsMsg, error) { var v appVersionsMsg err := cc.openAndUnmarshal("version", &v) if err != nil { return nil, err } return &v, nil } func (cc *clientProtocol) WriteAppData(ctx context.Context, v *genericMessage) error { nextPhase := cc.phaseCounter cc.phaseCounter++ jsonOut, err := json.Marshal(v) if err != nil { return err } phase := strconv.Itoa(nextPhase) return sendEncryptedMessage(ctx, cc.rc, jsonOut, cc.sharedKey, cc.sideID, phase) } func (cc *clientProtocol) openAndUnmarshal(phase string, v interface{}) error { gotMsg := <-cc.ch if gotMsg.Error != nil { return gotMsg.Error } if gotMsg.Phase != phase { return fmt.Errorf("got unexpected phase while waiting for %s: %s", phase, gotMsg.Phase) } return openAndUnmarshal(v, gotMsg, cc.sharedKey) } func (cc *clientProtocol) readPlaintext(ctx context.Context, phase string, v interface{}) error { var gotMsg rendezvous.MailboxEvent select { case gotMsg = <-cc.ch: case <-ctx.Done(): return ctx.Err() } if gotMsg.Error != nil { return gotMsg.Error } if gotMsg.Phase != phase { return fmt.Errorf("got unexpected phase while waiting for %s: %s", phase, gotMsg.Phase) } err := jsonHexUnmarshal(gotMsg.Body, &v) if err != nil { return err } return nil } type collectType int const ( collectOffer collectType = iota + 1 collectTransit collectAnswer ) func (ct collectType) String() string { switch ct { case collectOffer: return "Offer" case collectTransit: return "Transit" case collectAnswer: return "Answer" default: return fmt.Sprintf("collectTypeUnkown<%d>", ct) } } func (cc *clientProtocol) Collect(msgTypes ...collectType) (*msgCollector, error) { collector := newMsgCollector(cc.sharedKey) for _, mt := range msgTypes { switch mt { case collectOffer: collector.collectOffer = true case collectTransit: collector.collectTransit = true case collectAnswer: collector.collectAnswer = true default: return nil, fmt.Errorf("unknown collect msg type %d", msgTypes) } } go collector.collect(cc.ch) return collector, nil } func nameplateFromCode(code string) (string, error) { nameplate := strings.SplitN(code, "-", 2)[0] _, err := strconv.Atoi(nameplate) if err != nil { return "", errors.New("non-numeric nameplate") } return nameplate, nil } wormhole-william-1.0.6/wormhole/wormhole_test.go000066400000000000000000000442111431053014400220610ustar00rootroot00000000000000package wormhole import ( "bytes" "context" "encoding/hex" "errors" "fmt" "io" "io/ioutil" "net" "os" "path/filepath" "runtime/pprof" "strings" "sync" "testing" "time" "github.com/klauspost/compress/zip" "github.com/psanford/wormhole-william/internal/crypto" "github.com/psanford/wormhole-william/rendezvous" "github.com/psanford/wormhole-william/rendezvous/rendezvousservertest" ) func TestWormholeSendRecvText(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() // disable transit relay DefaultTransitRelayAddress = "" var c0Verifier string var c0 Client c0.RendezvousURL = url c0.VerifierOk = func(code string) bool { c0Verifier = code return true } var c1Verifier string var c1 Client c1.RendezvousURL = url c1.VerifierOk = func(code string) bool { c1Verifier = code return true } secretText := "Hialeah-deviltry" code, statusChan, err := c0.SendText(ctx, secretText) if err != nil { t.Fatal(err) } nameplate := strings.SplitN(code, "-", 2)[0] // recv with wrong code _, err = c1.Receive(ctx, fmt.Sprintf("%s-intermarrying-aliased", nameplate)) if err != errDecryptFailed { t.Fatalf("Recv side expected decrypt failed due to wrong code but got: %s", err) } status := <-statusChan if status.OK || status.Error != errDecryptFailed { t.Fatalf("Send side expected decrypt failed but got status: %+v", status) } code, statusChan, err = c0.SendText(ctx, secretText) if err != nil { t.Fatal(err) } // recv with correct code msg, err := c1.Receive(ctx, code) if err != nil { t.Fatalf("Recv side got unexpected err: %s", err) } msgBody, err := ioutil.ReadAll(msg) if err != nil { t.Fatalf("Recv side got read err: %s", err) } if string(msgBody) != secretText { t.Fatalf("Got Message does not match sent secret got=%s sent=%s", msgBody, secretText) } status = <-statusChan if !status.OK || status.Error != nil { t.Fatalf("Send side expected OK status but got: %+v", status) } if c0Verifier != c1Verifier { t.Fatalf("Expected verifiers to match but were different") } // Send with progress // we should get one update for progress when we get the ok // result back from the receiver secretText = "retrospectives-𐄷-cropper" var ( progressSentBytes int64 progressTotalBytes int64 progressCallCount int ) progressFunc := func(sentBytes int64, totalBytes int64) { progressCallCount++ progressSentBytes = sentBytes progressTotalBytes = totalBytes } code, statusChan, err = c0.SendText(ctx, secretText, WithProgress(progressFunc)) if err != nil { t.Fatal(err) } // recv with correct code msg, err = c1.Receive(ctx, code) if err != nil { t.Fatalf("Recv side got unexpected err: %s", err) } msgBody, err = ioutil.ReadAll(msg) if err != nil { t.Fatalf("Recv side got read err: %s", err) } if string(msgBody) != secretText { t.Fatalf("Got Message does not match sent secret got=%s sent=%s", msgBody, secretText) } status = <-statusChan if !status.OK || status.Error != nil { t.Fatalf("Send side expected OK status but got: %+v", status) } if c0Verifier != c1Verifier { t.Fatalf("Expected verifiers to match but were different") } if progressCallCount != 1 { t.Fatalf("progressCallCount got %d expected 1", progressCallCount) } if progressSentBytes != int64(len(msgBody)) { t.Fatalf("progressSentBytes got %d expected %d", progressSentBytes, int64(len(msgBody))) } if progressTotalBytes != int64(len(msgBody)) { t.Fatalf("progressTotalBytes got %d expected %d", progressTotalBytes, int64(len(msgBody))) } } func TestVerifierAbort(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() // disable transit relay DefaultTransitRelayAddress = "" var c0 Client c0.RendezvousURL = url c0.VerifierOk = func(code string) bool { return false } var c1 Client c1.RendezvousURL = url c1.VerifierOk = func(code string) bool { return true } secretText := "minced-incalculably" code, statusChan, err := c0.SendText(ctx, secretText) if err != nil { t.Fatal(err) } // recv with correct code _, err = c1.Receive(ctx, code) expectErr := errors.New("TransferError: sender rejected verification check, abandoned transfer") if err.Error() != expectErr.Error() { t.Fatalf("Expected recv err %q got %q", expectErr, err) } status := <-statusChan expectErr = errors.New("sender rejected verification check, abandoned transfer") if status.Error.Error() != expectErr.Error() { t.Fatalf("Send side expected %q error but got: %q", expectErr, status.Error) } } func TestWormholeFileReject(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() // disable transit relay for this test DefaultTransitRelayAddress = "" var c0 Client c0.RendezvousURL = url var c1 Client c1.RendezvousURL = url fileContent := make([]byte, 1<<16) for i := 0; i < len(fileContent); i++ { fileContent[i] = byte(i) } buf := bytes.NewReader(fileContent) code, resultCh, err := c0.SendFile(ctx, "file.txt", buf) if err != nil { t.Fatal(err) } receiver, err := c1.Receive(ctx, code) if err != nil { t.Fatal(err) } receiver.Reject() result := <-resultCh expectErr := "TransferError: transfer rejected" if result.Error.Error() != expectErr { t.Fatalf("Expected %q result but got: %+v", expectErr, result) } } func TestWormholeFileTransportSendRecvViaRelayServer(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() var c0 Client c0.RendezvousURL = url c0.TransitRelayAddress = relayServer.addr var c1 Client c1.RendezvousURL = url c1.TransitRelayAddress = relayServer.addr fileContent := make([]byte, 1<<16) for i := 0; i < len(fileContent); i++ { fileContent[i] = byte(i) } buf := bytes.NewReader(fileContent) code, resultCh, err := c0.SendFile(ctx, "file.txt", buf) if err != nil { t.Fatal(err) } receiver, err := c1.Receive(ctx, code) if err != nil { t.Fatal(err) } got, err := ioutil.ReadAll(receiver) if err != nil { t.Fatal(err) } if !bytes.Equal(got, fileContent) { t.Fatalf("File contents mismatch") } result := <-resultCh if !result.OK { t.Fatalf("Expected ok result but got: %+v", result) } } func TestWormholeBigFileTransportSendRecvViaRelayServer(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() var c0 Client c0.RendezvousURL = url c0.TransitRelayAddress = relayServer.addr var c1 Client c1.RendezvousURL = url c1.TransitRelayAddress = relayServer.addr // Create a fake file offer var fakeBigSize int64 = 32098461509 offer := &offerMsg{ File: &offerFile{ FileName: "fakefile", FileSize: fakeBigSize, }, } // just a pretend reader r := bytes.NewReader(make([]byte, 1)) // skip th wrapper so we can provide our own offer code, _, err := c0.sendFileDirectory(ctx, offer, r) //c0.SendFile(ctx, "file.txt", buf) if err != nil { t.Fatal(err) } receiver, err := c1.Receive(ctx, code) if err != nil { t.Fatal(err) } if int64(receiver.TransferBytes64) != fakeBigSize { t.Fatalf("Mismatch in size between what we are trying to send and what is (our parsed) offer. Expected %v but got %v", fakeBigSize, receiver.TransferBytes64) } } func TestWormholeFileTransportRecvMidStreamCancel(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() var c0 Client c0.RendezvousURL = url c0.TransitRelayAddress = relayServer.addr var c1 Client c1.RendezvousURL = url c1.TransitRelayAddress = relayServer.addr fileContent := make([]byte, 1<<16) for i := 0; i < len(fileContent); i++ { fileContent[i] = byte(i) } buf := bytes.NewReader(fileContent) code, resultCh, err := c0.SendFile(ctx, "file.txt", buf) if err != nil { t.Fatal(err) } childCtx, cancel := context.WithCancel(ctx) defer cancel() receiver, err := c1.Receive(childCtx, code) if err != nil { t.Fatal(err) } initialBuffer := make([]byte, 1<<10) _, err = io.ReadFull(receiver, initialBuffer) if err != nil { t.Fatal(err) } cancel() _, err = ioutil.ReadAll(receiver) if err == nil { t.Fatalf("Expected read error but got none") } result := <-resultCh if result.OK { t.Fatalf("Expected error result but got ok") } } func TestWormholeFileTransportSendMidStreamCancel(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() var c0 Client c0.RendezvousURL = url c0.TransitRelayAddress = relayServer.addr var c1 Client c1.RendezvousURL = url c1.TransitRelayAddress = relayServer.addr fileContent := make([]byte, 1<<16) for i := 0; i < len(fileContent); i++ { fileContent[i] = byte(i) } sendCtx, cancel := context.WithCancel(ctx) splitR := splitReader{ Reader: bytes.NewReader(fileContent), cancelAt: 1 << 10, cancel: cancel, } code, resultCh, err := c0.SendFile(sendCtx, "file.txt", &splitR) if err != nil { t.Fatal(err) } receiver, err := c1.Receive(ctx, code) if err != nil { t.Fatal(err) } gotMsg, err := ioutil.ReadAll(receiver) if err == nil { t.Fatalf("Expected read error but got none. got msg size: %d, orig_size: %d, cancel_at: %de", len(gotMsg), len(fileContent), splitR.cancelAt) } result := <-resultCh if result.OK { t.Fatal("Expected send resultCh to error but got none") } } func TestPendingSendCancelable(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() c0 := Client{ RendezvousURL: url, TransitRelayAddress: relayServer.addr, } fileContent := make([]byte, 1<<16) for i := 0; i < len(fileContent); i++ { fileContent[i] = byte(i) } buf := bytes.NewReader(fileContent) childCtx, cancel := context.WithCancel(ctx) defer cancel() code, resultCh, err := c0.SendFile(childCtx, "file.txt", buf) if err != nil { t.Fatal(err) } // connect to mailbox to wait for c0 to write its initial message rc := rendezvous.NewClient(url, crypto.RandSideID(), c0.appID()) _, err = rc.Connect(ctx) if err != nil { t.Fatal(err) } defer rc.Close(ctx, rendezvous.Happy) nameplate, err := nameplateFromCode(code) if err != nil { t.Fatal(err) } err = rc.AttachMailbox(ctx, nameplate) if err != nil { t.Fatal(err) } msgs := rc.MsgChan(ctx) select { case <-msgs: case <-time.After(5 * time.Second): t.Fatal("timeout waiting for c0 to send a message") } cancel() select { case result := <-resultCh: if result.OK { t.Fatalf("Expected cancellation error but got OK") } if result.Error == nil { t.Fatalf("Expected cancellation error") } case <-time.After(5 * time.Second): // log all goroutines pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) t.Fatalf("Wait for result timed out") } } func TestPendingRecvCancelable(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() testDisableLocalListener = true defer func() { testDisableLocalListener = false }() relayServer := newTestRelayServer() defer relayServer.close() c0 := Client{ RendezvousURL: url, TransitRelayAddress: relayServer.addr, } childCtx, cancel := context.WithCancel(ctx) defer cancel() code := "87-firetrap-fallacy" resultCh := make(chan error, 1) go func() { _, err := c0.Receive(childCtx, code) resultCh <- err }() // wait to see mailbox has been allocated, and then // wait to see PAKE message from receiver rc := rendezvous.NewClient(url, crypto.RandSideID(), c0.appID()) _, err := rc.Connect(ctx) if err != nil { t.Fatal(err) } defer rc.Close(ctx, rendezvous.Happy) for i := 0; i < 20; i++ { nameplates, err := rc.ListNameplates(ctx) if err != nil { t.Fatal(err) } if len(nameplates) > 0 { break } time.Sleep(5 * time.Millisecond) } defer rc.Close(ctx, rendezvous.Happy) nameplate, err := nameplateFromCode(code) if err != nil { t.Fatal(err) } err = rc.AttachMailbox(ctx, nameplate) if err != nil { t.Fatal(err) } msgs := rc.MsgChan(ctx) select { case <-msgs: case <-time.After(5 * time.Second): t.Fatal("timeout waiting for c0 to send a message") } cancel() select { case gotErr := <-resultCh: if gotErr == nil { t.Fatalf("Expected an error but got none") } case <-time.After(5 * time.Second): // log all goroutines pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) t.Fatalf("Timeout waiting for recv cancel") } } func TestWormholeDirectoryTransportSendRecvDirect(t *testing.T) { ctx := context.Background() rs := rendezvousservertest.NewServer() defer rs.Close() url := rs.WebSocketURL() // disable transit relay for this test DefaultTransitRelayAddress = "" var c0Verifier string var c0 Client c0.RendezvousURL = url c0.VerifierOk = func(code string) bool { c0Verifier = code return true } var c1Verifier string var c1 Client c1.RendezvousURL = url c1.VerifierOk = func(code string) bool { c1Verifier = code return true } personalizeContent := make([]byte, 1<<16) for i := 0; i < len(personalizeContent); i++ { personalizeContent[i] = byte(i) } bodiceContent := []byte("placarding-whereat") entries := []DirectoryEntry{ { Path: filepath.Join("skyjacking", "personalize.txt"), Reader: func() (io.ReadCloser, error) { b := bytes.NewReader(personalizeContent) return ioutil.NopCloser(b), nil }, }, { Path: filepath.Join("skyjacking", "bodice-Maytag.txt"), Reader: func() (io.ReadCloser, error) { b := bytes.NewReader(bodiceContent) return ioutil.NopCloser(b), nil }, }, } code, resultCh, err := c0.SendDirectory(ctx, "skyjacking", entries) if err != nil { t.Fatal(err) } receiver, err := c1.Receive(ctx, code) if err != nil { t.Fatal(err) } got, err := ioutil.ReadAll(receiver) if err != nil { t.Fatal(err) } r, err := zip.NewReader(bytes.NewReader(got), int64(len(got))) if err != nil { t.Fatal(err) } for _, f := range r.File { rc, err := f.Open() if err != nil { t.Fatal(err) } body, err := ioutil.ReadAll(rc) if err != nil { t.Fatal(err) } rc.Close() if f.Name == "personalize.txt" { if !bytes.Equal(body, personalizeContent) { t.Fatal("personalize.txt file content does not match") } } else if f.Name == "bodice-Maytag.txt" { if !bytes.Equal(bodiceContent, body) { t.Fatalf("bodice-Maytag.txt file content does not match %s vs %s", bodiceContent, body) } } else { t.Fatalf("Unexpected file %s", f.Name) } } result := <-resultCh if !result.OK { t.Fatalf("Expected ok result but got: %+v", result) } if c0Verifier == "" || c1Verifier == "" { t.Fatalf("Failed to get verifier code c0=%q c1=%q", c0Verifier, c1Verifier) } if c0Verifier != c1Verifier { t.Fatalf("Expected verifiers to match but were different") } } type testRelayServer struct { l net.Listener addr string wg sync.WaitGroup mu sync.Mutex streams map[string]net.Conn } func newTestRelayServer() *testRelayServer { l, err := net.Listen("tcp", ":0") if err != nil { panic(err) } rs := &testRelayServer{ l: l, addr: l.Addr().String(), streams: make(map[string]net.Conn), } go rs.run() return rs } func (ts *testRelayServer) close() { ts.l.Close() ts.wg.Wait() } func (ts *testRelayServer) run() { for { conn, err := ts.l.Accept() if err != nil { return } ts.wg.Add(1) go ts.handleConn(conn) } } var headerPrefix = []byte("please relay ") var headerSide = []byte(" for side ") func (ts *testRelayServer) handleConn(c net.Conn) { // requests look like: // "please relay 10bf5ab71e48a3ca74b0a0d4d54f66f38704a76d15885442a8df141680fd for side 4a74cb8a377c970a\n" defer ts.wg.Done() headerBuf := make([]byte, 64) matchExpect := func(expect []byte) bool { got := headerBuf[:len(expect)] _, err := io.ReadFull(c, got) if err != nil { c.Close() return false } if !bytes.Equal(got, expect) { c.Write([]byte("bad handshake\n")) c.Close() return false } return true } isHex := func(str string) bool { _, err := hex.DecodeString(str) if err != nil { c.Write([]byte("bad handshake\n")) c.Close() return false } return true } if !matchExpect(headerPrefix) { return } _, err := io.ReadFull(c, headerBuf) if err != nil { c.Close() return } chanID := string(headerBuf) if !isHex(chanID) { return } if !matchExpect(headerSide) { return } sideBuf := headerBuf[:16] _, err = io.ReadFull(c, sideBuf) if err != nil { c.Close() return } side := string(sideBuf) if !isHex(side) { return } // read \n _, err = io.ReadFull(c, headerBuf[:1]) if err != nil { c.Close() return } ts.mu.Lock() existing, found := ts.streams[chanID] if !found { ts.streams[chanID] = c } ts.mu.Unlock() if found { existing.Write([]byte("ok\n")) c.Write([]byte("ok\n")) go func() { io.Copy(c, existing) existing.Close() c.Close() }() io.Copy(existing, c) c.Close() existing.Close() } } type splitReader struct { *bytes.Reader offset int cancelAt int cancel func() didCancel bool } func (s *splitReader) Read(b []byte) (int, error) { n, err := s.Reader.Read(b) s.offset += n if !s.didCancel && s.offset >= s.cancelAt { s.cancel() s.didCancel = true // yield the cpu to give the cancellation goroutine a chance // to run (esp important for when GOMAXPROCS=1) time.Sleep(1 * time.Millisecond) } return n, err }