cmd/gomote: add gomote rdp subcommand to open an RDP proxy to a Windows buildlet

Fixes golang/go#26090

Change-Id: Ib58fb7767384778b67d0d62d34c1132d70d75c23
Reviewed-on: https://go-review.googlesource.com/c/build/+/207378
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index ea7f56b..3c51e08 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -188,7 +188,7 @@
 
 // RemoteName returns the name of this client's buildlet on the
 // coordinator. If this buildlet isn't a remote buildlet created via
-// a buildlet, this returns the empty string.
+// gomote, this returns the empty string.
 func (c *Client) RemoteName() string {
 	return c.remoteBuildlet
 }
@@ -258,6 +258,37 @@
 	return c.httpClient.Do(req)
 }
 
+// ProxyTCP connects to the given port on the remote buildlet.
+// The buildlet client must currently be a gomote client (RemoteName != "")
+// and the target type must be a VM type running on GCE. This was primarily
+// created for RDP to Windows machines, but it might get reused for other
+// purposes in the future.
+func (c *Client) ProxyTCP(port int) (io.ReadWriteCloser, error) {
+	if c.RemoteName() == "" {
+		return nil, errors.New("ProxyTCP currently only supports gomote-created buildlets")
+	}
+	req, err := http.NewRequest("POST", c.URL()+"/tcpproxy", nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Add("X-Target-Port", fmt.Sprint(port))
+	res, err := c.do(req)
+	if err != nil {
+		return nil, err
+	}
+	if res.StatusCode != http.StatusSwitchingProtocols {
+		slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
+		res.Body.Close()
+		return nil, fmt.Errorf("wanted 101 Switching Protocols; unexpected response: %v, %q", res.Status, slurp)
+	}
+	rwc, ok := res.Body.(io.ReadWriteCloser)
+	if !ok {
+		res.Body.Close()
+		return nil, fmt.Errorf("tcpproxy response was not a Writer")
+	}
+	return rwc, nil
+}
+
 // ProxyRoundTripper returns a RoundTripper that sends HTTP requests directly
 // through to the underlying buildlet, adding auth and X-Buildlet-Proxy headers
 // as necessary. This is really only intended for use by the coordinator.
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index 7114eb0..87ec0ca 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -34,6 +34,7 @@
     put14      put Go 1.4 in place
     puttar     extract a tar.gz to a buildlet
     rm         delete files or directories
+    rdp        RDP (Remote Desktop Protocol) to a Windows buildlet
     run        run a command on a buildlet
     ssh        ssh to a buildlet
 
@@ -149,6 +150,7 @@
 	registerCommand("put", "put files on a buildlet", put)
 	registerCommand("put14", "put Go 1.4 in place", put14)
 	registerCommand("puttar", "extract a tar.gz to a buildlet", putTar)
+	registerCommand("rdp", "RDP (Remote Desktop Protocol) to a Windows buildlet", rdp)
 	registerCommand("rm", "delete files or directories", rm)
 	registerCommand("run", "run a command on a buildlet", run)
 	registerCommand("ssh", "ssh to a buildlet", ssh)
diff --git a/cmd/gomote/rdp.go b/cmd/gomote/rdp.go
new file mode 100644
index 0000000..4ecae62
--- /dev/null
+++ b/cmd/gomote/rdp.go
@@ -0,0 +1,88 @@
+// Copyright 2019 The Go 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 main
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+
+	"golang.org/x/build/buildlet"
+	"golang.org/x/sync/errgroup"
+)
+
+const rdpPort = 3389
+
+func rdp(args []string) error {
+	fs := flag.NewFlagSet("rdp", flag.ContinueOnError)
+	fs.Usage = func() {
+		fmt.Fprintln(os.Stderr, "rdp usage: gomote rdp [--listen=...] <instance>")
+		fs.PrintDefaults()
+		os.Exit(1)
+	}
+	var listen string
+	fs.StringVar(&listen, "listen", "localhost:"+fmt.Sprint(rdpPort), "local address to listen on")
+	fs.Parse(args)
+	if fs.NArg() != 1 {
+		fs.Usage()
+	}
+	name := fs.Arg(0)
+	bc, err := remoteClient(name)
+	if err != nil {
+		return err
+	}
+
+	ln, err := net.Listen("tcp", listen)
+	if err != nil {
+		return err
+	}
+	log.Printf("Listening on %v to proxy RDP.", ln.Addr())
+	for {
+		c, err := ln.Accept()
+		if err != nil {
+			return err
+		}
+		go handleRDPConn(bc, c)
+	}
+}
+
+func handleRDPConn(bc *buildlet.Client, c net.Conn) {
+	const Lmsgprefix = 64 // new in Go 1.14, harmless before
+	log := log.New(os.Stderr, c.RemoteAddr().String()+": ", log.LstdFlags|Lmsgprefix)
+	log.Printf("accepted connection, dialing buildlet via coordinator proxy...")
+	rwc, err := bc.ProxyTCP(rdpPort)
+	if err != nil {
+		c.Close()
+		log.Printf("failed to connect to buildlet via coordinator: %v", err)
+		return
+	}
+
+	log.Printf("connected to buildlet; proxying data.")
+
+	grp, ctx := errgroup.WithContext(context.Background())
+	grp.Go(func() error {
+		_, err := io.Copy(rwc, c)
+		if err == nil {
+			return errors.New("local client closed")
+		}
+		return fmt.Errorf("error copying from local to remote: %v", err)
+	})
+	grp.Go(func() error {
+		_, err := io.Copy(c, rwc)
+		if err == nil {
+			return errors.New("remote server closed")
+		}
+		return fmt.Errorf("error copying from remote to local: %v", err)
+	})
+	<-ctx.Done()
+	rwc.Close()
+	c.Close()
+	log.Printf("closing RDP connection: %v", grp.Wait())
+}