diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9e32b41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# editorconfig.org +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..273fe10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +## Go +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so +# Folders +_obj +_test +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* +_testmain.go +*.exe +*.test +*.prof +# Output of the go coverage tool +*.out +# external packages folder +vendor/ + +## Builds +bin/* +build/* +release/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..430d42b --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0089ecf --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +PKG := resin +VERSION := $(shell git describe --abbrev=0 --tags --dirty) +EXECUTABLE := sshproxy + +all: bin/$(EXECUTABLE) + +bin/$(EXECUTABLE): + go build -o "$@" ./$(PKG) + +release: release/$(EXECUTABLE)-$(VERSION)_linux_arm5.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_linux_arm7.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_darwin_386.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_linux_386.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_darwin_amd64.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_freebsd_amd64.tar.bz2 \ + release/$(EXECUTABLE)-$(VERSION)_linux_amd64.tar.bz2 + +release-sign: release + for f in release/*.tar.bz2; do gpg --armor --detach-sign $$f; done + +clean: + rm -vrf bin/* build/* release/* + +# arm +build/linux_arm5/$(EXECUTABLE): + GOARM=5 GOARCH=arm GOOS=linux go build -o "$@" ./$(PKG) +build/linux_arm7/$(EXECUTABLE): + GOARM=7 GOARCH=arm GOOS=linux go build -o "$@" ./$(PKG) + +# 386 +build/darwin_386/$(EXECUTABLE): + GOARCH=386 GOOS=darwin go build -o "$@" ./$(PKG) +build/linux_386/$(EXECUTABLE): + GOARCH=386 GOOS=linux go build -o "$@" ./$(PKG) + +# amd64 +build/darwin_amd64/$(EXECUTABLE): + GOARCH=amd64 GOOS=darwin go build -o "$@" ./$(PKG) +build/freebsd_amd64/$(EXECUTABLE): + GOARCH=amd64 GOOS=freebsd go build -o "$@" ./$(PKG) +build/linux_amd64/$(EXECUTABLE): + GOARCH=amd64 GOOS=linux go build -o "$@" ./$(PKG) + +# compressed artifacts +release/$(EXECUTABLE)-$(VERSION)_%.tar.bz2: build/%/$(EXECUTABLE) + tar -jcvf "$@" -C "`dirname $<`" $(EXECUTABLE) + +.PHONY: clean release-sign diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a22ced --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# sshproxy + +[![GoDoc](https://godoc.org/github.com/resin-io/sshproxy?status.svg)](https://godoc.org/github.com/resin-io/sshproxy) +[![Go Report Card](https://goreportcard.com/badge/github.com/resin-io/sshproxy)](https://goreportcard.com/report/github.com/resin-io/sshproxy) + +sshproxy is a simple ssh server library exposing an even simpler API. +Authentication is handled by providing a ServerConfig, allowing full customisation. +After authentication control is handed to the specified shell executable, with a PTY +if requested by the connecting client. diff --git a/resin/README.md b/resin/README.md new file mode 100644 index 0000000..b9ce909 --- /dev/null +++ b/resin/README.md @@ -0,0 +1,60 @@ +# sshproxy/resin + +A "resin-ready" binary, requiring minimal configuration. + +Configuration is possible via commandline flags, environment variables +and config files. + +Config files should be named `sshproxy.` and exist in the sshproxy +work dir. The following config file formats are supported: + +* [YAML](http://yaml.org) (`sshproxy.yml`) +* [JSON](http://www.json.org) (`sshproxy.json`) +* [TOML](https://github.com/toml-lang/toml) (`sshproxy.toml`) +* [HCL](https://github.com/hashicorp/hcl) (`sshproxy.hcl`) +* [Java .properties](https://en.wikipedia.org/wiki/.properties) (`sshproxy.properties`) + +There are a total of 6 configuration options. With the exception of `dir` +they can all be set via commandline, environment or config file. + +| Name | Commandline | Environment | Config | +|----------|------------------|--------------------|-----------| +| API Host | `--apihost` `-H` | `RESIN_API_HOST` | `apihost` | +| API Port | `--apiport` `-P` | `RESIN_API_PORT` | `apiport` | +| API Key | `--apikey` `-K` | `SSHPROXY_API_KEY` | `apikey` | +| Dir | `--dir` `-d` | `SSHPROXY_DIR` | | +| Port | `--port` `-p` | `SSHPROXY_PORT` | `port` | +| Shell | `--shell` `-s` | `SSHPROXY_SHELL` | `shell` | + +``` +Usage of sshproxy: + -H, --apihost string Resin API Host (default "api.resin.io") + -K, --apikey string Resin API Key (required) + -P, --apiport string Resin API Port (default "443") + -d, --dir string Work dir, holds ssh keys and sshproxy config (default "/etc/sshproxy") + -p, --port int Port the ssh service will listen on (default 22) + -s, --shell string Path to shell to execute post-authentication (default "shell.sh") +``` + +## Example Usage + +``` +% go get github.com/resin-io/sshproxy/resin +% export SSHPROXY_DIR=$(mktemp -d /tmp/sshproxy.XXXXXXXX) +% echo -e '#!/usr/bin/env bash\nenv' > ${SSHPROXY_DIR}/shell.sh && chmod +x ${SSHPROXY_DIR}/shell.sh + SSHPROXY_PORT=2222 \ + SSHPROXY_API_KEY=... \ + go run ${GOPATH}/src/github.com/resin-io/sshproxy/resin/main.go +... +% ssh -o 'StrictHostKeyChecking=no' \ + -o 'UserKnownHostsFile=/dev/null' \ + resin@localhost -p2222 -- some command +Warning: Permanently added '[localhost]:2222' (RSA) to the list of known hosts. +SSH_USER=resin +PWD=... +LANG=en_GB.UTF-8 +SHLVL=1 +SSH_ORIGINAL_COMMAND=some command +LC_CTYPE=en_GB.UTF-8 +_=/usr/bin/env +``` diff --git a/resin/main.go b/resin/main.go new file mode 100644 index 0000000..37c35ab --- /dev/null +++ b/resin/main.go @@ -0,0 +1,130 @@ +/* +Copyright 2017 Resin.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// A "resin-ready" binary which handles authentication via resin api, +// requiring minimal configuration. +// +// See https://github.com/resin-io/sshproxy/tree/master/resin#readme +package main + +import ( + "crypto/subtle" + "errors" + "fmt" + "log" + "os" + "path" + "strings" + + "github.com/resin-io/pinejs-client-go" + "github.com/resin-io/sshproxy" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +func authHandler(baseURL, apiKey string) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) { + url := fmt.Sprintf("%s/%s", baseURL, "ewa") + client := pinejs.NewClient(url, apiKey) + + handler := func(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + users := make([]map[string]interface{}, 1) + users[0] = make(map[string]interface{}) + users[0]["pinejs"] = "user__has__public_key" + + filter := pinejs.QueryOption{ + Type: pinejs.Filter, + Content: []string{fmt.Sprintf("user/any(u:((tolower(u/username)) eq ('%s')))", + strings.ToLower(meta.User()))}, + Raw: true} + fields := pinejs.QueryOption{ + Type: pinejs.Select, + Content: []string{"user", "public_key"}, + } + if err := client.List(&users, filter, fields); err != nil { + return nil, err + } else if len(users) == 0 { + return nil, errors.New("Unauthorised") + } + + for _, user := range users { + k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(user["public_key"].(string))) + if err != nil { + return nil, err + } + if subtle.ConstantTimeCompare(k.Marshal(), key.Marshal()) == 1 { + return nil, nil + } + } + + return nil, errors.New("Unauthorised") + } + + return handler +} + +func init() { + pflag.CommandLine.StringP("apihost", "H", "api.resin.io", "Resin API Host") + pflag.CommandLine.StringP("apiport", "P", "443", "Resin API Port") + pflag.CommandLine.StringP("apikey", "K", "", "Resin API Key (required)") + pflag.CommandLine.StringP("dir", "d", "/etc/sshproxy", "Work dir, holds ssh keys and sshproxy config") + pflag.CommandLine.IntP("port", "p", 22, "Port the ssh service will listen on") + pflag.CommandLine.StringP("shell", "s", "shell.sh", "Path to shell to execute post-authentication") + + viper.BindPFlags(pflag.CommandLine) + viper.SetConfigName("sshproxy") + viper.SetEnvPrefix("SSHPROXY") + viper.BindEnv("apihost", "RESIN_API_HOST") + viper.BindEnv("apiport", "RESIN_API_PORT") + viper.BindEnv("apikey", "SSHPROXY_API_KEY") + viper.BindEnv("dir") + viper.BindEnv("port") + viper.BindEnv("shell") +} + +func main() { + log.SetFlags(0) + pflag.Parse() + viper.AddConfigPath(viper.GetString("dir")) + viper.AddConfigPath("/etc") + viper.ReadInConfig() + + // API Key is required + if !viper.IsSet("apikey") || viper.GetString("apikey") == "" { + fmt.Fprintln(os.Stderr, "Error: Resin API Key is required.") + pflag.Usage() + os.Exit(2) + } + + // `dir` path must be absolute + if viper.GetString("dir")[0] != '/' { + fmt.Fprintln(os.Stderr, "Error: dir must be absolute.") + os.Exit(2) + } + + // if shell is relative, prepend with dir + if viper.Get("shell").(string)[0] != '/' { + viper.Set("shell", path.Join(viper.GetString("dir"), viper.GetString("shell"))) + } + if _, err := os.Stat(viper.GetString("shell")); err != nil { + fmt.Fprintf(os.Stderr, "%s: No such file or directory\n", viper.Get("shell")) + os.Exit(2) + } + + apiURL := fmt.Sprintf("https://%s:%d", viper.GetString("apihost"), viper.GetInt("apiport")) + sshConfig := &ssh.ServerConfig{PublicKeyCallback: authHandler(apiURL, viper.GetString("apikey"))} + sshproxy.New(viper.GetString("dir"), viper.GetString("shell"), sshConfig).Listen(viper.GetString("port")) +} diff --git a/sshproxy.go b/sshproxy.go new file mode 100644 index 0000000..f15e710 --- /dev/null +++ b/sshproxy.go @@ -0,0 +1,234 @@ +/* +Copyright 2017 Resin.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sshproxy is a simple ssh server library exposing an even simpler API. +// Authentication is handled by providing a ServerConfig, allowing full customisation. +// After authentication control is handed to the specified shell executable, with a PTY +// if requested by the connecting client. +package sshproxy + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + + "github.com/kr/pty" + "golang.org/x/crypto/ssh" +) + +// Server holds server specific configuration data. +type Server struct { + keyDir string + config *ssh.ServerConfig + shell string +} + +// New takes a directory to generate/store server keys, a path to the shell +// and an ssh.ServerConfig. If no ServerConfig is provided, then +// ServerConfig.NoClientAuth is set to true. ed25519 and rsa server keys +// and loaded, and generated if they do not exist. Returns a new Server. +func New(keyDir, shell string, sshConfig *ssh.ServerConfig) *Server { + s := &Server{ + keyDir: keyDir, + config: sshConfig, + shell: shell, + } + if s.config == nil { + s.config = &ssh.ServerConfig{ + NoClientAuth: true, + } + } + for _, keyType := range []string{"ed25519", "rsa"} { + s.addHostKey(keyType) + } + + return s +} + +// Wraps ServerConfig.AddHostKey to create parent directories and keys if they do not already exist +func (s *Server) addHostKey(keyType string) { + keyPath := filepath.Join(s.keyDir, fmt.Sprintf("id_%s", keyType)) + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + // create keyPath parent directories + os.MkdirAll(filepath.Dir(keyPath), os.ModePerm) + // generate ssh server keys + log.Printf("Generating private key... (%s)", keyType) + err := exec.Command("ssh-keygen", "-f", keyPath, "-t", "rsa", "-N", "").Run() + if err != nil { + panic(fmt.Sprintf("Failed to generate private key: %s\n%v", keyPath, err)) + } + } + + log.Printf("Loading private key... (%s)", keyPath) + raw, err := ioutil.ReadFile(keyPath) + if err != nil { + panic(fmt.Sprintf("Failed to read private key: %s\n%v", keyPath, err)) + } + pkey, err := ssh.ParsePrivateKey(raw) + if err != nil { + panic(fmt.Sprintf("Failed to parse private key: %s\n%v", keyPath, err)) + } + + s.config.AddHostKey(pkey) +} + +// Listen for new ssh connections on the specified port. +func (s *Server) Listen(port string) { + hostPort := net.JoinHostPort("0.0.0.0", port) + listener, err := net.Listen("tcp", hostPort) + if err != nil { + panic(err) + } + log.Printf("Listening on ssh://%s\n", hostPort) + + for { + conn, err := listener.Accept() + if err != nil { + // TODO: handle failed connection + continue + } + log.Printf("New TCP connection from %s", conn.RemoteAddr()) + + go s.upgradeConnection(conn) + } +} + +// Attempts to perform SSH handshake on new TCP connections +func (s *Server) upgradeConnection(conn net.Conn) { + sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.config) + if err != nil { + log.Printf("Handshake with %s failed (%s)", conn.RemoteAddr(), err) + return + } + log.Printf("New SSH connection from %s (%s)", conn.RemoteAddr(), sshConn.ClientVersion()) + + defer func() { + conn.Close() + log.Printf("Closed connection to %s", conn.RemoteAddr()) + }() + go ssh.DiscardRequests(reqs) + s.handleChannels(chans, sshConn) +} + +// After successful handshake, handle new channels. Only the "session" type is supported. +func (s *Server) handleChannels(chans <-chan ssh.NewChannel, conn *ssh.ServerConn) { + for newChannel := range chans { + log.Printf("New SSH channel from %s", conn.RemoteAddr()) + if chanType := newChannel.ChannelType(); chanType != "session" { + newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Unsupported channel type: %s", chanType)) + continue + } + + channel, reqs, err := newChannel.Accept() + if err != nil { + log.Printf("Could not accept channel request (%s)", err) + continue + } + + defer func() { + channel.Close() + log.Printf("Closed SSH channel with %s", conn.RemoteAddr()) + }() + // Do not block handling requests so we can service new channels + go s.handleRequests(reqs, channel, conn) + } +} + +// Service requests on given channel +func (s *Server) handleRequests(reqs <-chan *ssh.Request, channel ssh.Channel, conn *ssh.ServerConn) { + env := make([]string, 0) + wantsPty := false + for req := range reqs { + log.Printf("New SSH request '%s' from %s", req.Type, conn.RemoteAddr()) + switch req.Type { + case "env": + // append client env to the command environment + keyLen := req.Payload[3] + valLen := req.Payload[keyLen+7] + key := string(req.Payload[4 : keyLen+4]) + val := string(req.Payload[keyLen+8 : keyLen+valLen+8]) + env = append(env, fmt.Sprintf("%s=%s", key, val)) + req.Reply(true, nil) + case "pty-req": + // client has requested a PTY + wantsPty = true + req.Reply(true, nil) + case "shell": + // client has not supplied a command, reject + req.Reply(false, nil) + case "exec": + // setup is done, parse client exec command + cmdLen := req.Payload[3] + command := string(req.Payload[4 : cmdLen+4]) + req.Reply(true, nil) + s.handleExec(conn, channel, command, env, wantsPty) + log.Printf("Closing SSH channel with %s", conn.RemoteAddr()) + channel.Close() + return + default: + log.Printf("Discarding request with unknown type '%s'", req.Type) + } + } +} + +// Hand off to shell, creating PTY if requested +func (s *Server) handleExec(conn *ssh.ServerConn, channel ssh.Channel, command string, env []string, wantsPty bool) { + log.Printf("Handling command '%s' from %s", command, conn.RemoteAddr()) + cmd := exec.Command(s.shell) + cmd.Env = append(cmd.Env, env...) + cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_USER=%s", conn.User())) + cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_ORIGINAL_COMMAND=%s", command)) + + if wantsPty { + p, err := pty.Start(cmd) + if err != nil { + panic(err) + } + go io.Copy(p, channel) + io.Copy(channel, p) + } else { + stdout, err := cmd.StdoutPipe() + if err != nil { + panic(err) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + panic(err) + } + + stdin, err := cmd.StdinPipe() + if err != nil { + panic(err) + } + + if err = cmd.Start(); err != nil { + panic(err) + } + + go io.Copy(stdin, channel) + go io.Copy(channel, stdout) + go io.Copy(channel.Stderr(), stderr) + + cmd.Wait() + } + channel.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) +}