| package sftp |
| |
| // sftp server counterpart |
| |
| import ( |
| "encoding" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strconv" |
| "sync" |
| "syscall" |
| "time" |
| ) |
| |
| 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 { |
| in io.Reader |
| out io.WriteCloser |
| outMutex *sync.Mutex |
| debugStream io.Writer |
| readOnly bool |
| lastID uint32 |
| pktChan chan rxPacket |
| openFiles map[string]*os.File |
| openFilesLock *sync.RWMutex |
| handleCount int |
| maxTxPacket uint32 |
| workerCount int |
| } |
| |
| 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 |
| readonly() bool |
| } |
| |
| // 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(in io.Reader, out io.WriteCloser, options ...ServerOption) (*Server, error) { |
| s := &Server{ |
| in: in, |
| out: out, |
| outMutex: &sync.Mutex{}, |
| debugStream: ioutil.Discard, |
| pktChan: make(chan rxPacket, sftpServerWorkerCount), |
| openFiles: map[string]*os.File{}, |
| openFilesLock: &sync.RWMutex{}, |
| maxTxPacket: 1 << 15, |
| workerCount: sftpServerWorkerCount, |
| } |
| |
| 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 |
| } |
| |
| // Unmarshal a single logical packet from the secure channel |
| func (svr *Server) rxPackets() error { |
| defer close(svr.pktChan) |
| |
| for { |
| pktType, pktBytes, err := recvPacket(svr.in) |
| switch err { |
| case nil: |
| svr.pktChan <- rxPacket{fxp(pktType), pktBytes} |
| case io.EOF: |
| return nil |
| default: |
| fmt.Fprintf(svr.debugStream, "recvPacket error: %v\n", err) |
| return err |
| } |
| } |
| } |
| |
| // Up to N parallel servers |
| func (svr *Server) sftpServerWorker(doneChan chan error) { |
| for pkt := range svr.pktChan { |
| dPkt, err := svr.decodePacket(pkt.pktType, pkt.pktBytes) |
| if err != nil { |
| fmt.Fprintf(svr.debugStream, "decodePacket error: %v\n", err) |
| doneChan <- err |
| return |
| } |
| |
| // If server is operating read-only and a write operation is requested, |
| // return permission denied |
| if !dPkt.readonly() && svr.readOnly { |
| _ = svr.sendPacket(statusFromError(dPkt.id(), syscall.EPERM)) |
| continue |
| } |
| |
| _ = dPkt.respond(svr) |
| } |
| doneChan <- nil |
| } |
| |
| // Serve serves SFTP connections until the streams stop or the SFTP subsystem |
| // is stopped. |
| func (svr *Server) Serve() error { |
| go svr.rxPackets() |
| doneChan := make(chan error) |
| for i := 0; i < svr.workerCount; i++ { |
| go svr.sftpServerWorker(doneChan) |
| } |
| for i := 0; i < svr.workerCount; i++ { |
| if err := <-doneChan; err != nil { |
| // abort early and shut down the session on un-decodable packets |
| break |
| } |
| } |
| // close any still-open files |
| for handle, file := range svr.openFiles { |
| fmt.Fprintf(svr.debugStream, "sftp server file with handle '%v' left open: %v\n", handle, file.Name()) |
| file.Close() |
| } |
| return svr.out.Close() |
| } |
| |
| func (svr *Server) decodePacket(pktType fxp, pktBytes []byte) (serverRespondablePacket, error) { |
| var pkt serverRespondablePacket |
| switch pktType { |
| case ssh_FXP_INIT: |
| pkt = &sshFxInitPacket{} |
| case ssh_FXP_LSTAT: |
| pkt = &sshFxpLstatPacket{} |
| case ssh_FXP_OPEN: |
| pkt = &sshFxpOpenPacket{} |
| case ssh_FXP_CLOSE: |
| pkt = &sshFxpClosePacket{} |
| case ssh_FXP_READ: |
| pkt = &sshFxpReadPacket{} |
| case ssh_FXP_WRITE: |
| pkt = &sshFxpWritePacket{} |
| case ssh_FXP_FSTAT: |
| pkt = &sshFxpFstatPacket{} |
| case ssh_FXP_SETSTAT: |
| pkt = &sshFxpSetstatPacket{} |
| case ssh_FXP_FSETSTAT: |
| pkt = &sshFxpFsetstatPacket{} |
| case ssh_FXP_OPENDIR: |
| pkt = &sshFxpOpendirPacket{} |
| case ssh_FXP_READDIR: |
| pkt = &sshFxpReaddirPacket{} |
| case ssh_FXP_REMOVE: |
| pkt = &sshFxpRemovePacket{} |
| case ssh_FXP_MKDIR: |
| pkt = &sshFxpMkdirPacket{} |
| case ssh_FXP_RMDIR: |
| pkt = &sshFxpRmdirPacket{} |
| case ssh_FXP_REALPATH: |
| pkt = &sshFxpRealpathPacket{} |
| case ssh_FXP_STAT: |
| pkt = &sshFxpStatPacket{} |
| case ssh_FXP_RENAME: |
| pkt = &sshFxpRenamePacket{} |
| case ssh_FXP_READLINK: |
| pkt = &sshFxpReadlinkPacket{} |
| case ssh_FXP_SYMLINK: |
| pkt = &sshFxpSymlinkPacket{} |
| default: |
| return nil, fmt.Errorf("unhandled packet type: %s", pktType) |
| } |
| err := pkt.UnmarshalBinary(pktBytes) |
| return pkt, err |
| } |
| |
| func (p sshFxInitPacket) respond(svr *Server) error { |
| return svr.sendPacket(sshFxVersionPacket{sftpProtocolVersion, nil}) |
| } |
| |
| // The init packet has no ID, so we just return a zero-value ID |
| func (p sshFxInitPacket) id() uint32 { return 0 } |
| func (p sshFxInitPacket) readonly() bool { return true } |
| |
| 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 |
| } |
| |
| func (p sshFxpLstatPacket) readonly() bool { return true } |
| |
| func (p sshFxpLstatPacket) respond(svr *Server) error { |
| // stat the requested file |
| info, err := os.Lstat(p.Path) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| return svr.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| } |
| |
| func (p sshFxpStatPacket) readonly() bool { return true } |
| |
| func (p sshFxpStatPacket) respond(svr *Server) error { |
| // stat the requested file |
| info, err := os.Stat(p.Path) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| return svr.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| } |
| |
| func (p sshFxpFstatPacket) readonly() bool { return true } |
| |
| func (p sshFxpFstatPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendPacket(statusFromError(p.ID, syscall.EBADF)) |
| } |
| |
| info, err := f.Stat() |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| return svr.sendPacket(sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| }) |
| } |
| |
| func (p sshFxpMkdirPacket) readonly() bool { return false } |
| |
| func (p sshFxpMkdirPacket) respond(svr *Server) error { |
| // TODO FIXME: ignore flags field |
| err := os.Mkdir(p.Path, 0755) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpRmdirPacket) readonly() bool { return false } |
| |
| func (p sshFxpRmdirPacket) respond(svr *Server) error { |
| err := os.Remove(p.Path) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpRemovePacket) readonly() bool { return false } |
| |
| func (p sshFxpRemovePacket) respond(svr *Server) error { |
| err := os.Remove(p.Filename) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpRenamePacket) readonly() bool { return false } |
| |
| func (p sshFxpRenamePacket) respond(svr *Server) error { |
| err := os.Rename(p.Oldpath, p.Newpath) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpSymlinkPacket) readonly() bool { return false } |
| |
| func (p sshFxpSymlinkPacket) respond(svr *Server) error { |
| err := os.Symlink(p.Targetpath, p.Linkpath) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| var emptyFileStat = []interface{}{uint32(0)} |
| |
| func (p sshFxpReadlinkPacket) readonly() bool { return true } |
| |
| func (p sshFxpReadlinkPacket) respond(svr *Server) error { |
| f, err := os.Readlink(p.Path) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| return svr.sendPacket(sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []sshFxpNameAttr{{ |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }}, |
| }) |
| } |
| |
| func (p sshFxpRealpathPacket) readonly() bool { return true } |
| |
| func (p sshFxpRealpathPacket) respond(svr *Server) error { |
| f, err := filepath.EvalSymlinks(p.Path) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| f, err = filepath.Abs(f) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| f = filepath.Clean(f) |
| |
| return svr.sendPacket(sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []sshFxpNameAttr{{ |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }}, |
| }) |
| } |
| |
| func (p sshFxpOpendirPacket) readonly() bool { return true } |
| |
| func (p sshFxpOpendirPacket) respond(svr *Server) error { |
| return sshFxpOpenPacket{ |
| ID: p.ID, |
| Path: p.Path, |
| Pflags: ssh_FXF_READ, |
| }.respond(svr) |
| } |
| |
| 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.sendPacket(statusFromError(p.ID, 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.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| handle := svr.nextHandle(f) |
| return svr.sendPacket(sshFxpHandlePacket{p.ID, handle}) |
| } |
| |
| func (p sshFxpClosePacket) readonly() bool { return true } |
| |
| func (p sshFxpClosePacket) respond(svr *Server) error { |
| return svr.sendPacket(statusFromError(p.ID, svr.closeHandle(p.Handle))) |
| } |
| |
| func (p sshFxpReadPacket) readonly() bool { return true } |
| |
| func (p sshFxpReadPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendPacket(statusFromError(p.ID, syscall.EBADF)) |
| } |
| |
| if p.Len > svr.maxTxPacket { |
| p.Len = svr.maxTxPacket |
| } |
| ret := sshFxpDataPacket{ |
| ID: p.ID, |
| Length: p.Len, |
| Data: make([]byte, p.Len), |
| } |
| |
| n, err := f.ReadAt(ret.Data, int64(p.Offset)) |
| if err != nil && (err != io.EOF || n == 0) { |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| ret.Length = uint32(n) |
| return svr.sendPacket(ret) |
| } |
| |
| func (p sshFxpWritePacket) readonly() bool { return false } |
| |
| func (p sshFxpWritePacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendPacket(statusFromError(p.ID, syscall.EBADF)) |
| } |
| |
| _, err := f.WriteAt(p.Data, int64(p.Offset)) |
| return svr.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpReaddirPacket) readonly() bool { return true } |
| |
| func (p sshFxpReaddirPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendPacket(statusFromError(p.ID, syscall.EBADF)) |
| } |
| |
| dirname := f.Name() |
| dirents, err := f.Readdir(128) |
| if err != nil { |
| return svr.sendPacket(statusFromError(p.ID, 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) readonly() bool { return false } |
| |
| 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.sendPacket(statusFromError(p.ID, err)) |
| } |
| |
| func (p sshFxpFsetstatPacket) readonly() bool { return false } |
| |
| func (p sshFxpFsetstatPacket) respond(svr *Server) error { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return svr.sendPacket(statusFromError(p.ID, 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.sendPacket(statusFromError(p.ID, 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(id uint32, err error) sshFxpStatusPacket { |
| ret := sshFxpStatusPacket{ |
| ID: 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 |
| } |