cmd/tailscale/cli: add push subcommand
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
eeacf84dae
commit
1e26d4ae19
|
@ -69,6 +69,7 @@ change in the future.
|
||||||
pingCmd,
|
pingCmd,
|
||||||
versionCmd,
|
versionCmd,
|
||||||
webCmd,
|
webCmd,
|
||||||
|
pushCmd,
|
||||||
},
|
},
|
||||||
FlagSet: rootfs,
|
FlagSet: rootfs,
|
||||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pushCmd = &ffcli.Command{
|
||||||
|
Name: "push",
|
||||||
|
ShortUsage: "push [--flags] <hostname-or-IP> <file>",
|
||||||
|
ShortHelp: "Push a file to a host",
|
||||||
|
Exec: runPush,
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("push", flag.ExitOnError)
|
||||||
|
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||||
|
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var pushArgs struct {
|
||||||
|
name string
|
||||||
|
verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPush(ctx context.Context, args []string) error {
|
||||||
|
if len(args) != 2 || args[0] == "" {
|
||||||
|
return errors.New("usage: push <hostname-or-IP> <file>")
|
||||||
|
}
|
||||||
|
var ip string
|
||||||
|
|
||||||
|
hostOrIP, fileArg := args[0], args[1]
|
||||||
|
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerAPIPort, err := discoverPeerAPIPort(ctx, ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileContents io.Reader
|
||||||
|
var name = pushArgs.name
|
||||||
|
if fileArg == "-" {
|
||||||
|
fileContents = os.Stdin
|
||||||
|
if name == "" {
|
||||||
|
sniff, err := io.ReadAll(io.LimitReader(fileContents, 4<<20))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exts, _ := mime.ExtensionsByType(http.DetectContentType(sniff)); len(exts) > 0 {
|
||||||
|
name = "stdin" + exts[0]
|
||||||
|
} else {
|
||||||
|
name = "stdin.txt"
|
||||||
|
}
|
||||||
|
fileContents = io.MultiReader(bytes.NewReader(sniff), fileContents)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(fileArg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
fileContents = f
|
||||||
|
if name == "" {
|
||||||
|
name = fileArg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstURL := "http://" + net.JoinHostPort(ip, fmt.Sprint(peerAPIPort)) + "/v0/put/" + url.PathEscape(name)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pushArgs.verbose {
|
||||||
|
log.Printf("sending to %v ...", dstURL)
|
||||||
|
}
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode == 200 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
io.Copy(os.Stdout, res.Body)
|
||||||
|
return errors.New(res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverPeerAPIPort(ctx context.Context, ip string) (port uint16, err error) {
|
||||||
|
c, bc, ctx, cancel := connect(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
prc := make(chan *ipnstate.PingResult, 2)
|
||||||
|
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||||
|
if n.ErrMessage != nil {
|
||||||
|
log.Fatal(*n.ErrMessage)
|
||||||
|
}
|
||||||
|
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||||
|
prc <- pr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go pump(ctx, bc, c)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
discoPings := 0
|
||||||
|
timer := time.NewTimer(10 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
sendPings := func() {
|
||||||
|
bc.Ping(ip, false)
|
||||||
|
bc.Ping(ip, true)
|
||||||
|
}
|
||||||
|
sendPings()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
sendPings()
|
||||||
|
case <-timer.C:
|
||||||
|
return 0, fmt.Errorf("timeout contacting %v; it offline?", ip)
|
||||||
|
case pr := <-prc:
|
||||||
|
if p := pr.PeerAPIPort; p != 0 {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
discoPings++
|
||||||
|
if discoPings == 3 {
|
||||||
|
return 0, fmt.Errorf("%v is online, but seems to be running an old Tailscale version", ip)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue