sketch: initial container ssh support (#15)

Adds an in-process ssh server to the sketch agent running inside
the container.

The ssh server implementation uses https://github.com/gliderlabs/ssh/

This change does not automatically generate any keys (this may come later).
You specify the server identity private key and the user's authorized public
keys on the sketch command line.

The host sketch process reads these files from the cli flags at startup. Once
the container is launched, it passes these keys to to the container
sketch process via new /init POST body fields.
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 6043c23..6ef95ee 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -20,6 +20,7 @@
 	"strings"
 	"time"
 
+	"sketch.dev/loop/server"
 	"sketch.dev/loop/webui"
 	"sketch.dev/skribe"
 )
@@ -68,6 +69,15 @@
 	// Sketch client public key.
 	SketchPubKey string
 
+	// Host port for the container's ssh server
+	SSHPort int
+
+	// Public keys authorized to connect to the container's ssh server
+	SSHAuthorizedKeys []byte
+
+	// Private key used to identify the container's ssh server
+	SSHServerIdentity []byte
+
 	// Host information to pass to the container
 	HostHostname   string
 	HostOS         string
@@ -251,7 +261,7 @@
 		// the scrollback (which is not good, but also not fatal).  I can't see why it does this
 		// though, since none of the calls in postContainerInitConfig obviously write to stdout
 		// or stderr.
-		if err := postContainerInitConfig(ctx, localAddr, commit, gitSrv.gitPort, gitSrv.pass); err != nil {
+		if err := postContainerInitConfig(ctx, localAddr, commit, gitSrv.gitPort, gitSrv.pass, config.SSHServerIdentity, config.SSHAuthorizedKeys); err != nil {
 			slog.ErrorContext(ctx, "LaunchContainer.postContainerInitConfig", slog.String("err", err.Error()))
 			errCh <- appendInternalErr(err)
 		}
@@ -368,6 +378,9 @@
 	if config.SketchPubKey != "" {
 		cmdArgs = append(cmdArgs, "-e", "SKETCH_PUB_KEY="+config.SketchPubKey)
 	}
+	if config.SSHPort != 0 {
+		cmdArgs = append(cmdArgs, "-p", fmt.Sprintf("%d:2022", config.SSHPort)) // forward container ssh port to host ssh port
+	}
 	if relPath != "." {
 		cmdArgs = append(cmdArgs, "-w", "/app/"+relPath)
 	}
@@ -471,13 +484,17 @@
 }
 
 // Contact the container and configure it.
-func postContainerInitConfig(ctx context.Context, localAddr, commit, gitPort, gitPass string) error {
+func postContainerInitConfig(ctx context.Context, localAddr, commit, gitPort, gitPass string, sshServerIdentity, sshAuthorizedKeys []byte) error {
 	localURL := "http://" + localAddr
-	initMsg, err := json.Marshal(map[string]string{
-		"commit":          commit,
-		"git_remote_addr": fmt.Sprintf("http://sketch:%s@host.docker.internal:%s/.git", gitPass, gitPort),
-		"host_addr":       localAddr,
-	})
+
+	initMsg, err := json.Marshal(
+		server.InitRequest{
+			Commit:            commit,
+			GitRemoteAddr:     fmt.Sprintf("http://sketch:%s@host.docker.internal:%s/.git", gitPass, gitPort),
+			HostAddr:          localAddr,
+			SSHAuthorizedKeys: sshAuthorizedKeys,
+			SSHServerIdentity: sshServerIdentity,
+		})
 	if err != nil {
 		return fmt.Errorf("init msg: %w", err)
 	}