| package sftp |
| |
| // sftp server counterpart |
| |
| import ( |
| "encoding" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strconv" |
| "sync" |
| "syscall" |
| "time" |
| |
| "github.com/pkg/errors" |
| ) |
| |
| const ( |
| sftpServerWorkerCount = 8 |
| ) |
| |
| // Server is an SSH File Transfer Protocol (sftp) server. |
| // This is intended to provide the sftp subsystem to an ssh server daemon. |
| // This implementation currently supports most of sftp server protocol version 3, |
| // as specified at http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 |
| type Server struct { |
| serverConn |
| debugStream io.Writer |
| readOnly bool |
| pktMgr packetManager |
| openFiles map[string]*os.File |
| openFilesLock sync.RWMutex |
| handleCount int |
| maxTxPacket uint32 |
| } |
| |
| func (svr *Server) nextHandle(f *os.File) string { |
| svr.openFilesLock.Lock() |
| defer svr.openFilesLock.Unlock() |
| svr.handleCount++ |
| handle := strconv.Itoa(svr.handleCount) |
| svr.openFiles[handle] = f |
| return handle |
| } |
| |
| func (svr *Server) closeHandle(handle string) error { |
| svr.openFilesLock.Lock() |
| defer svr.openFilesLock.Unlock() |
| if f, ok := svr.openFiles[handle]; ok { |
| delete(svr.openFiles, handle) |
| return f.Close() |
| } |
| |
| return syscall.EBADF |
| } |
| |
| func (svr *Server) getHandle(handle string) (*os.File, bool) { |
| svr.openFilesLock.RLock() |
| defer svr.openFilesLock.RUnlock() |
| f, ok := svr.openFiles[handle] |
| return f, ok |
| } |
| |
| type serverRespondablePacket interface { |
| encoding.BinaryUnmarshaler |
| id() uint32 |
| respond(svr *Server) error |
| } |
| |
| // NewServer creates a new Server instance around the provided streams, serving |
| // content from the root of the filesystem. Optionally, ServerOption |
| // functions may be specified to further configure the Server. |
| // |
| // A subsequent call to Serve() is required to begin serving files over SFTP. |
| func NewServer(rwc io.ReadWriteCloser, options ...ServerOption) (*Server, error) { |
| svrConn := serverConn{ |
| conn: conn{ |
| Reader: rwc, |
| WriteCloser: rwc, |
| }, |
| } |
| s := &Server{ |
| serverConn: svrConn, |
| debugStream: ioutil.Discard, |
| pktMgr: newPktMgr(&svrConn), |
| openFiles: make(map[string]*os.File), |
| maxTxPacket: 1 << 15, |
| } |
| |
| for _, o := range options { |
| if err := o(s); err != nil { |
| return nil, err |
| } |
| } |
| |
| return s, nil |
| } |
| |
| // A ServerOption is a function which applies configuration to a Server. |
| type ServerOption func(*Server) error |
| |
| // WithDebug enables Server debugging output to the supplied io.Writer. |
| func WithDebug(w io.Writer) ServerOption { |
| return func(s *Server) error { |
| s.debugStream = w |
| return nil |
| } |
| } |
| |
| // ReadOnly configures a Server to serve files in read-only mode. |
| func ReadOnly() ServerOption { |
| return func(s *Server) error { |
| s.readOnly = true |
| return nil |
| } |
| } |
| |
| type rxPacket struct { |
| pktType fxp |
| pktBytes []byte |
| } |
| |
| // Up to N parallel servers |
| func (svr *Server) sftpServerWorker(pktChan chan requestPacket) error { |
| for pkt := range pktChan { |
| |
| // readonly checks |
| readonly := true |
| switch pkt := pkt.(type) { |
| case notReadOnly: |
| readonly = false |
| case *sshFxpOpenPacket: |
| readonly = pkt.readonly() |
| case *sshFxpExtendedPacket: |
| readonly = pkt.SpecificPacket.readonly() |
| } |
| |
| // If server is operating read-only and a write operation is requested, |
| // return permission denied |
| if !readonly && svr.readOnly { |
| if err := svr.sendError(pkt, syscall.EPERM); err != nil { |
| return errors.Wrap(err, "failed to send read only packet response") |
| } |
| continue |
| } |
| |
| if err := handlePacket(svr, pkt); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func handlePacket(s *Server, p interface{}) error { |
| switch p := p.(type) { |
| case *sshFxInitPacket: |
| return s.sendPacket(sshFxVersionPacket{sftpProtocolVersion, nil}) |
| case *sshFxpStatPacket: |
| // stat the requested file |
| info, err := os.Stat(p.Path) |
| if err != nil { |
| return s.sendError(p, err) |
| } |
| return s.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| case *sshFxpLstatPacket: |
| // stat the requested file |
| info, err := os.Lstat(p.Path) |
| if err != nil { |
| return s.sendError(p, err) |
| } |
| return s.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| case *sshFxpFstatPacket: |
| f, ok := s.getHandle(p.Handle) |
| if !ok { |
| return s.sendError(p, syscall.EBADF) |
| } |
| |
| info, err := f.Stat() |
| if err != nil { |
| return s.sendError(p, err) |
| } |
| |
| return s.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| case *sshFxpMkdirPacket: |
| // TODO FIXME: ignore flags field |
| err := os.Mkdir(p.Path, 0755) |
| return s.sendError(p, err) |
| case *sshFxpRmdirPacket: |
| err := os.Remove(p.Path) |
| return s.sendError(p, err) |
| case *sshFxpRemovePacket: |
| err := os.Remove(p.Filename) |
| return s.sendError(p, err) |
| case *sshFxpRenamePacket: |
| err := os.Rename(p.Oldpath, p.Newpath) |
| return s.sendError(p, err) |
| case *sshFxpSymlinkPacket: |
| err := os.Symlink(p.Targetpath, p.Linkpath) |
| return s.sendError(p, err) |
| case *sshFxpClosePacket: |
| return s.sendError(p, s.closeHandle(p.Handle)) |
| case *sshFxpReadlinkPacket: |
| f, err := os.Readlink(p.Path) |
| if err != nil { |
| return s.sendError(p, err) |
| } |
| |
| return s.sendPacket(sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []sshFxpNameAttr{{ |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }}, |
| }) |
| |
| case *sshFxpRealpathPacket: |
| f, err := filepath.Abs(p.Path) |
| if err != nil { |
| return s.sendError(p, err) |
| } |
| f = filepath.Clean(f) |
| f = filepath.ToSlash(f) // make path more Unix like on windows servers |
| return s.sendPacket(sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []sshFxpNameAttr{{ |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }}, |
| }) |
| case *sshFxpOpendirPacket: |
| return sshFxpOpenPacket{ |
| ID: p.ID, |
| Path: p.Path, |
| Pflags: ssh_FXF_READ, |
| }.respond(s) |
| case *sshFxpReadPacket: |
| f, ok := s.getHandle(p.Handle) |
| if !ok { |
| return s.sendError(p, syscall.EBADF) |
| } |
| |
| data := make([]byte, clamp(p.Len, s.maxTxPacket)) |
| n, err := f.ReadAt(data, int64(p.Offset)) |
| if err != nil && (err != io.EOF || n == 0) { |
| return s.sendError(p, err) |
| } |
| return s.sendPacket(sshFxpDataPacket{ |
| ID: p.ID, |
| Length: uint32(n), |
| Data: data[:n], |
| }) |
| case *sshFxpWritePacket: |
| f, ok := s.getHandle(p.Handle) |
| if !ok { |
| return s.sendError(p, syscall.EBADF) |
| } |
| |
| _, err := f.WriteAt(p.Data, int64(p.Offset)) |
| return s.sendError(p, err) |
| case serverRespondablePacket: |
| err := p.respond(s) |
| return errors.Wrap(err, "pkt.respond failed") |
| default: |
| return errors.Errorf("unexpected packet type %T", p) |
| } |
| } |
| |
| type requestChan chan requestPacket |
| |
| func (svr *Server) sftpServerWorkers(worker func(requestChan)) requestChan { |
| |
| rwChan := make(chan requestPacket, sftpServerWorkerCount) |
| for i := 0; i < sftpServerWorkerCount; i++ { |
| go worker(rwChan) |
| } |
| |
| cmdChan := make(chan requestPacket) |
| go worker(cmdChan) |
| |
| pktChan := make(chan requestPacket, sftpServerWorkerCount) |
| go func() { |
| // start with cmdChan |
| curChan := cmdChan |
| for pkt := range pktChan { |
| // on file open packet, switch to rwChan |
| switch pkt.(type) { |
| case *sshFxpOpenPacket: |
| curChan = rwChan |
| // on file close packet, switch back to cmdChan |
| // after waiting for any reads/writes to finish |
| case *sshFxpClosePacket: |
| // wait for rwChan to finish |
| svr.pktMgr.working.Wait() |
| // stop using rwChan |
| curChan = cmdChan |
| } |
| svr.pktMgr.incomingPacket(pkt) |
| curChan <- pkt |
| } |
| close(rwChan) |
| close(cmdChan) |
| }() |
| |
| return pktChan |
| } |
| |
| // Serve serves SFTP connections until the streams stop or the SFTP subsystem |
| // is stopped. |
| func (svr *Server) Serve() error { |
| var wg sync.WaitGroup |
| wg.Add(1) |
| workerFunc := func(ch requestChan) { |
| wg.Add(1) |
| defer wg.Done() |
| if err := svr.sftpServerWorker(ch); err != nil { |
| svr.conn.Close() // shuts down recvPacket |
| } |
| } |
| pktChan := svr.sftpServerWorkers(workerFunc) |
| |
| var err error |
| var pkt requestPacket |
| var pktType uint8 |
| var pktBytes []byte |
| for { |
| pktType, pktBytes, err = svr.recvPacket() |
| if err != nil { |
| break |
| } |
| |
| pkt, err = makePacket(rxPacket{fxp(pktType), pktBytes}) |
| if err != nil { |
| debug("makePacket err: %v", err) |
| svr.conn.Close() // shuts down recvPacket |
| break |
| } |
| |
| pktChan <- pkt |
| } |
| wg.Done() |
| |
| close(pktChan) // shuts down sftpServerWorkers |
| wg.Wait() // wait for all workers to exit |
| svr.pktMgr.close() // shuts down packetManager |
| |
| // close any still-open files |
| for handle, file := range svr.openFiles { |
| fmt.Fprintf(svr.debugStream, "sftp server file with handle %q left open: %v\n", handle, file.Name()) |
| file.Close() |
| } |
| return err // error from recvPacket |
| } |
| |
| // Wrap underlying connection methods to use packetManager |
| func (svr *Server) sendPacket(m encoding.BinaryMarshaler) error { |
| if pkt, ok := m.(responsePacket); ok { |
| svr.pktMgr.readyPacket(pkt) |
| } else { |
| return errors.Errorf("unexpected packet type %T", m) |
| } |
| return nil |
| } |
| |
| func (svr *Server) sendError(p ider, err error) error { |
| return svr.sendPacket(statusFromError(p, err)) |
| } |
| |
| type ider interface { |
| id() uint32 |
| } |
| |
| // The init packet has no ID, so we just return a zero-value ID |
| func (p sshFxInitPacket) id() uint32 { return 0 } |
| |
| type sshFxpStatResponse struct { |
| ID uint32 |
| info os.FileInfo |
| } |
| |
| func (p sshFxpStatResponse) MarshalBinary() ([]byte, error) { |
| b := []byte{ssh_FXP_ATTRS} |
| b = marshalUint32(b, p.ID) |
| b = marshalFileInfo(b, p.info) |
| return b, nil |
| } |
| |
| var emptyFileStat = []interface{}{uint32(0)} |
| |
| func (p sshFxpOpenPacket) readonly() bool { |
| return !p.hasPflags(ssh_FXF_WRITE) |
| } |
| |
| func (p sshFxpOpenPacket) hasPflags(flags ...uint32) bool { |
| for _, f := range flags { |
| if p.Pflags&f == 0 { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func (p sshFxpOpenPacket) respond(svr *Server) error { |
| var osFlags int |
| if p.hasPflags(ssh_FXF_READ, ssh_FXF_WRITE) { |
| osFlags |= os.O_RDWR |
| } else if p.hasPflags(ssh_FXF_WRITE) { |
| osFlags |= os.O_WRONLY |
| } else if p.hasPflags(ssh_FXF_READ) { |
| osFlags |= os.O_RDONLY |
| } else { |
| // how are they opening? |
| return svr.sendError(p, syscall.EINVAL) |
| } |
| |
| if p.hasPflags(ssh_FXF_APPEND) { |
| osFlags |= os.O_APPEND |
| } |
| if p.hasPflags(ssh_FXF_CREAT) { |
| osFlags |= os.O_CREATE |
| } |
| if p.hasPflags(ssh_FXF_TRUNC) { |
| osFlags |= os.O_TRUNC |
| } |
| if p.hasPflags(ssh_FXF_EXCL) { |
| osFlags |= os.O_EXCL |
| } |
| |
| f, err := os.OpenFile(p.Path, osFlags, 0644) |
| if err != nil { |
| return svr.sendError(p, err) |
| } |
| |
| handle := svr.nextHandle(f) |
| return svr.sendPacket(sshFxpHandlePacket{p.ID, handle}) |
| } |
| |
| func (p sshFxpReaddirPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendError(p, syscall.EBADF) |
| } |
| |
| dirname := f.Name() |
| dirents, err := f.Readdir(128) |
| if err != nil { |
| return svr.sendError(p, err) |
| } |
| |
| ret := sshFxpNamePacket{ID: p.ID} |
| for _, dirent := range dirents { |
| ret.NameAttrs = append(ret.NameAttrs, sshFxpNameAttr{ |
| Name: dirent.Name(), |
| LongName: runLs(dirname, dirent), |
| Attrs: []interface{}{dirent}, |
| }) |
| } |
| return svr.sendPacket(ret) |
| } |
| |
| func (p sshFxpSetstatPacket) respond(svr *Server) error { |
| // additional unmarshalling is required for each possibility here |
| b := p.Attrs.([]byte) |
| var err error |
| |
| debug("setstat name \"%s\"", p.Path) |
| if (p.Flags & ssh_FILEXFER_ATTR_SIZE) != 0 { |
| var size uint64 |
| if size, b, err = unmarshalUint64Safe(b); err == nil { |
| err = os.Truncate(p.Path, int64(size)) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_PERMISSIONS) != 0 { |
| var mode uint32 |
| if mode, b, err = unmarshalUint32Safe(b); err == nil { |
| err = os.Chmod(p.Path, os.FileMode(mode)) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_ACMODTIME) != 0 { |
| var atime uint32 |
| var mtime uint32 |
| if atime, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if mtime, b, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| atimeT := time.Unix(int64(atime), 0) |
| mtimeT := time.Unix(int64(mtime), 0) |
| err = os.Chtimes(p.Path, atimeT, mtimeT) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_UIDGID) != 0 { |
| var uid uint32 |
| var gid uint32 |
| if uid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if gid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| err = os.Chown(p.Path, int(uid), int(gid)) |
| } |
| } |
| |
| return svr.sendError(p, err) |
| } |
| |
| func (p sshFxpFsetstatPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendError(p, syscall.EBADF) |
| } |
| |
| // additional unmarshalling is required for each possibility here |
| b := p.Attrs.([]byte) |
| var err error |
| |
| debug("fsetstat name \"%s\"", f.Name()) |
| if (p.Flags & ssh_FILEXFER_ATTR_SIZE) != 0 { |
| var size uint64 |
| if size, b, err = unmarshalUint64Safe(b); err == nil { |
| err = f.Truncate(int64(size)) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_PERMISSIONS) != 0 { |
| var mode uint32 |
| if mode, b, err = unmarshalUint32Safe(b); err == nil { |
| err = f.Chmod(os.FileMode(mode)) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_ACMODTIME) != 0 { |
| var atime uint32 |
| var mtime uint32 |
| if atime, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if mtime, b, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| atimeT := time.Unix(int64(atime), 0) |
| mtimeT := time.Unix(int64(mtime), 0) |
| err = os.Chtimes(f.Name(), atimeT, mtimeT) |
| } |
| } |
| if (p.Flags & ssh_FILEXFER_ATTR_UIDGID) != 0 { |
| var uid uint32 |
| var gid uint32 |
| if uid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if gid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| err = f.Chown(int(uid), int(gid)) |
| } |
| } |
| |
| return svr.sendError(p, err) |
| } |
| |
| // translateErrno translates a syscall error number to a SFTP error code. |
| func translateErrno(errno syscall.Errno) uint32 { |
| switch errno { |
| case 0: |
| return ssh_FX_OK |
| case syscall.ENOENT: |
| return ssh_FX_NO_SUCH_FILE |
| case syscall.EPERM: |
| return ssh_FX_PERMISSION_DENIED |
| } |
| |
| return ssh_FX_FAILURE |
| } |
| |
| func statusFromError(p ider, err error) sshFxpStatusPacket { |
| ret := sshFxpStatusPacket{ |
| ID: p.id(), |
| StatusError: StatusError{ |
| // ssh_FX_OK = 0 |
| // ssh_FX_EOF = 1 |
| // ssh_FX_NO_SUCH_FILE = 2 ENOENT |
| // ssh_FX_PERMISSION_DENIED = 3 |
| // ssh_FX_FAILURE = 4 |
| // ssh_FX_BAD_MESSAGE = 5 |
| // ssh_FX_NO_CONNECTION = 6 |
| // ssh_FX_CONNECTION_LOST = 7 |
| // ssh_FX_OP_UNSUPPORTED = 8 |
| Code: ssh_FX_OK, |
| }, |
| } |
| if err != nil { |
| debug("statusFromError: error is %T %#v", err, err) |
| ret.StatusError.Code = ssh_FX_FAILURE |
| ret.StatusError.msg = err.Error() |
| if err == io.EOF { |
| ret.StatusError.Code = ssh_FX_EOF |
| } else if errno, ok := err.(syscall.Errno); ok { |
| ret.StatusError.Code = translateErrno(errno) |
| } else if pathError, ok := err.(*os.PathError); ok { |
| debug("statusFromError: error is %T %#v", pathError.Err, pathError.Err) |
| if errno, ok := pathError.Err.(syscall.Errno); ok { |
| ret.StatusError.Code = translateErrno(errno) |
| } |
| } |
| } |
| return ret |
| } |
| |
| func clamp(v, max uint32) uint32 { |
| if v > max { |
| return max |
| } |
| return v |
| } |