all repos — flounder @ 7c768ba43d4a3800691801abef4c5ab509817f6f

A small site builder for the Gemini protocol

sftp.go (view raw)

  1// An example SFTP server implementation using the golang SSH package.
  2// Serves the whole filesystem visible to the user, and has a hard-coded username and password,
  3// so not for real use!
  4package main
  5
  6import (
  7	"flag"
  8	"fmt"
  9	"io"
 10	"io/ioutil"
 11	"log"
 12	"net"
 13	"os"
 14	"path"
 15	"path/filepath"
 16
 17	"github.com/pkg/sftp"
 18	"golang.org/x/crypto/ssh"
 19)
 20
 21type Connection struct {
 22	User string
 23}
 24
 25func (con *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
 26	// check user perms -- cant read others hidden files
 27	fullpath := path.Join(c.FilesDirectory, filepath.Clean(request.Filepath))
 28	f, err := os.Open(fullpath)
 29	if err != nil {
 30		return nil, err
 31	}
 32	return f, nil
 33}
 34
 35func (con *Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
 36	// check user perms -- cant write others files
 37	fullpath := path.Join(c.FilesDirectory, filepath.Clean(request.Filepath))
 38	f, err := os.Open(fullpath)
 39	if err != nil {
 40		return nil, err
 41	}
 42	return f, nil
 43}
 44
 45func (conn *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
 46	fullpath := path.Join(c.FilesDirectory, filepath.Clean(request.Filepath))
 47	switch request.Method {
 48	case "List":
 49		f, err := os.Open(fullpath)
 50		if err != nil {
 51			return nil, err
 52		}
 53		fileInfo, err := f.Readdir(-1)
 54		if err != nil {
 55			return nil, err
 56		}
 57		return listerat(fileInfo), nil
 58	case "Stat":
 59		stat, err := os.Stat(fullpath)
 60		if err != nil {
 61			return nil, err
 62		}
 63		return listerat([]os.FileInfo{stat}), nil
 64	}
 65	return nil, fmt.Errorf("Invalid command")
 66}
 67
 68func (c *Connection) Filecmd(request *sftp.Request) error {
 69	// remove, rename, setstat? find out
 70	return nil
 71}
 72
 73// TODO hide hidden folders
 74// Users have write persm on their files, read perms on all
 75
 76func buildHandlers(connection *Connection) sftp.Handlers {
 77	return sftp.Handlers{
 78		connection,
 79		connection,
 80		connection,
 81		connection,
 82	}
 83}
 84
 85// Based on example server code from golang.org/x/crypto/ssh and server_standalone
 86func runSFTPServer() {
 87
 88	var (
 89		readOnly    bool
 90		debugStderr bool
 91	)
 92
 93	flag.BoolVar(&readOnly, "R", false, "read-only server")
 94	flag.BoolVar(&debugStderr, "e", false, "debug to stderr")
 95	flag.Parse()
 96
 97	debugStream := ioutil.Discard
 98	if debugStderr {
 99		debugStream = os.Stderr
100	}
101
102	// An SSH server is represented by a ServerConfig, which holds
103	// certificate details and handles authentication of ServerConns.
104	config := &ssh.ServerConfig{
105		// PublicKeyCallback
106		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
107			// Should use constant-time compare (or better, salt+hash) in
108			// a production setting.
109			fmt.Fprintf(debugStream, "Login: %s\n", c.User())
110			if c.User() == "alex" && string(pass) == "alex" {
111				return nil, nil
112			}
113			return nil, fmt.Errorf("password rejected for %q", c.User())
114		},
115	}
116
117	privateBytes, err := ioutil.ReadFile("id_rsa")
118	if err != nil {
119		log.Fatal("Failed to load private key", err)
120	}
121
122	private, err := ssh.ParsePrivateKey(privateBytes)
123	if err != nil {
124		log.Fatal("Failed to parse private key", err)
125	}
126
127	config.AddHostKey(private)
128
129	// Once a ServerConfig has been configured, connections can be
130	// accepted.
131	listener, err := net.Listen("tcp", "0.0.0.0:2024")
132	if err != nil {
133		log.Fatal("failed to listen for connection", err)
134	}
135	fmt.Printf("Listening on %v\n", listener.Addr())
136
137	nConn, err := listener.Accept()
138	if err != nil {
139		log.Fatal("failed to accept incoming connection", err)
140	}
141
142	// Before use, a handshake must be performed on the incoming net.Conn.
143	sconn, chans, reqs, err := ssh.NewServerConn(nConn, config)
144	if err != nil {
145		log.Fatal("failed to handshake", err)
146	}
147	log.Println("login detected:", sconn.User())
148	fmt.Fprintf(debugStream, "SSH server established\n")
149
150	// The incoming Request channel must be serviced.
151	go ssh.DiscardRequests(reqs)
152
153	// Service the incoming Channel channel.
154	for newChannel := range chans {
155		// Channels have a type, depending on the application level
156		// protocol intended. In the case of an SFTP session, this is "subsystem"
157		// with a payload string of "<length=4>sftp"
158		fmt.Fprintf(debugStream, "Incoming channel: %s\n", newChannel.ChannelType())
159		if newChannel.ChannelType() != "session" {
160			newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
161			fmt.Fprintf(debugStream, "Unknown channel type: %s\n", newChannel.ChannelType())
162			continue
163		}
164		channel, requests, err := newChannel.Accept()
165		if err != nil {
166			log.Fatal("could not accept channel.", err)
167		}
168		fmt.Fprintf(debugStream, "Channel accepted\n")
169
170		// Sessions have out-of-band requests such as "shell",
171		// "pty-req" and "env".  Here we handle only the
172		// "subsystem" request.
173		go func(in <-chan *ssh.Request) {
174			for req := range in {
175				fmt.Fprintf(debugStream, "Request: %v\n", req.Type)
176				ok := false
177				switch req.Type {
178				case "subsystem":
179					fmt.Fprintf(debugStream, "Subsystem: %s\n", req.Payload[4:])
180					if string(req.Payload[4:]) == "sftp" {
181						ok = true
182					}
183				}
184				fmt.Fprintf(debugStream, " - accepted: %v\n", ok)
185				req.Reply(ok, nil)
186			}
187		}(requests)
188		connection := Connection{"alex"}
189		root := buildHandlers(&connection)
190		server := sftp.NewRequestServer(channel, root)
191		if err := server.Serve(); err == io.EOF {
192			server.Close()
193			log.Print("sftp client exited session.")
194		} else if err != nil {
195			log.Fatal("sftp server completed with error:", err)
196		}
197	}
198}
199
200type listerat []os.FileInfo
201
202// Modeled after strings.Reader's ReadAt() implementation
203func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
204	var n int
205	if offset >= int64(len(f)) {
206		return 0, io.EOF
207	}
208	n = copy(ls, f[offset:])
209	if n < len(ls) {
210		return n, io.EOF
211	}
212	return n, nil
213}