diff --git a/cmd/ussher/main.go b/cmd/ussher/main.go index 50e8d8d..7d3f203 100644 --- a/cmd/ussher/main.go +++ b/cmd/ussher/main.go @@ -1,7 +1,40 @@ package main -import "fmt" +import ( + "fmt" + "log" + "net/http" + + "github.com/barakmich/ussher" +) func main() { - fmt.Println("vim-go") + config := &ussher.Config{ + Port: 2222, + HTTPPort: 8080, + BindAddress: "0.0.0.0", + HostKeyPath: "id_ed25519", + Keystore: ussher.AcceptAllKeys(), + HTTPMux: http.NewServeMux(), + SSHApps: make(map[string]ussher.SSHApp), + } + err := ussher.RegisterEchoApp(config) + if err != nil { + fmt.Println(err) + } + go runHTTPServer(config) + err = ussher.RunSSHServer(config) + if err != nil { + fmt.Printf("%#v", err) + } +} + +func runHTTPServer(config *ussher.Config) { + httpAddr := fmt.Sprintf("%s:%d", config.BindAddress, config.HTTPPort) + log.Printf("Listening HTTP: %s\n", httpAddr) + srv := &http.Server{ + Addr: httpAddr, + Handler: config.HTTPMux, + } + log.Fatal(srv.ListenAndServe()) } diff --git a/config.go b/config.go index 41a1bae..288c87d 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package ussher import ( "io/ioutil" + "net/http" "golang.org/x/crypto/ssh" ) @@ -9,6 +10,11 @@ import ( type Config struct { HostKeyPath string Keystore Keystore + BindAddress string + Port int + HTTPPort int + SSHApps map[string]SSHApp + HTTPMux *http.ServeMux } func (c *Config) GetPrivateKey() (ssh.Signer, error) { @@ -18,7 +24,3 @@ func (c *Config) GetPrivateKey() (ssh.Signer, error) { } return ssh.ParsePrivateKey(bytes) } - -type Keystore interface { - CheckPublicKey(user string, key ssh.PublicKey) (*ssh.Permissions, error) -} diff --git a/echo_app.go b/echo_app.go new file mode 100644 index 0000000..750300b --- /dev/null +++ b/echo_app.go @@ -0,0 +1,30 @@ +package ussher + +import ( + "fmt" + "io" + "net/http" + + "golang.org/x/crypto/ssh" +) + +type echoHandler struct { + count int +} + +func (e *echoHandler) HandleExec(conn *ssh.ServerConn, channel ssh.Channel) { + e.count += 1 + io.Copy(channel, channel) +} + +func (e *echoHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + msg := fmt.Sprintf("Hello Echo World! Calls: %d", e.count) + w.Write([]byte(msg)) +} + +func RegisterEchoApp(conf *Config) error { + e := &echoHandler{} + conf.SSHApps["echo"] = e + conf.HTTPMux.Handle("/echo/", e) + return nil +} diff --git a/keystore.go b/keystore.go new file mode 100644 index 0000000..96ad22e --- /dev/null +++ b/keystore.go @@ -0,0 +1,17 @@ +package ussher + +import "golang.org/x/crypto/ssh" + +type Keystore interface { + CheckPublicKey(user string, key ssh.PublicKey) (*ssh.Permissions, error) +} + +func AcceptAllKeys() *allKeyAcceptor { + return &allKeyAcceptor{} +} + +type allKeyAcceptor struct{} + +func (all *allKeyAcceptor) CheckPublicKey(user string, key ssh.PublicKey) (*ssh.Permissions, error) { + return nil, nil +} diff --git a/ssh_app.go b/ssh_app.go new file mode 100644 index 0000000..4f77cf8 --- /dev/null +++ b/ssh_app.go @@ -0,0 +1,7 @@ +package ussher + +import "golang.org/x/crypto/ssh" + +type SSHApp interface { + HandleExec(conn *ssh.ServerConn, channel ssh.Channel) +} diff --git a/ssh_server.go b/ssh_server.go index b8b3040..498e304 100644 --- a/ssh_server.go +++ b/ssh_server.go @@ -1,6 +1,13 @@ package ussher -import "golang.org/x/crypto/ssh" +import ( + "fmt" + "log" + "net" + "time" + + "golang.org/x/crypto/ssh" +) func RunSSHServer(config *Config) error { sshConfig := &ssh.ServerConfig{ @@ -14,4 +21,91 @@ func RunSSHServer(config *Config) error { return err } sshConfig.AddHostKey(private) + + // Once a ServerConfig has been configured, connections can be accepted. + listenAddr := fmt.Sprintf("%s:%d", config.BindAddress, config.Port) + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + log.Fatalf("Failed to listen on %s (%s)", listenAddr, err) + } + + // Accept all connections + log.Printf("Listening on %s...", listenAddr) + for { + tcpConn, err := listener.Accept() + if err != nil { + log.Printf("Failed to accept incoming connection (%s)", err) + continue + } + // Before use, a handshake must be performed on the incoming net.Conn. + sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, sshConfig) + if err != nil { + log.Printf("Failed to handshake (%s)", err) + continue + } + + log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) + // Discard all global out-of-band Requests + go ssh.DiscardRequests(reqs) + // Accept all channels + go handleChannels(chans, sshConn, config) + } +} + +func handleChannels(chans <-chan ssh.NewChannel, conn *ssh.ServerConn, config *Config) { + for newChannel := range chans { + go handleChannel(newChannel, conn, config) + } +} + +func handleChannel(newChannel ssh.NewChannel, conn *ssh.ServerConn, config *Config) { + // We're emulating a shell command; so this is a "session" + if t := newChannel.ChannelType(); t != "session" { + newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + return + } + + connection, requests, err := newChannel.Accept() + if err != nil { + log.Printf("Could not accept channel (%s)", err) + return + } + go func() { + var exit bool + for { + if exit { + break + } + select { + case req := <-requests: + switch req.Type { + case "shell": + log.Printf("Requested shell; none can be provided") + connection.Close() + exit = true + case "exec": + appname := string(req.Payload[4:]) // skip the first four bytes, which are length of string + for k, v := range config.SSHApps { + fmt.Println(k, " ", []byte(k)) + fmt.Printf("%#v\n", v) + } + fmt.Println(appname, req.Payload) + v, ok := config.SSHApps[appname] + if ok { + v.HandleExec(conn, connection) + } else { + log.Printf("Can't find app %s", appname) + } + connection.Close() + exit = true + default: + // Ignore things like 'pty' and 'env' variables. + continue + } + case <-time.After(5 * time.Second): + fmt.Println("connection timeout") + connection.Close() + } + } + }() }