Skip to content

Instantly share code, notes, and snippets.

@artyom
Created February 25, 2017 17:26
Show Gist options
  • Select an option

  • Save artyom/1a7d46c9511dd93b0714baf7b9745605 to your computer and use it in GitHub Desktop.

Select an option

Save artyom/1a7d46c9511dd93b0714baf7b9745605 to your computer and use it in GitHub Desktop.
Example ssh server with both interactive terminal & sftp support
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"github.com/artyom/autoflags"
"github.com/pkg/sftp"
)
func main() {
args := struct {
Addr string `flag:"addr,address to listen"`
Auth string `flag:"auth,path to authorized_keys file"`
PK string `flag:"hostkey,path to private host key"`
}{
Addr: "localhost:2022",
Auth: "authorized_keys",
PK: "id_rsa",
}
autoflags.Define(&args)
flag.Parse()
if err := run(args.Addr, args.Auth, args.PK); err != nil {
log.Fatal(err)
}
}
func run(addr, keysFile, pkFile string) error {
config := &ssh.ServerConfig{}
if err := addHostKey(config, pkFile); err != nil {
return err
}
pkeyAuthFunc, err := authChecker(keysFile)
if err != nil {
return err
}
config.PublicKeyCallback = pkeyAuthFunc
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
return err
}
go func(conn net.Conn) {
if err := serveConn(conn, config); err != nil {
log.Println(err)
}
}(conn)
}
}
func serveConn(conn net.Conn, config *ssh.ServerConfig) error {
defer log.Println("serveConn finished")
defer conn.Close()
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
return err
}
defer sconn.Close()
_ = sconn // TODO: check Permissions
go ssh.DiscardRequests(reqs)
for newChannel := range chans {
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
return err
}
go func(sshCh ssh.Channel, in <-chan *ssh.Request) {
defer log.Println("channel/requets handler finished")
for req := range in {
var ok bool
switch {
case req.Type == "pty-req":
req.Reply(true, nil)
continue
case req.Type == "shell":
ok = true
go func() {
// defer sconn.Close() // XXX(?)
defer sshCh.Close() // SSH_MSG_CHANNEL_CLOSE
defer sshCh.CloseWrite() // SSH_MSG_CHANNEL_EOF
defer sshCh.SendRequest("[email protected]", false, nil)
switch err := serveTerminal(sshCh); err {
case nil:
sshCh.SendRequest("exit-status", false, ssh.Marshal(&exitStatusMsg{0}))
default:
sshCh.SendRequest("exit-status", false, ssh.Marshal(&exitStatusMsg{1}))
}
}()
case req.Type == "subsystem" && string(req.Payload[4:]) == "sftp":
ok = true
go func() {
defer sshCh.Close() // SSH_MSG_CHANNEL_CLOSE
sftpServer, err := sftp.NewServer(sshCh, sftp.ReadOnly())
if err != nil {
return
}
_ = sftpServer.Serve()
}()
}
req.Reply(ok, nil)
if ok {
break
}
}
for req := range in {
req.Reply(false, nil)
}
}(channel, requests)
}
return nil
}
func serveTerminal(rw io.ReadWriter) error {
log.Println("serveTerminal started")
defer log.Println("serveTerminal finished")
term := terminal.NewTerminal(rw, "> ")
for {
line, err := term.ReadLine()
switch err {
case nil:
case io.EOF:
return nil
default:
return err
}
log.Println("line read:", line)
if _, err := fmt.Fprintf(term, "You said: %q\n", line); err != nil {
return err
}
}
}
func authChecker(name string) (func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error), error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
type keyMeta struct {
key ssh.PublicKey
opts map[string]string
}
var pkeys []keyMeta
sc := bufio.NewScanner(f)
for sc.Scan() {
pk, _, opts, _, err := ssh.ParseAuthorizedKey(sc.Bytes())
if err != nil {
return nil, err
}
pkeys = append(pkeys, keyMeta{key: pk, opts: splitOpts(opts)})
}
if err := sc.Err(); err != nil {
return nil, err
}
return func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
keyBytes := key.Marshal()
for _, k := range pkeys {
if bytes.Equal(keyBytes, k.key.Marshal()) {
return &ssh.Permissions{
Extensions: k.opts,
}, nil
}
}
return nil, fmt.Errorf("no keys matched")
}, nil
}
func splitOpts(opts []string) map[string]string {
if len(opts) == 0 {
return nil
}
m := make(map[string]string, len(opts))
for _, s := range opts {
ss := strings.SplitN(s, "=", 2)
switch len(ss) {
case 1:
m[s] = ""
case 2:
m[ss[0]] = ss[1]
}
}
return m
}
func addHostKey(config *ssh.ServerConfig, keyFile string) error {
privateBytes, err := ioutil.ReadFile(keyFile)
if err != nil {
return err
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return err
}
config.AddHostKey(private)
return nil
}
type exitStatusMsg struct {
Status uint32
}
@indiscrete-void
Copy link
Copy Markdown

Thank You!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment