├── LICENSE ├── README.md ├── const.go ├── debug.go ├── doc.go ├── emptyfs.go ├── example ├── dir_fs.go ├── main.go ├── password.go ├── runserver_highlevel.go ├── runserver_lowlevel.go └── synthetic_fs.go ├── example_test.go ├── file.go ├── fuzz └── fuzz.go ├── handle.go ├── listen.go ├── nodebug.go ├── readdir.go ├── server.go └── server_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Taru Karttunen 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sftpd - SFTP server library in Go 2 | 3 | # License: MIT - Docs [![GoDoc](https://godoc.org/github.com/taruti/sftpd?status.png)](http://godoc.org/github.com/taruti/sftpd) 4 | 5 | # Recent changes - 2019 6 | + `Attr.FillFrom` cannot fail and now does not return an error value. Previously it was always nil. 7 | 8 | # FAQ 9 | 10 | ## ssh: no common algorithms 11 | 12 | The client and the server cannot agree on algorithms. Typically this 13 | is caused by an ECDSA host key. If using sshutil add the 14 | ``sshutil.RSA2048`` flag. 15 | 16 | ## Enabling debugging output 17 | 18 | ``` 19 | go build -tags debug -a 20 | ``` 21 | 22 | Will enable debugging output using package `log`. 23 | 24 | # TODO 25 | + Renames 26 | + Symlink creation 27 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | const ( 4 | ssh_FXP_INIT = 1 5 | ssh_FXP_VERSION = 2 6 | ssh_FXP_OPEN = 3 7 | ssh_FXP_CLOSE = 4 8 | ssh_FXP_READ = 5 9 | ssh_FXP_WRITE = 6 10 | ssh_FXP_LSTAT = 7 11 | ssh_FXP_FSTAT = 8 12 | ssh_FXP_SETSTAT = 9 13 | ssh_FXP_FSETSTAT = 10 14 | ssh_FXP_OPENDIR = 11 15 | ssh_FXP_READDIR = 12 16 | ssh_FXP_REMOVE = 13 17 | ssh_FXP_MKDIR = 14 18 | ssh_FXP_RMDIR = 15 19 | ssh_FXP_REALPATH = 16 20 | ssh_FXP_STAT = 17 21 | ssh_FXP_RENAME = 18 22 | ssh_FXP_READLINK = 19 23 | ssh_FXP_SYMLINK = 20 24 | ssh_FXP_STATUS = 101 25 | ssh_FXP_HANDLE = 102 26 | ssh_FXP_DATA = 103 27 | ssh_FXP_NAME = 104 28 | ssh_FXP_ATTRS = 105 29 | ssh_FXP_EXTENDED = 200 30 | ssh_FXP_EXTENDED_REPLY = 201 31 | ) 32 | 33 | const ( 34 | ssh_FX_OK = 0 35 | ssh_FX_EOF = 1 36 | ssh_FX_NO_SUCH_FILE = 2 37 | ssh_FX_PERMISSION_DENIED = 3 38 | ssh_FX_FAILURE = 4 39 | ssh_FX_BAD_MESSAGE = 5 40 | ssh_FX_NO_CONNECTION = 6 41 | ssh_FX_CONNECTION_LOST = 7 42 | ssh_FX_OP_UNSUPPORTED = 8 43 | ) 44 | 45 | const ( 46 | ssh_FILEXFER_ATTR_SIZE = 0x00000001 47 | ssh_FILEXFER_ATTR_UIDGID = 0x00000002 48 | ssh_FILEXFER_ATTR_PERMISSIONS = 0x00000004 49 | ssh_FILEXFER_ATTR_ACMODTIME = 0x00000008 50 | ssh_FILEXFER_ATTR_EXTENDED = 0x80000000 51 | ) 52 | 53 | // These are used to get more pretty debugging output. 54 | type ssh_fxp byte 55 | type ssh_fx byte 56 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package sftpd 4 | 5 | import ( 6 | "log" 7 | "strconv" 8 | ) 9 | 10 | var debug func(...interface{}) = log.Println 11 | var debugf func(string, ...interface{}) = log.Printf 12 | 13 | func (b ssh_fxp) String() string { 14 | s := ssh_fxp_map[b] 15 | if s == "" { 16 | s = "INVALID:" + strconv.Itoa(int(b)) 17 | } 18 | return s 19 | } 20 | 21 | var ssh_fxp_map = map[ssh_fxp]string{ 22 | ssh_FXP_INIT: `ssh_FXP_INIT`, 23 | ssh_FXP_VERSION: `ssh_FXP_VERSION`, 24 | ssh_FXP_OPEN: `ssh_FXP_OPEN`, 25 | ssh_FXP_CLOSE: `ssh_FXP_CLOSE`, 26 | ssh_FXP_READ: `ssh_FXP_READ`, 27 | ssh_FXP_WRITE: `ssh_FXP_WRITE`, 28 | ssh_FXP_LSTAT: `ssh_FXP_LSTAT`, 29 | ssh_FXP_FSTAT: `ssh_FXP_FSTAT`, 30 | ssh_FXP_SETSTAT: `ssh_FXP_SETSTAT`, 31 | ssh_FXP_FSETSTAT: `ssh_FXP_FSETSTAT`, 32 | ssh_FXP_OPENDIR: `ssh_FXP_OPENDIR`, 33 | ssh_FXP_READDIR: `ssh_FXP_READDIR`, 34 | ssh_FXP_REMOVE: `ssh_FXP_REMOVE`, 35 | ssh_FXP_MKDIR: `ssh_FXP_MKDIR`, 36 | ssh_FXP_RMDIR: `ssh_FXP_RMDIR`, 37 | ssh_FXP_REALPATH: `ssh_FXP_REALPATH`, 38 | ssh_FXP_STAT: `ssh_FXP_STAT`, 39 | ssh_FXP_RENAME: `ssh_FXP_RENAME`, 40 | ssh_FXP_READLINK: `ssh_FXP_READLINK`, 41 | ssh_FXP_SYMLINK: `ssh_FXP_SYMLINK`, 42 | ssh_FXP_STATUS: `ssh_FXP_STATUS`, 43 | ssh_FXP_HANDLE: `ssh_FXP_HANDLE`, 44 | ssh_FXP_DATA: `ssh_FXP_DATA`, 45 | ssh_FXP_NAME: `ssh_FXP_NAME`, 46 | ssh_FXP_ATTRS: `ssh_FXP_ATTRS`, 47 | ssh_FXP_EXTENDED: `ssh_FXP_EXTENDED`, 48 | ssh_FXP_EXTENDED_REPLY: `ssh_FXP_EXTENDED_REPLY`, 49 | } 50 | 51 | func (b ssh_fx) String() string { 52 | s := ssh_fx_map[b] 53 | if s == "" { 54 | s = "INVALID" 55 | } 56 | return s 57 | } 58 | 59 | var ssh_fx_map = map[ssh_fx]string{ 60 | ssh_FX_OK: `ssh_FX_OK`, 61 | ssh_FX_EOF: `ssh_FX_EOF`, 62 | ssh_FX_NO_SUCH_FILE: `ssh_FX_NO_SUCH_FILE`, 63 | ssh_FX_PERMISSION_DENIED: `ssh_FX_PERMISSION_DENIED`, 64 | ssh_FX_FAILURE: `ssh_FX_FAILURE`, 65 | ssh_FX_BAD_MESSAGE: `ssh_FX_BAD_MESSAGE`, 66 | ssh_FX_NO_CONNECTION: `ssh_FX_NO_CONNECTION`, 67 | ssh_FX_CONNECTION_LOST: `ssh_FX_CONNECTION_LOST`, 68 | ssh_FX_OP_UNSUPPORTED: `ssh_FX_OP_UNSUPPORTED`, 69 | } 70 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package sftpd is sftp (SSH File Transfer Protocol) server library. 2 | package sftpd 3 | -------------------------------------------------------------------------------- /emptyfs.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "errors" 5 | "path" 6 | ) 7 | 8 | var Failure = errors.New("Failure") 9 | 10 | type EmptyFile struct{} 11 | 12 | func (EmptyFile) Close() error { return nil } 13 | func (EmptyFile) ReadAt([]byte, int64) (int, error) { return 0, Failure } 14 | func (EmptyFile) WriteAt([]byte, int64) (int, error) { return 0, Failure } 15 | func (EmptyFile) FStat() (*Attr, error) { return nil, Failure } 16 | func (EmptyFile) FSetStat(*Attr) error { return Failure } 17 | 18 | type EmptyFS struct{} 19 | 20 | func (EmptyFS) OpenFile(string, uint32, *Attr) (File, error) { return nil, Failure } 21 | func (EmptyFS) OpenDir(string) (Dir, error) { return nil, Failure } 22 | func (EmptyFS) Remove(string) error { return Failure } 23 | func (EmptyFS) Rename(string, string, uint32) error { return Failure } 24 | func (EmptyFS) Mkdir(string, *Attr) error { return Failure } 25 | func (EmptyFS) Rmdir(string) error { return Failure } 26 | func (EmptyFS) Stat(string, bool) (*Attr, error) { return nil, Failure } 27 | func (EmptyFS) SetStat(string, *Attr) error { return Failure } 28 | func (EmptyFS) ReadLink(p string) (string, error) { return "", Failure } 29 | func (EmptyFS) CreateLink(p string, t string, f uint32) error { return Failure } 30 | func (EmptyFS) RealPath(p string) (string, error) { return simpleRealPath(p), nil } 31 | 32 | func simpleRealPath(fp string) string { 33 | switch fp { 34 | case "", ".": 35 | fp = "/" 36 | default: 37 | fp = path.Clean(fp) 38 | } 39 | return fp 40 | } 41 | -------------------------------------------------------------------------------- /example/dir_fs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Read only file system using the file system 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "strings" 9 | 10 | "github.com/taruti/sftpd" 11 | ) 12 | 13 | type readOnlyDirFs struct { 14 | sftpd.EmptyFS 15 | } 16 | 17 | type rdir struct { 18 | d *os.File 19 | } 20 | 21 | type rfile struct { 22 | sftpd.EmptyFile 23 | f *os.File 24 | } 25 | 26 | func (rf rfile) Close() error { return rf.f.Close() } 27 | func (rf rfile) ReadAt(bs []byte, pos int64) (int, error) { return rf.f.ReadAt(bs, pos) } 28 | 29 | func (d rdir) Readdir(count int) ([]sftpd.NamedAttr, error) { 30 | fis, e := d.d.Readdir(count) 31 | if e != nil { 32 | return nil, e 33 | } 34 | rs := make([]sftpd.NamedAttr, len(fis)) 35 | for i, fi := range fis { 36 | rs[i].Name = fi.Name() 37 | rs[i].FillFrom(fi) 38 | } 39 | return rs, nil 40 | } 41 | func (d rdir) Close() error { 42 | return d.d.Close() 43 | } 44 | 45 | // Warning: 46 | // Use your own path mangling functionality in production code. 47 | // This can be quite non-trivial depending on the operating system. 48 | // The code below is not sufficient for production servers. 49 | func rfsMangle(path string) (string, error) { 50 | if strings.Contains(path, "..") { 51 | return "", errors.New("Invalid path") 52 | } 53 | if len(path) > 0 && path[0] == '/' { 54 | path = path[1:] 55 | } 56 | path = "/tmp/test-sftpd/" + path 57 | return path, nil 58 | } 59 | 60 | func (fs readOnlyDirFs) OpenDir(path string) (sftpd.Dir, error) { 61 | p, e := rfsMangle(path) 62 | if e != nil { 63 | return nil, e 64 | } 65 | f, e := os.Open(p) 66 | if e != nil { 67 | return nil, e 68 | } 69 | return rdir{f}, nil 70 | } 71 | 72 | func (fs readOnlyDirFs) OpenFile(path string, mode uint32, a *sftpd.Attr) (sftpd.File, error) { 73 | p, e := rfsMangle(path) 74 | if e != nil { 75 | return nil, e 76 | } 77 | f, e := os.Open(p) 78 | if e != nil { 79 | return nil, e 80 | } 81 | return rfile{f: f}, nil 82 | } 83 | 84 | func (fs readOnlyDirFs) Stat(name string, islstat bool) (*sftpd.Attr, error) { 85 | p, e := rfsMangle(name) 86 | if e != nil { 87 | return nil, e 88 | } 89 | var fi os.FileInfo 90 | if islstat { 91 | fi, e = os.Lstat(p) 92 | } else { 93 | fi, e = os.Stat(p) 94 | } 95 | if e != nil { 96 | return nil, e 97 | } 98 | var a sftpd.Attr 99 | a.FillFrom(fi) 100 | 101 | return &a, nil 102 | } 103 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | go RunServerHighLevel("127.0.0.1:2023", synthetic{}) 5 | go RunServerLowLevel("127.0.0.1:2024", readOnlyDirFs{}) 6 | <-make(chan int) 7 | } 8 | -------------------------------------------------------------------------------- /example/password.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | ) 7 | 8 | var testUser = "test" 9 | var testPass = prandAlphaNumeric(16) 10 | 11 | const alnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 12 | 13 | // Pseudo random alpha numeric password generation for this example 14 | func prandAlphaNumeric(n int) []byte { 15 | bs := make([]byte, n) 16 | _, e := io.ReadFull(rand.Reader, bs) 17 | if e != nil { 18 | panic(e) 19 | } 20 | for i, b := range bs { 21 | bs[i] = alnum[int(b)%len(alnum)] 22 | } 23 | return bs 24 | } 25 | -------------------------------------------------------------------------------- /example/runserver_highlevel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/taruti/sftpd" 7 | "github.com/taruti/sshutil" 8 | ) 9 | 10 | // RunServerHighLevel is an example how to use the low level API 11 | func RunServerHighLevel(hostport string, fs sftpd.FileSystem) { 12 | cfg := sftpd.Config{HostPort: hostport, FileSystem: fs, LogFunc: log.Println} 13 | cfg.Init() 14 | cfg.PasswordCallback = sshutil.CreatePasswordCheck(testUser, testPass) 15 | 16 | // Add the sshutil.RSA2048 and sshutil.Save flags if needed for the server in question... 17 | hkey, e := sshutil.KeyLoader{Flags: sshutil.Create}.Load() 18 | if e != nil { 19 | log.Println(e) 20 | return 21 | } 22 | cfg.AddHostKey(hkey) 23 | 24 | log.Printf("Listening on %s user %s pass %s\n", hostport, testUser, testPass) 25 | cfg.RunServer() 26 | } 27 | -------------------------------------------------------------------------------- /example/runserver_lowlevel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "github.com/taruti/sftpd" 8 | "github.com/taruti/sshutil" 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | // RunServerLowLevel is an example how to use the low level API 13 | func RunServerLowLevel(hostport string, fs sftpd.FileSystem) { 14 | e := runServer(hostport, fs) 15 | if e != nil { 16 | log.Println("running server errored:", e) 17 | } 18 | } 19 | 20 | func runServer(hostport string, fs sftpd.FileSystem) error { 21 | config := &ssh.ServerConfig{ 22 | PasswordCallback: sshutil.CreatePasswordCheck(testUser, testPass), 23 | } 24 | 25 | // Add the sshutil.RSA2048 and sshutil.Save flags if needed for the server in question... 26 | hkey, e := sshutil.KeyLoader{Flags: sshutil.Create | sshutil.RSA2048}.Load() 27 | // hkey, e := sshutil.KeyLoader{Flags: sshutil.Create}.Load() 28 | if e != nil { 29 | return e 30 | } 31 | 32 | config.AddHostKey(hkey) 33 | 34 | listener, e := net.Listen("tcp", hostport) 35 | if e != nil { 36 | return e 37 | } 38 | 39 | log.Printf("Listening on %s user %s pass %s\n", hostport, testUser, testPass) 40 | 41 | for { 42 | conn, e := listener.Accept() 43 | if e != nil { 44 | return e 45 | } 46 | go HandleConn(conn, config, fs) 47 | } 48 | } 49 | 50 | func HandleConn(conn net.Conn, config *ssh.ServerConfig, fs sftpd.FileSystem) { 51 | defer conn.Close() 52 | e := handleConn(conn, config, fs) 53 | if e != nil { 54 | log.Println("sftpd connection errored:", e) 55 | } 56 | } 57 | func handleConn(conn net.Conn, config *ssh.ServerConfig, fs sftpd.FileSystem) error { 58 | sc, chans, reqs, e := ssh.NewServerConn(conn, config) 59 | if e != nil { 60 | return e 61 | } 62 | defer sc.Close() 63 | 64 | // The incoming Request channel must be serviced. 65 | go PrintDiscardRequests(reqs) 66 | 67 | // Service the incoming Channel channel. 68 | for newChannel := range chans { 69 | if newChannel.ChannelType() != "session" { 70 | newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") 71 | continue 72 | } 73 | channel, requests, err := newChannel.Accept() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | go func(in <-chan *ssh.Request) { 79 | for req := range in { 80 | ok := false 81 | switch { 82 | case sftpd.IsSftpRequest(req): 83 | ok = true 84 | go func() { 85 | e := sftpd.ServeChannel(channel, fs) 86 | if e != nil { 87 | log.Println("sftpd servechannel failed:", e) 88 | } 89 | }() 90 | } 91 | req.Reply(ok, nil) 92 | } 93 | }(requests) 94 | 95 | } 96 | return nil 97 | } 98 | 99 | func PrintDiscardRequests(in <-chan *ssh.Request) { 100 | for req := range in { 101 | log.Println("Discarding ssh request", req.Type, *req) 102 | if req.WantReply { 103 | req.Reply(false, nil) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /example/synthetic_fs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Synthetic file system example 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "io" 9 | "log" 10 | 11 | "github.com/taruti/sftpd" 12 | ) 13 | 14 | type synthetic struct { 15 | sftpd.EmptyFS 16 | } 17 | 18 | type synthDir struct { 19 | m map[string]*synthFile 20 | } 21 | 22 | type synthFile struct { 23 | sftpd.EmptyFile 24 | bs []byte 25 | } 26 | 27 | var synthRoot = &synthDir{map[string]*synthFile{ 28 | "foo": &synthFile{bs: []byte("foo contents")}, 29 | "bar": &synthFile{bs: []byte("bar contents")}, 30 | }} 31 | 32 | func (fs synthetic) OpenDir(path string) (sftpd.Dir, error) { 33 | d := synthDir{m: map[string]*synthFile{}} 34 | for k, v := range synthRoot.m { 35 | d.m[k] = v 36 | } 37 | return &d, nil 38 | } 39 | 40 | func (fs synthetic) OpenFile(path string, mode uint32, attr *sftpd.Attr) (sftpd.File, error) { 41 | if len(path) > 0 && path[0] == '/' { 42 | path = path[1:] 43 | } 44 | f, ok := synthRoot.m[path] 45 | if ok { 46 | return f, nil 47 | } 48 | return nil, errors.New("Not found!") 49 | } 50 | 51 | func (fs synthetic) Stat(path string, islstat bool) (*sftpd.Attr, error) { 52 | var a sftpd.Attr 53 | log.Println("STAT", path) 54 | if path == "" || path == "/" || path == "." { 55 | a.Flags = sftpd.ATTR_MODE 56 | a.Mode = sftpd.MODE_DIR | 0755 57 | return &a, nil 58 | } 59 | if path[0] == '/' { 60 | path = path[1:] 61 | } 62 | f, ok := synthRoot.m[path] 63 | if ok { 64 | f.fillAttr(&a) 65 | log.Println("STAT FILE", f, a) 66 | return &a, nil 67 | } 68 | return nil, errors.New("not found") 69 | } 70 | 71 | func (d synthDir) Readdir(count int) ([]sftpd.NamedAttr, error) { 72 | var rs []sftpd.NamedAttr 73 | for k, v := range d.m { 74 | na := sftpd.NamedAttr{Name: k} 75 | v.fillAttr(&na.Attr) 76 | rs = append(rs, na) 77 | delete(d.m, k) 78 | } 79 | if len(rs) == 0 { 80 | return nil, io.EOF 81 | } 82 | return rs, nil 83 | } 84 | 85 | func (d synthDir) Close() error { 86 | return nil 87 | } 88 | 89 | func (f *synthFile) ReadAt(bs []byte, offset int64) (int, error) { 90 | return bytes.NewReader(f.bs).ReadAt(bs, offset) 91 | } 92 | 93 | func (f *synthFile) WriteAt(bs []byte, offset int64) (int, error) { 94 | log.Printf("WriteAt %d bytes at %d, `%X`", len(bs), offset, bs) 95 | return len(bs), nil 96 | } 97 | 98 | func (f *synthFile) fillAttr(attr *sftpd.Attr) { 99 | attr.Flags = sftpd.ATTR_SIZE | sftpd.ATTR_MODE 100 | attr.Size = uint64(len(f.bs)) 101 | attr.Mode = sftpd.MODE_REGULAR | 0644 102 | } 103 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/taruti/sshutil" 7 | ) 8 | 9 | func ExampleConfig(fs FileSystem) { 10 | cfg := Config{HostPort: ":2022", FileSystem: fs, LogFunc: log.Println} 11 | cfg.Init() 12 | cfg.PasswordCallback = sshutil.CreatePasswordCheck(testUser, testPass) 13 | 14 | // This creates a new host key for each run of the test. 15 | // Add the sshutil.RSA2048 and sshutil.Save flags if wanted. 16 | hkey, e := sshutil.KeyLoader{Flags: sshutil.Create}.Load() 17 | if e != nil { 18 | log.Println(e) 19 | return 20 | } 21 | cfg.AddHostKey(hkey) 22 | 23 | cfg.RunServer() 24 | } 25 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type Attr struct { 10 | Flags uint32 11 | Size uint64 12 | Uid, Gid uint32 13 | User, Group string 14 | Mode os.FileMode 15 | ATime, MTime time.Time 16 | Extended []string 17 | } 18 | 19 | type NamedAttr struct { 20 | Name string 21 | Attr 22 | } 23 | 24 | const ( 25 | ATTR_SIZE = ssh_FILEXFER_ATTR_SIZE 26 | ATTR_UIDGID = ssh_FILEXFER_ATTR_UIDGID 27 | ATTR_MODE = ssh_FILEXFER_ATTR_PERMISSIONS 28 | ATTR_TIME = ssh_FILEXFER_ATTR_ACMODTIME 29 | MODE_REGULAR = os.FileMode(0) 30 | MODE_DIR = os.ModeDir 31 | ) 32 | 33 | type Dir interface { 34 | io.Closer 35 | Readdir(count int) ([]NamedAttr, error) 36 | } 37 | 38 | type File interface { 39 | io.Closer 40 | io.ReaderAt 41 | io.WriterAt 42 | FStat() (*Attr, error) 43 | FSetStat(*Attr) error 44 | } 45 | 46 | type FileSystem interface { 47 | OpenFile(name string, flags uint32, attr *Attr) (File, error) 48 | OpenDir(name string) (Dir, error) 49 | Remove(name string) error 50 | Rename(old string, new string, flags uint32) error 51 | Mkdir(name string, attr *Attr) error 52 | Rmdir(name string) error 53 | Stat(name string, islstat bool) (*Attr, error) 54 | SetStat(name string, attr *Attr) error 55 | ReadLink(path string) (string, error) 56 | CreateLink(path string, target string, flags uint32) error 57 | RealPath(path string) (string, error) 58 | } 59 | 60 | // FillFrom fills an Attr from a os.FileInfo 61 | func (a *Attr) FillFrom(fi os.FileInfo) { 62 | *a = Attr{} 63 | a.Flags = ATTR_SIZE | ATTR_MODE 64 | a.Size = uint64(fi.Size()) 65 | a.Mode = fi.Mode() 66 | a.MTime = fi.ModTime() 67 | } 68 | 69 | func fileModeToSftp(m os.FileMode) uint32 { 70 | var raw = uint32(m.Perm()) 71 | switch { 72 | case m.IsDir(): 73 | raw |= 0040000 74 | case m.IsRegular(): 75 | raw |= 0100000 76 | } 77 | return raw 78 | } 79 | 80 | func sftpToFileMode(raw uint32) os.FileMode { 81 | var m = os.FileMode(raw & 0777) 82 | switch { 83 | case raw&0040000 != 0: 84 | m |= os.ModeDir 85 | case raw&0100000 != 0: 86 | // regular 87 | } 88 | return m 89 | } 90 | -------------------------------------------------------------------------------- /fuzz/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/taruti/sftpd" 8 | ) 9 | 10 | // Fuzz is the interface for the go-fuzz. 11 | func Fuzz(data []byte) int { 12 | frd := &fakeRandChannel{bytes.NewReader(data), 0} 13 | err := sftpd.ServeChannel(frd, sftpd.EmptyFS{}) 14 | if err != nil { 15 | return 0 16 | } 17 | if frd.nw >= 10 { 18 | return 2 19 | } 20 | return 1 21 | } 22 | 23 | type fakeRandChannel struct { 24 | *bytes.Reader 25 | nw int 26 | } 27 | 28 | func (f *fakeRandChannel) Write(bs []byte) (int, error) { 29 | f.nw += len(bs) 30 | return len(bs), nil 31 | } 32 | func (*fakeRandChannel) Close() error { return nil } 33 | func (*fakeRandChannel) CloseWrite() error { return nil } 34 | func (*fakeRandChannel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { 35 | return true, nil 36 | } 37 | func (*fakeRandChannel) Stderr() io.ReadWriter { return nil } 38 | -------------------------------------------------------------------------------- /handle.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import "strconv" 4 | 5 | type handles struct { 6 | f map[string]File 7 | d map[string]Dir 8 | c int64 9 | } 10 | 11 | func (h *handles) init() { 12 | h.f = map[string]File{} 13 | h.d = map[string]Dir{} 14 | } 15 | 16 | func (h *handles) closeAll() { 17 | for _, x := range h.f { 18 | x.Close() 19 | } 20 | for _, x := range h.d { 21 | x.Close() 22 | } 23 | } 24 | func (h *handles) closeHandle(k string) { 25 | if k == "" { 26 | return 27 | } 28 | if k[0] == 'f' { 29 | x, ok := h.f[k] 30 | if ok { 31 | x.Close() 32 | } 33 | delete(h.f, k) 34 | } else if k[0] == 'd' { 35 | x, ok := h.d[k] 36 | if ok { 37 | x.Close() 38 | } 39 | delete(h.d, k) 40 | } 41 | } 42 | func (h *handles) nfiles() int { return len(h.f) } 43 | func (h *handles) ndir() int { return len(h.d) } 44 | 45 | func (h *handles) newFile(f File) string { 46 | h.c++ 47 | k := "f" + strconv.FormatInt(h.c, 16) 48 | h.f[k] = f 49 | return k 50 | } 51 | func (h *handles) newDir(f Dir) string { 52 | h.c++ 53 | k := "d" + strconv.FormatInt(h.c, 16) 54 | h.d[k] = f 55 | return k 56 | } 57 | func (h *handles) getFile(n string) File { 58 | return h.f[n] 59 | } 60 | func (h *handles) getDir(n string) Dir { 61 | return h.d[n] 62 | } 63 | -------------------------------------------------------------------------------- /listen.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "net" 5 | 6 | "golang.org/x/crypto/ssh" 7 | ) 8 | 9 | // Config is the configuration struct for the high level API. 10 | type Config struct { 11 | // ServerConfig should be initialized properly with 12 | // e.g. PasswordCallback and AddHostKey 13 | ssh.ServerConfig 14 | // HostPort specifies specifies [host]:port to listen on. 15 | // e.g. ":2022" or "127.0.0.1:2023". 16 | HostPort string 17 | // LogFunction is used to log errors. 18 | // e.g. log.Println has the right type. 19 | LogFunc func(v ...interface{}) 20 | // FileSystem contains the FileSystem used for this server. 21 | FileSystem FileSystem 22 | 23 | readyChan chan error 24 | connChan chan net.Listener 25 | } 26 | 27 | // Init inits a Config. 28 | func (c *Config) Init() { 29 | c.readyChan = make(chan error, 1) 30 | c.connChan = make(chan net.Listener, 1) 31 | } 32 | 33 | // RunServer runs the server using the high level API. 34 | func (c *Config) RunServer() error { 35 | if c.LogFunc == nil { 36 | c.LogFunc = func(...interface{}) {} 37 | } 38 | e := runServer(c) 39 | if e != nil { 40 | c.LogFunc("sftpd server failed:", e) 41 | } 42 | return e 43 | } 44 | 45 | func runServer(c *Config) error { 46 | listener, e := net.Listen("tcp", c.HostPort) 47 | c.readyChan <- e 48 | close(c.readyChan) 49 | c.connChan <- listener 50 | close(c.connChan) 51 | if e != nil { 52 | return e 53 | } 54 | 55 | for { 56 | conn, e := listener.Accept() 57 | if e != nil { 58 | return e 59 | } 60 | go handleConn(conn, c) 61 | } 62 | } 63 | 64 | func handleConn(conn net.Conn, config *Config) { 65 | defer conn.Close() 66 | e := doHandleConn(conn, config) 67 | if e != nil { 68 | config.LogFunc("sftpd connection error:", e) 69 | } 70 | } 71 | 72 | func doHandleConn(conn net.Conn, config *Config) error { 73 | sc, chans, reqs, e := ssh.NewServerConn(conn, &config.ServerConfig) 74 | if e != nil { 75 | return e 76 | } 77 | defer sc.Close() 78 | 79 | // The incoming Request channel must be serviced. 80 | go printDiscardRequests(config, reqs) 81 | 82 | // Service the incoming Channel channel. 83 | for newChannel := range chans { 84 | if newChannel.ChannelType() != "session" { 85 | newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") 86 | continue 87 | } 88 | channel, requests, err := newChannel.Accept() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | go func(in <-chan *ssh.Request) { 94 | for req := range in { 95 | ok := false 96 | switch { 97 | case IsSftpRequest(req): 98 | ok = true 99 | go func() { 100 | e := ServeChannel(channel, config.FileSystem) 101 | if e != nil { 102 | config.LogFunc("sftpd servechannel failed:", e) 103 | } 104 | }() 105 | } 106 | req.Reply(ok, nil) 107 | } 108 | }(requests) 109 | 110 | } 111 | return nil 112 | } 113 | 114 | func printDiscardRequests(c *Config, in <-chan *ssh.Request) { 115 | for req := range in { 116 | c.LogFunc("sftpd discarding ssh request", req.Type, *req) 117 | if req.WantReply { 118 | req.Reply(false, nil) 119 | } 120 | } 121 | } 122 | 123 | // BlockTillReady will block till the Config is ready to accept connections. 124 | // Returns an error if listening failed. Can be called in a concurrent fashion. 125 | // This is new API - make sure Init is called on the Config before using it. 126 | func (c *Config) BlockTillReady() error { 127 | err, _ := <-c.readyChan 128 | return err 129 | } 130 | 131 | // Close closes the server assosiated with this config. Can be called in a concurrent 132 | // fashion. 133 | // This is new API - make sure Init is called on the Config before using it. 134 | func (c *Config) Close() error { 135 | for c := range c.connChan { 136 | c.Close() 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /nodebug.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | package sftpd 4 | 5 | func debug(...interface{}) {} 6 | func debugf(string, ...interface{}) {} 7 | -------------------------------------------------------------------------------- /readdir.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func readdirLongName(fi *NamedAttr) string { 9 | return fmt.Sprintf("%10s %3d %-8s %-8s %8d %12s %s", 10 | fi.Mode.String(), 11 | 1, // links 12 | fi.User, fi.Group, 13 | fi.Size, 14 | readdirTimeFormat(fi.MTime), 15 | fi.Name, 16 | ) 17 | } 18 | 19 | func readdirTimeFormat(t time.Time) string { 20 | // We return timestamps in UTC, should we offer a customisation point for users? 21 | if t.Year() == time.Now().Year() { 22 | return t.Format("Jan _2 15:04") 23 | } 24 | return t.Format("Jan _2 2006") 25 | } 26 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "time" 12 | 13 | "github.com/taruti/binp" 14 | "github.com/taruti/bytepool" 15 | "golang.org/x/crypto/ssh" 16 | ) 17 | 18 | var sftpSubSystem = []byte{0, 0, 0, 4, 115, 102, 116, 112} 19 | 20 | // IsSftpRequest checks whether a given ssh.Request is for sftp. 21 | func IsSftpRequest(req *ssh.Request) bool { 22 | return req.Type == "subsystem" && bytes.Equal(sftpSubSystem, req.Payload) 23 | } 24 | 25 | var initReply = []byte{0, 0, 0, 5, ssh_FXP_VERSION, 0, 0, 0, 3} 26 | 27 | // ServeChannel serves a ssh.Channel with the given FileSystem. 28 | func ServeChannel(c ssh.Channel, fs FileSystem) error { 29 | defer c.Close() 30 | var h handles 31 | h.init() 32 | defer h.closeAll() 33 | brd := bufio.NewReaderSize(c, 64*1024) 34 | var e error 35 | var plen int 36 | var op byte 37 | var bs []byte 38 | var id uint32 39 | for { 40 | if e != nil { 41 | debug("Sending error", e) 42 | e = writeErr(c, id, e) 43 | if e != nil { 44 | return e 45 | } 46 | } 47 | discard(brd, plen) 48 | plen, op, e = readPacketHeader(brd) 49 | if e != nil { 50 | return e 51 | } 52 | plen-- 53 | debugf("CR op=%v data len=%d\n", ssh_fxp(op), plen) 54 | if plen < 2 { 55 | return errors.New("Packet too short") 56 | } 57 | // Feeding too large values to peek is ok, it just errors. 58 | bs, e = brd.Peek(plen) 59 | if e != nil { 60 | return e 61 | } 62 | debugf("Data %X\n", bs) 63 | p := binp.NewParser(bs) 64 | switch op { 65 | case ssh_FXP_INIT: 66 | e = wrc(c, initReply) 67 | case ssh_FXP_OPEN: 68 | var path string 69 | var flags uint32 70 | var a Attr 71 | e = parseAttr(p.B32(&id).B32String(&path).B32(&flags), &a).End() 72 | if e != nil { 73 | return e 74 | } 75 | if h.nfiles() >= maxFiles { 76 | e = errTooManyFiles 77 | continue 78 | } 79 | var f File 80 | f, e = fs.OpenFile(path, flags, &a) 81 | if e != nil { 82 | continue 83 | } 84 | e = writeHandle(c, id, h.newFile(f)) 85 | case ssh_FXP_CLOSE: 86 | var handle string 87 | e = p.B32(&id).B32String(&handle).End() 88 | if e != nil { 89 | return e 90 | } 91 | h.closeHandle(handle) 92 | e = writeErr(c, id, nil) 93 | case ssh_FXP_READ: 94 | var handle string 95 | var offset uint64 96 | var length uint32 97 | var n int 98 | e = p.B32(&id).B32String(&handle).B64(&offset).B32(&length).End() 99 | if e != nil { 100 | return e 101 | } 102 | f := h.getFile(handle) 103 | if f == nil { 104 | return errInvalidHandle 105 | } 106 | if length > 64*1024 { 107 | length = 64 * 1024 108 | } 109 | bs := bytepool.Alloc(int(length)) 110 | n, e = f.ReadAt(bs, int64(offset)) 111 | // Handle go readers that return io.EOF and bytes at the same time. 112 | if e == io.EOF && n > 0 { 113 | e = nil 114 | } 115 | if e != nil { 116 | bytepool.Free(bs) 117 | continue 118 | } 119 | bs = bs[0:n] 120 | e = wrc(c, binp.Out().B32(1+4+4+uint32(len(bs))).Byte(ssh_FXP_DATA).B32(id).B32(uint32(len(bs))).Out()) 121 | if e == nil { 122 | e = wrc(c, bs) 123 | } 124 | bytepool.Free(bs) 125 | case ssh_FXP_WRITE: 126 | var handle string 127 | var offset uint64 128 | var length uint32 129 | p.B32(&id).B32String(&handle).B64(&offset).B32(&length) 130 | f := h.getFile(handle) 131 | if f == nil { 132 | return errInvalidHandle 133 | } 134 | var bs []byte 135 | e = p.NBytesPeek(int(length), &bs).End() 136 | if e != nil { 137 | return e 138 | } 139 | _, e = f.WriteAt(bs, int64(offset)) 140 | e = writeErr(c, id, e) 141 | case ssh_FXP_LSTAT, ssh_FXP_STAT: 142 | var path string 143 | var a *Attr 144 | e = p.B32(&id).B32String(&path).End() 145 | if e != nil { 146 | return e 147 | } 148 | a, e = fs.Stat(path, op == ssh_FXP_LSTAT) 149 | debug("stat/lstat", path, "=>", a, e) 150 | e = writeAttr(c, id, a, e) 151 | case ssh_FXP_FSTAT: 152 | var handle string 153 | var a *Attr 154 | e = p.B32(&id).B32String(&handle).End() 155 | if e != nil { 156 | return e 157 | } 158 | f := h.getFile(handle) 159 | if f == nil { 160 | return errInvalidHandle 161 | } 162 | a, e = f.FStat() 163 | e = writeAttr(c, id, a, e) 164 | case ssh_FXP_SETSTAT: 165 | var path string 166 | var a Attr 167 | e = parseAttr(p.B32(&id).B32String(&path), &a).End() 168 | if e != nil { 169 | return e 170 | } 171 | e = writeErr(c, id, fs.SetStat(path, &a)) 172 | case ssh_FXP_FSETSTAT: 173 | var handle string 174 | var a Attr 175 | e = parseAttr(p.B32(&id).B32String(&handle), &a).End() 176 | if e != nil { 177 | return e 178 | } 179 | f := h.getFile(handle) 180 | if f == nil { 181 | return errInvalidHandle 182 | } 183 | e = writeErr(c, id, f.FSetStat(&a)) 184 | case ssh_FXP_OPENDIR: 185 | var path string 186 | var dh Dir 187 | e = p.B32(&id).B32String(&path).End() 188 | if e != nil { 189 | return e 190 | } 191 | dh, e = fs.OpenDir(path) 192 | debug("opendir", id, path, "=>", dh, e) 193 | if e != nil { 194 | continue 195 | } 196 | e = writeHandle(c, id, h.newDir(dh)) 197 | 198 | case ssh_FXP_READDIR: 199 | var handle string 200 | e = p.B32(&id).B32String(&handle).End() 201 | if e != nil { 202 | return e 203 | } 204 | f := h.getDir(handle) 205 | if f == nil { 206 | return errInvalidHandle 207 | } 208 | var fis []NamedAttr 209 | fis, e = f.Readdir(1024) 210 | debug("readdir", id, handle, fis, e) 211 | if e != nil { 212 | continue 213 | } 214 | var l binp.Len 215 | o := binp.Out().LenB32(&l).LenStart(&l).Byte(ssh_FXP_NAME).B32(id).B32(uint32(len(fis))) 216 | for _, fi := range fis { 217 | n := fi.Name 218 | o.B32String(n).B32String(readdirLongName(&fi)).B32(fi.Flags) 219 | if fi.Flags&ATTR_SIZE != 0 { 220 | o.B64(uint64(fi.Size)) 221 | } 222 | if fi.Flags&ATTR_UIDGID != 0 { 223 | o.B32(fi.Uid).B32(fi.Gid) 224 | } 225 | if fi.Flags&ATTR_MODE != 0 { 226 | o.B32(fileModeToSftp(fi.Mode)) 227 | } 228 | if fi.Flags&ATTR_TIME != 0 { 229 | outTimes(o, &fi.Attr) 230 | } 231 | } 232 | o.LenDone(&l) 233 | e = wrc(c, o.Out()) 234 | 235 | case ssh_FXP_REMOVE: 236 | var path string 237 | e = p.B32(&id).B32String(&path).End() 238 | if e != nil { 239 | return e 240 | } 241 | e = writeErr(c, id, fs.Remove(path)) 242 | case ssh_FXP_MKDIR: 243 | var path string 244 | var a Attr 245 | p = p.B32(&id).B32String(&path) 246 | e = parseAttr(p, &a).End() 247 | if e != nil { 248 | return e 249 | } 250 | e = writeErr(c, id, fs.Mkdir(path, &a)) 251 | case ssh_FXP_RMDIR: 252 | var path string 253 | e = p.B32(&id).B32String(&path).End() 254 | if e != nil { 255 | return e 256 | } 257 | e = writeErr(c, id, fs.Rmdir(path)) 258 | case ssh_FXP_REALPATH: 259 | var path, newpath string 260 | p.B32(&id).B32String(&path).End() 261 | newpath, e = fs.RealPath(path) 262 | debug("realpath: mapping", path, "=>", newpath, e) 263 | e = writeNameOnly(c, id, newpath, e) 264 | case ssh_FXP_RENAME: 265 | debug("FIXME RENAME NOT SUPPORTED") 266 | e = writeFail(c, id) // FIXME 267 | case ssh_FXP_READLINK: 268 | var path string 269 | e = p.B32(&id).B32String(&path).End() 270 | path, e = fs.ReadLink(path) 271 | e = writeNameOnly(c, id, path, e) 272 | case ssh_FXP_SYMLINK: 273 | debug("FIXME SYMLINK NOT SUPPORTED") 274 | e = writeFail(c, id) // FIXME 275 | } 276 | if e != nil { 277 | return e 278 | } 279 | } 280 | } 281 | 282 | var errInvalidHandle = errors.New("Client supplied an invalid handle") 283 | var errTooManyFiles = errors.New("Too many files") 284 | 285 | const maxFiles = 0x100 286 | 287 | func readPacketHeader(rd *bufio.Reader) (int, byte, error) { 288 | bs := make([]byte, 5) 289 | _, e := io.ReadFull(rd, bs) 290 | if e != nil { 291 | return 0, 0, e 292 | } 293 | return int(binary.BigEndian.Uint32(bs)), bs[4], nil 294 | } 295 | 296 | func parseAttr(p *binp.Parser, a *Attr) *binp.Parser { 297 | p = p.B32(&a.Flags) 298 | if a.Flags&ssh_FILEXFER_ATTR_SIZE != 0 { 299 | p = p.B64(&a.Size) 300 | } 301 | if a.Flags&ssh_FILEXFER_ATTR_UIDGID != 0 { 302 | p = p.B32(&a.Uid).B32(&a.Gid) 303 | } 304 | if a.Flags&ssh_FILEXFER_ATTR_PERMISSIONS != 0 { 305 | var mode uint32 306 | p = p.B32(&mode) 307 | a.Mode = sftpToFileMode(mode) 308 | } 309 | if a.Flags&ssh_FILEXFER_ATTR_ACMODTIME != 0 { 310 | p = inTimes(p, a) 311 | } 312 | if a.Flags&ssh_FILEXFER_ATTR_EXTENDED != 0 { 313 | var count uint32 314 | p = p.B32(&count) 315 | if count > 0xFF { 316 | return nil 317 | } 318 | ss := make([]string, 2*int(count)) 319 | for i := 0; i < int(count); i++ { 320 | var k, v string 321 | p = p.B32String(&k).B32String(&v) 322 | ss[2*i+0] = k 323 | ss[2*i+1] = v 324 | } 325 | a.Extended = ss 326 | } 327 | return p 328 | } 329 | 330 | func writeAttr(c ssh.Channel, id uint32, a *Attr, e error) error { 331 | if e != nil { 332 | return writeErr(c, id, e) 333 | } 334 | var l binp.Len 335 | o := binp.Out().LenB32(&l).LenStart(&l).Byte(ssh_FXP_ATTRS).B32(id).B32(a.Flags) 336 | if a.Flags&ssh_FILEXFER_ATTR_SIZE != 0 { 337 | o = o.B64(a.Size) 338 | } 339 | if a.Flags&ssh_FILEXFER_ATTR_UIDGID != 0 { 340 | o = o.B32(a.Uid).B32(a.Gid) 341 | } 342 | if a.Flags&ssh_FILEXFER_ATTR_PERMISSIONS != 0 { 343 | o = o.B32(fileModeToSftp(a.Mode)) 344 | } 345 | if a.Flags&ssh_FILEXFER_ATTR_ACMODTIME != 0 { 346 | outTimes(o, a) 347 | } 348 | if a.Flags&ssh_FILEXFER_ATTR_EXTENDED != 0 { 349 | count := uint32(len(a.Extended) / 2) 350 | o = o.B32(count) 351 | for _, s := range a.Extended { 352 | o = o.B32String(s) 353 | } 354 | } 355 | o.LenDone(&l) 356 | return wrc(c, o.Out()) 357 | } 358 | 359 | func writeNameOnly(c ssh.Channel, id uint32, path string, e error) error { 360 | if e != nil { 361 | return writeErr(c, id, e) 362 | } 363 | var l binp.Len 364 | o := binp.Out().LenB32(&l).LenStart(&l).Byte(ssh_FXP_NAME).B32(id).B32(1) 365 | o.B32String(path).B32String(path).B32(0) 366 | o.LenDone(&l) 367 | return wrc(c, o.Out()) 368 | } 369 | 370 | var failTmpl = []byte{0, 0, 0, 1 + 4 + 4 + 4 + 4, ssh_FXP_STATUS, 0, 0, 0, 0, 0, 0, 0, ssh_FX_FAILURE, 0, 0, 0, 0, 0, 0, 0, 0} 371 | 372 | func writeFail(c ssh.Channel, id uint32) error { 373 | bs := make([]byte, len(failTmpl)) 374 | copy(bs, failTmpl) 375 | binary.BigEndian.PutUint32(bs[5:], id) 376 | return wrc(c, bs) 377 | } 378 | 379 | func writeErr(c ssh.Channel, id uint32, err error) error { 380 | bs := make([]byte, len(failTmpl)) 381 | copy(bs, failTmpl) 382 | binary.BigEndian.PutUint32(bs[5:], id) 383 | var code ssh_fx 384 | switch { 385 | case err == nil: 386 | code = ssh_FX_OK 387 | case err == io.EOF: 388 | code = ssh_FX_EOF 389 | case os.IsPermission(err): 390 | code = ssh_FX_PERMISSION_DENIED 391 | case os.IsNotExist(err): 392 | code = ssh_FX_NO_SUCH_FILE 393 | default: 394 | code = ssh_FX_FAILURE 395 | } 396 | debug("Sending sftp error code", code) 397 | bs[12] = byte(code) 398 | return wrc(c, bs) 399 | } 400 | 401 | func writeHandle(c ssh.Channel, id uint32, handle string) error { 402 | return wrc(c, binp.OutCap(4+9+len(handle)).B32(uint32(9+len(handle))).B8(ssh_FXP_HANDLE).B32(id).B32String(handle).Out()) 403 | } 404 | 405 | func wrc(c ssh.Channel, bs []byte) error { 406 | _, e := c.Write(bs) 407 | return e 408 | } 409 | 410 | func discard(brd *bufio.Reader, n int) error { 411 | if n == 0 { 412 | return nil 413 | } 414 | m, e := io.Copy(ioutil.Discard, &io.LimitedReader{R: brd, N: int64(n)}) 415 | if int(m) == n && e == io.EOF { 416 | e = nil 417 | } 418 | return e 419 | } 420 | 421 | func outTimes(o *binp.Printer, a *Attr) { 422 | o.B32(uint32(a.ATime.Unix())).B32(uint32(a.MTime.Unix())) 423 | } 424 | func inTimes(p *binp.Parser, a *Attr) *binp.Parser { 425 | var at, mt uint32 426 | p = p.B32(&at).B32(&mt) 427 | a.ATime = time.Unix(int64(at), 0) 428 | a.MTime = time.Unix(int64(mt), 0) 429 | return p 430 | } 431 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package sftpd 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "io" 7 | "net" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | client "github.com/pkg/sftp" 13 | "github.com/taruti/sshutil" 14 | "golang.org/x/crypto/ssh" 15 | ) 16 | 17 | const alnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 18 | 19 | var testUser = "test" 20 | var testPass = func() []byte { 21 | bs := make([]byte, 16) 22 | _, e := io.ReadFull(rand.Reader, bs) 23 | if e != nil { 24 | panic(e) 25 | } 26 | for i, b := range bs { 27 | bs[i] = alnum[int(b)%len(alnum)] 28 | } 29 | return bs 30 | }() 31 | 32 | var tdebug = debug 33 | var tdebugf = debugf 34 | 35 | func failOnErr(t *testing.T, err error, reason string) { 36 | if err != nil { 37 | t.Fatalf("%s: %v", reason, err) 38 | } 39 | } 40 | 41 | func PrintDiscardRequests(in <-chan *ssh.Request) { 42 | for req := range in { 43 | tdebug("PRINTDISC", req.Type, *req) 44 | if req.WantReply { 45 | req.Reply(false, nil) 46 | } 47 | } 48 | } 49 | 50 | func TestServer(t *testing.T) { 51 | tdebug = t.Log 52 | tdebugf = t.Logf 53 | tdebugf("Listening on port 2022 user %s pass %s\n", testUser, testPass) 54 | 55 | config := &ssh.ServerConfig{ 56 | PasswordCallback: sshutil.CreatePasswordCheck(testUser, testPass), 57 | } 58 | 59 | // Add the sshutil.RSA2048 and sshutil.Save flags if needed for the server in question... 60 | hkey, e := sshutil.KeyLoader{Flags: sshutil.Create}.Load() 61 | failOnErr(t, e, "Failed to parse host key") 62 | tdebugf("Public key: %s\n", sshutil.PublicKeyHash(hkey.PublicKey())) 63 | 64 | config.AddHostKey(hkey) 65 | 66 | listener, e := net.Listen("tcp", "127.0.0.1:2022") 67 | failOnErr(t, e, "Failed to listen") 68 | 69 | go ClientDo() 70 | 71 | // for { 72 | nConn, e := listener.Accept() 73 | failOnErr(t, e, "Failed to accept") 74 | handleTestConn(nConn, config, t, EmptyFS{}) 75 | // } 76 | 77 | go ClientDo() 78 | 79 | // for { 80 | nConn, e = listener.Accept() 81 | failOnErr(t, e, "Failed to accept") 82 | os.Mkdir("/tmp/test-sftpd", 0700) 83 | handleTestConn(nConn, config, t, rfs{}) 84 | // } 85 | } 86 | 87 | func handleTestConn(conn net.Conn, config *ssh.ServerConfig, t *testing.T, fs FileSystem) { 88 | sc, chans, reqs, e := ssh.NewServerConn(conn, config) 89 | failOnErr(t, e, "Failed to initiate new connection") 90 | 91 | tdebug("sc", sc) 92 | 93 | // The incoming Request channel must be serviced. 94 | go PrintDiscardRequests(reqs) 95 | 96 | // Service the incoming Channel channel. 97 | for newChannel := range chans { 98 | tdebug("NEWCHANNEL", newChannel, newChannel.ChannelType()) 99 | if newChannel.ChannelType() != "session" { 100 | newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") 101 | continue 102 | } 103 | channel, requests, err := newChannel.Accept() 104 | if err != nil { 105 | panic("could not accept channel.") 106 | } 107 | 108 | go func(in <-chan *ssh.Request) { 109 | for req := range in { 110 | tdebug("REQUEST:", *req) 111 | ok := false 112 | switch { 113 | case IsSftpRequest(req): 114 | ok = true 115 | go func() { tdebug(ServeChannel(channel, fs)) }() 116 | } 117 | req.Reply(ok, nil) 118 | } 119 | }(requests) 120 | 121 | } 122 | } 123 | 124 | func ClientDo() { 125 | e := clientDo() 126 | if e != nil { 127 | tdebug("CLIENT ERROR", e) 128 | } 129 | } 130 | func clientDo() error { 131 | var cc ssh.ClientConfig 132 | cc.User = string(testUser) 133 | cc.Auth = append(cc.Auth, ssh.Password(string(testPass))) 134 | // Use this only for localhost testing. 135 | cc.HostKeyCallback = ssh.InsecureIgnoreHostKey() 136 | conn, e := ssh.Dial("tcp4", "127.0.0.1:2022", &cc) 137 | if e != nil { 138 | return e 139 | } 140 | defer conn.Close() 141 | cl, e := client.NewClient(conn) 142 | if e != nil { 143 | return e 144 | } 145 | cl.Mkdir("/D") 146 | cl.Chmod("/D", 0700) 147 | cl.Remove("/D") 148 | rs, e := cl.ReadDir("/") 149 | if e != nil { 150 | return e 151 | } 152 | tdebug(rs) 153 | return nil 154 | } 155 | 156 | func TestRandomInput(t *testing.T) { 157 | fs := EmptyFS{} 158 | rd := &fakeRandChannel{} 159 | for i := 0; i < 10000; i++ { 160 | rd.rem = 5 161 | ServeChannel(rd, fs) 162 | } 163 | for i := 0; i < 257; i++ { 164 | for j := 0; j < 1000; j++ { 165 | rd.rem = i 166 | ServeChannel(rd, fs) 167 | } 168 | } 169 | } 170 | 171 | type fakeRandChannel struct{ rem int } 172 | 173 | func (fr *fakeRandChannel) Read(data []byte) (int, error) { 174 | if len(data) > fr.rem { 175 | data = data[0:fr.rem] 176 | } 177 | n, e := rand.Read(data) 178 | fr.rem -= n 179 | if fr.rem <= 0 { 180 | fr.rem = 0 181 | e = io.EOF 182 | } 183 | if len(data) >= 4 && data[0]&1 == 0 { 184 | data[0], data[1], data[2] = 0, 0, 0 185 | for i := 5; len(data) >= 4+i; i += 4 { 186 | data[i], data[i+1], data[i+2] = 0, 0, 0 187 | } 188 | } 189 | return n, e 190 | } 191 | func (*fakeRandChannel) Write(bs []byte) (int, error) { 192 | tmp := make([]byte, 1) 193 | rand.Read(tmp) 194 | if tmp[0]&7 == 7 { 195 | return 0, errors.New("Random write error") 196 | } 197 | return len(bs), nil 198 | } 199 | func (*fakeRandChannel) Close() error { return nil } 200 | func (*fakeRandChannel) CloseWrite() error { return nil } 201 | func (*fakeRandChannel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { 202 | return true, nil 203 | } 204 | func (fr *fakeRandChannel) Stderr() io.ReadWriter { return fr } 205 | 206 | type rfile struct { 207 | EmptyFile 208 | f *os.File 209 | } 210 | 211 | func (rf rfile) Close() error { return rf.f.Close() } 212 | func (rf rfile) ReadAt(bs []byte, pos int64) (int, error) { return rf.f.ReadAt(bs, pos) } 213 | 214 | type rfs struct { 215 | EmptyFS 216 | } 217 | 218 | type rdir struct { 219 | d *os.File 220 | } 221 | 222 | func (d rdir) Readdir(count int) ([]NamedAttr, error) { 223 | fis, e := d.d.Readdir(count) 224 | if e != nil { 225 | return nil, e 226 | } 227 | nas := make([]NamedAttr, len(fis)) 228 | for i, fi := range fis { 229 | nas[i].Name = fi.Name() 230 | nas[i].FillFrom(fi) 231 | } 232 | return nas, nil 233 | } 234 | func (d rdir) Close() error { 235 | return d.d.Close() 236 | } 237 | 238 | // Warning: 239 | // Use your own path mangling functionality in production code. 240 | // This can be quite non-trivial depending on the operating system. 241 | // The code below is not sufficient for production servers. 242 | func rfsMangle(path string) (string, error) { 243 | if strings.Contains(path, "..") { 244 | return "", errors.New("Invalid path") 245 | } 246 | if len(path) > 0 && path[0] == '/' { 247 | path = path[1:] 248 | } 249 | path = "/tmp/test-sftpd/" + path 250 | tdebug("MANGLE -> " + path) 251 | return path, nil 252 | } 253 | 254 | func (fs rfs) OpenDir(path string) (Dir, error) { 255 | p, e := rfsMangle(path) 256 | if e != nil { 257 | return nil, e 258 | } 259 | f, e := os.Open(p) 260 | if e != nil { 261 | return nil, e 262 | } 263 | return rdir{f}, nil 264 | } 265 | 266 | func (fs rfs) OpenFile(path string, mode uint32, a *Attr) (File, error) { 267 | p, e := rfsMangle(path) 268 | if e != nil { 269 | return nil, e 270 | } 271 | f, e := os.Open(p) 272 | if e != nil { 273 | return nil, e 274 | } 275 | return rfile{f: f}, nil 276 | } 277 | 278 | func (fs rfs) Stat(name string, islstat bool) (*Attr, error) { 279 | p, e := rfsMangle(name) 280 | if e != nil { 281 | return nil, e 282 | } 283 | var fi os.FileInfo 284 | if islstat { 285 | fi, e = os.Lstat(p) 286 | } else { 287 | fi, e = os.Stat(p) 288 | } 289 | if e != nil { 290 | return nil, e 291 | } 292 | var a Attr 293 | a.FillFrom(fi) 294 | 295 | return &a, nil 296 | } 297 | --------------------------------------------------------------------------------