From 9287f2417594d7a956cbee5db1a5175427efdb20 Mon Sep 17 00:00:00 2001 From: Michael Houston Date: Sat, 7 Nov 2015 19:18:42 +0000 Subject: [PATCH 1/2] Created working example SFTP server Purely gluing together the standalone_server and the SSH server examples, since it took me a while to work out how they fit together. Thought it may save the next person a few minutes! --- .gitignore | 3 + examples/sftp-server/README.md | 12 +++ examples/sftp-server/main.go | 132 +++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 examples/sftp-server/README.md create mode 100644 examples/sftp-server/main.go diff --git a/.gitignore b/.gitignore index a864e484..9fc1e3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ server_standalone/server_standalone +examples/sftp-server/id_rsa +examples/sftp-server/id_rsa.pub +examples/sftp-server/sftp-server diff --git a/examples/sftp-server/README.md b/examples/sftp-server/README.md new file mode 100644 index 00000000..bd96f2d8 --- /dev/null +++ b/examples/sftp-server/README.md @@ -0,0 +1,12 @@ +Example SFTP server implementation +=== + +In order to use this example you will need an RSA key. + +On linux-like systems with openssh installed, you can use the command: + +``` +ssh-keygen -t rsa -f id_rsa +``` + +Then you will be able to run the sftp-server command in the current directory. diff --git a/examples/sftp-server/main.go b/examples/sftp-server/main.go new file mode 100644 index 00000000..c1e6df08 --- /dev/null +++ b/examples/sftp-server/main.go @@ -0,0 +1,132 @@ +// An example SFTP server implementation using the golang SSH package. +// Serves the whole filesystem visible to the user, and has a hard-coded username and password, +// so not for real use! +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "net" + "os" + + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +// Based on example server code from golang.org/x/crypto/ssh and server_standalone +func main() { + + readOnly := false + debugLevelStr := "none" + debugLevel := 0 + debugStderr := false + rootDir := "" + + flag.BoolVar(&readOnly, "R", false, "read-only server") + flag.BoolVar(&debugStderr, "e", false, "debug to stderr") + flag.StringVar(&debugLevelStr, "l", "none", "debug level") + flag.StringVar(&debugLevelStr, "root", "", "root directory") + flag.Parse() + + // term := terminal.NewTerminal(channel, "> ") + debugStream := ioutil.Discard + if debugStderr { + debugStream = os.Stderr + debugLevel = 1 + } + + // An SSH server is represented by a ServerConfig, which holds + // certificate details and handles authentication of ServerConns. + config := &ssh.ServerConfig{ + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + // Should use constant-time compare (or better, salt+hash) in + // a production setting. + fmt.Fprintf(debugStream, "Login: %s\n", c.User()) + if c.User() == "testuser" && string(pass) == "tiger" { + return nil, nil + } + return nil, fmt.Errorf("password rejected for %q", c.User()) + }, + } + + privateBytes, err := ioutil.ReadFile("id_rsa") + if err != nil { + panic("Failed to load private key") + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + panic("Failed to parse private key") + } + + config.AddHostKey(private) + + // Once a ServerConfig has been configured, connections can be + // accepted. + listener, err := net.Listen("tcp", "0.0.0.0:2022") + if err != nil { + panic("failed to listen for connection") + } + fmt.Printf("Listening on %v\n", listener.Addr()) + + nConn, err := listener.Accept() + if err != nil { + panic("failed to accept incoming connection") + } + + // Before use, a handshake must be performed on the incoming + // net.Conn. + _, chans, reqs, err := ssh.NewServerConn(nConn, config) + if err != nil { + panic("failed to handshake") + } + fmt.Fprintf(debugStream, "SSH server established\n") + + // The incoming Request channel must be serviced. + go ssh.DiscardRequests(reqs) + + // Service the incoming Channel channel. + for newChannel := range chans { + // Channels have a type, depending on the application level + // protocol intended. In the case of a shell, the type is + // "session" and ServerShell may be used to present a simple + // terminal interface. + fmt.Fprintf(debugStream, "Incoming channel: %s\n", newChannel.ChannelType()) + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + fmt.Fprintf(debugStream, "Unknown channel type: %s\n", newChannel.ChannelType()) + continue + } + channel, requests, err := newChannel.Accept() + if err != nil { + panic("could not accept channel.") + } + fmt.Fprintf(debugStream, "Channel accepted\n") + + // Sessions have out-of-band requests such as "shell", + // "pty-req" and "env". Here we handle only the + // "subsystem" request. + go func(in <-chan *ssh.Request) { + for req := range in { + fmt.Fprintf(debugStream, "Request: %v\n", req.Type) + ok := false + switch req.Type { + case "subsystem": + fmt.Fprintf(debugStream, "Subsystem: %s\n", req.Payload[4:]) + if string(req.Payload[4:]) == "sftp" { + ok = true + } + } + fmt.Fprintf(debugStream, " - accepted: %v\n", ok) + req.Reply(ok, nil) + } + }(requests) + + server, err := sftp.NewServer(channel, channel, debugStream, debugLevel, readOnly, rootDir) + if err != nil { + panic(err) + } + go server.Serve() + } +} From cf6c57c01f3103cf3ffe341e6d191e887fe7fef1 Mon Sep 17 00:00:00 2001 From: Mike Houston Date: Sun, 8 Nov 2015 12:36:10 +0000 Subject: [PATCH 2/2] Updates to code style Replaced panic with log.Fatal Removed old commented code line Cleaned up declaration of flag variables Fixed rootDir flag variable - was pointing to debugLevelStr Log any final error returned by SFTP subsystem --- examples/sftp-server/main.go | 39 +++++++++++++++++++----------------- server_standalone/main.go | 11 ++++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/examples/sftp-server/main.go b/examples/sftp-server/main.go index c1e6df08..3dc3393c 100644 --- a/examples/sftp-server/main.go +++ b/examples/sftp-server/main.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io/ioutil" + "log" "net" "os" @@ -17,19 +18,20 @@ import ( // Based on example server code from golang.org/x/crypto/ssh and server_standalone func main() { - readOnly := false - debugLevelStr := "none" - debugLevel := 0 - debugStderr := false - rootDir := "" + var ( + readOnly bool + debugLevelStr string + debugLevel int + debugStderr bool + rootDir string + ) flag.BoolVar(&readOnly, "R", false, "read-only server") flag.BoolVar(&debugStderr, "e", false, "debug to stderr") flag.StringVar(&debugLevelStr, "l", "none", "debug level") - flag.StringVar(&debugLevelStr, "root", "", "root directory") + flag.StringVar(&rootDir, "root", "", "root directory") flag.Parse() - // term := terminal.NewTerminal(channel, "> ") debugStream := ioutil.Discard if debugStderr { debugStream = os.Stderr @@ -52,12 +54,12 @@ func main() { privateBytes, err := ioutil.ReadFile("id_rsa") if err != nil { - panic("Failed to load private key") + log.Fatal("Failed to load private key", err) } private, err := ssh.ParsePrivateKey(privateBytes) if err != nil { - panic("Failed to parse private key") + log.Fatal("Failed to parse private key", err) } config.AddHostKey(private) @@ -66,20 +68,20 @@ func main() { // accepted. listener, err := net.Listen("tcp", "0.0.0.0:2022") if err != nil { - panic("failed to listen for connection") + log.Fatal("failed to listen for connection", err) } fmt.Printf("Listening on %v\n", listener.Addr()) nConn, err := listener.Accept() if err != nil { - panic("failed to accept incoming connection") + log.Fatal("failed to accept incoming connection", err) } // Before use, a handshake must be performed on the incoming // net.Conn. _, chans, reqs, err := ssh.NewServerConn(nConn, config) if err != nil { - panic("failed to handshake") + log.Fatal("failed to handshake", err) } fmt.Fprintf(debugStream, "SSH server established\n") @@ -89,9 +91,8 @@ func main() { // Service the incoming Channel channel. for newChannel := range chans { // Channels have a type, depending on the application level - // protocol intended. In the case of a shell, the type is - // "session" and ServerShell may be used to present a simple - // terminal interface. + // protocol intended. In the case of an SFTP session, this is "subsystem" + // with a payload string of "sftp" fmt.Fprintf(debugStream, "Incoming channel: %s\n", newChannel.ChannelType()) if newChannel.ChannelType() != "session" { newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") @@ -100,7 +101,7 @@ func main() { } channel, requests, err := newChannel.Accept() if err != nil { - panic("could not accept channel.") + log.Fatal("could not accept channel.", err) } fmt.Fprintf(debugStream, "Channel accepted\n") @@ -125,8 +126,10 @@ func main() { server, err := sftp.NewServer(channel, channel, debugStream, debugLevel, readOnly, rootDir) if err != nil { - panic(err) + log.Fatal(err) + } + if err := server.Serve(); err != nil { + log.Fatal("sftp server completed with error:", err) } - go server.Serve() } } diff --git a/server_standalone/main.go b/server_standalone/main.go index 0b510a5c..77b8ec6a 100644 --- a/server_standalone/main.go +++ b/server_standalone/main.go @@ -13,10 +13,13 @@ import ( ) func main() { - readOnly := false - debugLevelStr := "none" - debugLevel := 0 - debugStderr := false + var ( + readOnly bool + debugLevelStr string + debugLevel int + debugStderr bool + ) + flag.BoolVar(&readOnly, "R", false, "read-only server") flag.BoolVar(&debugStderr, "e", false, "debug to stderr") flag.StringVar(&debugLevelStr, "l", "none", "debug level")