| package sftp |
| |
| // sftp server counterpart |
| |
| import ( |
| "encoding" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strconv" |
| "sync" |
| "syscall" |
| "time" |
| |
| "github.com/pkg/errors" |
| ) |
| |
| const ( |
| // SftpServerWorkerCount defines the number of workers for the SFTP server |
| 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 |
| } |
| |
| 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 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) responsePacket |
| } |
| |
| // 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), |
| } |
| |
| 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 |
| } |
| } |
| |
| // WithAllocator enable the allocator. |
| // After processing a packet we keep in memory the allocated slices |
| // and we reuse them for new packets. |
| // The allocator is experimental |
| func WithAllocator() ServerOption { |
| return func(s *Server) error { |
| alloc := newAllocator() |
| s.pktMgr.alloc = alloc |
| s.conn.alloc = alloc |
| return nil |
| } |
| } |
| |
| type rxPacket struct { |
| pktType fxp |
| pktBytes []byte |
| } |
| |
| // Up to N parallel servers |
| func (svr *Server) sftpServerWorker(pktChan chan orderedRequest) error { |
| for pkt := range pktChan { |
| // readonly checks |
| readonly := true |
| switch pkt := pkt.requestPacket.(type) { |
| case notReadOnly: |
| readonly = false |
| case *sshFxpOpenPacket: |
| readonly = pkt.readonly() |
| case *sshFxpExtendedPacket: |
| readonly = pkt.readonly() |
| } |
| |
| // If server is operating read-only and a write operation is requested, |
| // return permission denied |
| if !readonly && svr.readOnly { |
| svr.pktMgr.readyPacket( |
| svr.pktMgr.newOrderedResponse(statusFromError(pkt.id(), syscall.EPERM), pkt.orderID()), |
| ) |
| continue |
| } |
| |
| if err := handlePacket(svr, pkt); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func handlePacket(s *Server, p orderedRequest) error { |
| var rpkt responsePacket |
| orderID := p.orderID() |
| switch p := p.requestPacket.(type) { |
| case *sshFxInitPacket: |
| rpkt = &sshFxVersionPacket{ |
| Version: sftpProtocolVersion, |
| Extensions: sftpExtensions, |
| } |
| case *sshFxpStatPacket: |
| // stat the requested file |
| info, err := os.Stat(p.Path) |
| rpkt = &sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| case *sshFxpLstatPacket: |
| // stat the requested file |
| info, err := os.Lstat(p.Path) |
| rpkt = &sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| case *sshFxpFstatPacket: |
| f, ok := s.getHandle(p.Handle) |
| var err error = EBADF |
| var info os.FileInfo |
| if ok { |
| info, err = f.Stat() |
| rpkt = &sshFxpStatResponse{ |
| ID: p.ID, |
| info: info, |
| } |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| case *sshFxpMkdirPacket: |
| // TODO FIXME: ignore flags field |
| err := os.Mkdir(p.Path, 0755) |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpRmdirPacket: |
| err := os.Remove(p.Path) |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpRemovePacket: |
| err := os.Remove(p.Filename) |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpRenamePacket: |
| err := os.Rename(p.Oldpath, p.Newpath) |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpSymlinkPacket: |
| err := os.Symlink(p.Targetpath, p.Linkpath) |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpClosePacket: |
| rpkt = statusFromError(p.ID, s.closeHandle(p.Handle)) |
| case *sshFxpReadlinkPacket: |
| f, err := os.Readlink(p.Path) |
| rpkt = &sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []*sshFxpNameAttr{ |
| { |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }, |
| }, |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| case *sshFxpRealpathPacket: |
| f, err := filepath.Abs(p.Path) |
| f = cleanPath(f) |
| rpkt = &sshFxpNamePacket{ |
| ID: p.ID, |
| NameAttrs: []*sshFxpNameAttr{ |
| { |
| Name: f, |
| LongName: f, |
| Attrs: emptyFileStat, |
| }, |
| }, |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| case *sshFxpOpendirPacket: |
| if stat, err := os.Stat(p.Path); err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } else if !stat.IsDir() { |
| rpkt = statusFromError(p.ID, &os.PathError{ |
| Path: p.Path, Err: syscall.ENOTDIR}) |
| } else { |
| rpkt = (&sshFxpOpenPacket{ |
| ID: p.ID, |
| Path: p.Path, |
| Pflags: sshFxfRead, |
| }).respond(s) |
| } |
| case *sshFxpReadPacket: |
| var err error = EBADF |
| f, ok := s.getHandle(p.Handle) |
| if ok { |
| err = nil |
| data := p.getDataSlice(s.pktMgr.alloc, orderID) |
| n, _err := f.ReadAt(data, int64(p.Offset)) |
| if _err != nil && (_err != io.EOF || n == 0) { |
| err = _err |
| } |
| rpkt = &sshFxpDataPacket{ |
| ID: p.ID, |
| Length: uint32(n), |
| Data: data[:n], |
| // do not use data[:n:n] here to clamp the capacity, we allocated extra capacity above to avoid reallocations |
| } |
| } |
| if err != nil { |
| rpkt = statusFromError(p.ID, err) |
| } |
| |
| case *sshFxpWritePacket: |
| f, ok := s.getHandle(p.Handle) |
| var err error = EBADF |
| if ok { |
| _, err = f.WriteAt(p.Data, int64(p.Offset)) |
| } |
| rpkt = statusFromError(p.ID, err) |
| case *sshFxpExtendedPacket: |
| if p.SpecificPacket == nil { |
| rpkt = statusFromError(p.ID, ErrSSHFxOpUnsupported) |
| } else { |
| rpkt = p.respond(s) |
| } |
| case serverRespondablePacket: |
| rpkt = p.respond(s) |
| default: |
| return errors.Errorf("unexpected packet type %T", p) |
| } |
| |
| s.pktMgr.readyPacket(s.pktMgr.newOrderedResponse(rpkt, orderID)) |
| return nil |
| } |
| |
| // Serve serves SFTP connections until the streams stop or the SFTP subsystem |
| // is stopped. |
| func (svr *Server) Serve() error { |
| defer func() { |
| if svr.pktMgr.alloc != nil { |
| svr.pktMgr.alloc.Free() |
| } |
| }() |
| var wg sync.WaitGroup |
| runWorker := func(ch chan orderedRequest) { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| if err := svr.sftpServerWorker(ch); err != nil { |
| svr.conn.Close() // shuts down recvPacket |
| } |
| }() |
| } |
| pktChan := svr.pktMgr.workerChan(runWorker) |
| |
| var err error |
| var pkt requestPacket |
| var pktType uint8 |
| var pktBytes []byte |
| for { |
| pktType, pktBytes, err = svr.serverConn.recvPacket(svr.pktMgr.getNextOrderID()) |
| if err != nil { |
| // we don't care about releasing allocated pages here, the server will quit and the allocator freed |
| break |
| } |
| |
| pkt, err = makePacket(rxPacket{fxp(pktType), pktBytes}) |
| if err != nil { |
| switch errors.Cause(err) { |
| case errUnknownExtendedPacket: |
| //if err := svr.serverConn.sendError(pkt, ErrSshFxOpUnsupported); err != nil { |
| // debug("failed to send err packet: %v", err) |
| // svr.conn.Close() // shuts down recvPacket |
| // break |
| //} |
| default: |
| debug("makePacket err: %v", err) |
| svr.conn.Close() // shuts down recvPacket |
| break |
| } |
| } |
| |
| pktChan <- svr.pktMgr.newOrderedRequest(pkt) |
| } |
| |
| close(pktChan) // shuts down sftpServerWorkers |
| wg.Wait() // wait for all workers to exit |
| |
| // 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 |
| } |
| |
| 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) marshalPacket() ([]byte, []byte, error) { |
| l := 4 + 1 + 4 // uint32(length) + byte(type) + uint32(id) |
| |
| b := make([]byte, 4, l) |
| b = append(b, sshFxpAttrs) |
| b = marshalUint32(b, p.ID) |
| |
| var payload []byte |
| payload = marshalFileInfo(payload, p.info) |
| |
| return b, payload, nil |
| } |
| |
| func (p *sshFxpStatResponse) MarshalBinary() ([]byte, error) { |
| header, payload, err := p.marshalPacket() |
| return append(header, payload...), err |
| } |
| |
| var emptyFileStat = []interface{}{uint32(0)} |
| |
| func (p *sshFxpOpenPacket) readonly() bool { |
| return !p.hasPflags(sshFxfWrite) |
| } |
| |
| 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) responsePacket { |
| var osFlags int |
| if p.hasPflags(sshFxfRead, sshFxfWrite) { |
| osFlags |= os.O_RDWR |
| } else if p.hasPflags(sshFxfWrite) { |
| osFlags |= os.O_WRONLY |
| } else if p.hasPflags(sshFxfRead) { |
| osFlags |= os.O_RDONLY |
| } else { |
| // how are they opening? |
| return statusFromError(p.ID, syscall.EINVAL) |
| } |
| |
| // Don't use O_APPEND flag as it conflicts with WriteAt. |
| // The sshFxfAppend flag is a no-op here as the client sends the offsets. |
| |
| if p.hasPflags(sshFxfCreat) { |
| osFlags |= os.O_CREATE |
| } |
| if p.hasPflags(sshFxfTrunc) { |
| osFlags |= os.O_TRUNC |
| } |
| if p.hasPflags(sshFxfExcl) { |
| osFlags |= os.O_EXCL |
| } |
| |
| f, err := os.OpenFile(p.Path, osFlags, 0644) |
| if err != nil { |
| return statusFromError(p.ID, err) |
| } |
| |
| handle := svr.nextHandle(f) |
| return &sshFxpHandlePacket{ID: p.ID, Handle: handle} |
| } |
| |
| func (p *sshFxpReaddirPacket) respond(svr *Server) responsePacket { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return statusFromError(p.ID, EBADF) |
| } |
| |
| dirname := f.Name() |
| dirents, err := f.Readdir(128) |
| if err != nil { |
| return 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 ret |
| } |
| |
| func (p *sshFxpSetstatPacket) respond(svr *Server) responsePacket { |
| // additional unmarshalling is required for each possibility here |
| b := p.Attrs.([]byte) |
| var err error |
| |
| debug("setstat name \"%s\"", p.Path) |
| if (p.Flags & sshFileXferAttrSize) != 0 { |
| var size uint64 |
| if size, b, err = unmarshalUint64Safe(b); err == nil { |
| err = os.Truncate(p.Path, int64(size)) |
| } |
| } |
| if (p.Flags & sshFileXferAttrPermissions) != 0 { |
| var mode uint32 |
| if mode, b, err = unmarshalUint32Safe(b); err == nil { |
| err = os.Chmod(p.Path, os.FileMode(mode)) |
| } |
| } |
| if (p.Flags & sshFileXferAttrACmodTime) != 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 & sshFileXferAttrUIDGID) != 0 { |
| var uid uint32 |
| var gid uint32 |
| if uid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if gid, _, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| err = os.Chown(p.Path, int(uid), int(gid)) |
| } |
| } |
| |
| return statusFromError(p.ID, err) |
| } |
| |
| func (p *sshFxpFsetstatPacket) respond(svr *Server) responsePacket { |
| f, ok := svr.getHandle(p.Handle) |
| if !ok { |
| return statusFromError(p.ID, 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 & sshFileXferAttrSize) != 0 { |
| var size uint64 |
| if size, b, err = unmarshalUint64Safe(b); err == nil { |
| err = f.Truncate(int64(size)) |
| } |
| } |
| if (p.Flags & sshFileXferAttrPermissions) != 0 { |
| var mode uint32 |
| if mode, b, err = unmarshalUint32Safe(b); err == nil { |
| err = f.Chmod(os.FileMode(mode)) |
| } |
| } |
| if (p.Flags & sshFileXferAttrACmodTime) != 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 & sshFileXferAttrUIDGID) != 0 { |
| var uid uint32 |
| var gid uint32 |
| if uid, b, err = unmarshalUint32Safe(b); err != nil { |
| } else if gid, _, err = unmarshalUint32Safe(b); err != nil { |
| } else { |
| err = f.Chown(int(uid), int(gid)) |
| } |
| } |
| |
| return statusFromError(p.ID, err) |
| } |
| |
| func statusFromError(id uint32, err error) *sshFxpStatusPacket { |
| ret := &sshFxpStatusPacket{ |
| ID: id, |
| StatusError: StatusError{ |
| // sshFXOk = 0 |
| // sshFXEOF = 1 |
| // sshFXNoSuchFile = 2 ENOENT |
| // sshFXPermissionDenied = 3 |
| // sshFXFailure = 4 |
| // sshFXBadMessage = 5 |
| // sshFXNoConnection = 6 |
| // sshFXConnectionLost = 7 |
| // sshFXOPUnsupported = 8 |
| Code: sshFxOk, |
| }, |
| } |
| if err == nil { |
| return ret |
| } |
| |
| debug("statusFromError: error is %T %#v", err, err) |
| ret.StatusError.Code = sshFxFailure |
| ret.StatusError.msg = err.Error() |
| |
| if os.IsNotExist(err) { |
| ret.StatusError.Code = sshFxNoSuchFile |
| return ret |
| } |
| if code, ok := translateSyscallError(err); ok { |
| ret.StatusError.Code = code |
| return ret |
| } |
| |
| switch e := err.(type) { |
| case fxerr: |
| ret.StatusError.Code = uint32(e) |
| default: |
| if e == io.EOF { |
| ret.StatusError.Code = sshFxEOF |
| } |
| } |
| |
| return ret |
| } |
| |
| func clamp(v, max uint32) uint32 { |
| if v > max { |
| return max |
| } |
| return v |
| } |
| |
| func runLsTypeWord(dirent os.FileInfo) string { |
| // find first character, the type char |
| // b Block special file. |
| // c Character special file. |
| // d Directory. |
| // l Symbolic link. |
| // s Socket link. |
| // p FIFO. |
| // - Regular file. |
| tc := '-' |
| mode := dirent.Mode() |
| if (mode & os.ModeDir) != 0 { |
| tc = 'd' |
| } else if (mode & os.ModeDevice) != 0 { |
| tc = 'b' |
| if (mode & os.ModeCharDevice) != 0 { |
| tc = 'c' |
| } |
| } else if (mode & os.ModeSymlink) != 0 { |
| tc = 'l' |
| } else if (mode & os.ModeSocket) != 0 { |
| tc = 's' |
| } else if (mode & os.ModeNamedPipe) != 0 { |
| tc = 'p' |
| } |
| |
| // owner |
| orc := '-' |
| if (mode & 0400) != 0 { |
| orc = 'r' |
| } |
| owc := '-' |
| if (mode & 0200) != 0 { |
| owc = 'w' |
| } |
| oxc := '-' |
| ox := (mode & 0100) != 0 |
| setuid := (mode & os.ModeSetuid) != 0 |
| if ox && setuid { |
| oxc = 's' |
| } else if setuid { |
| oxc = 'S' |
| } else if ox { |
| oxc = 'x' |
| } |
| |
| // group |
| grc := '-' |
| if (mode & 040) != 0 { |
| grc = 'r' |
| } |
| gwc := '-' |
| if (mode & 020) != 0 { |
| gwc = 'w' |
| } |
| gxc := '-' |
| gx := (mode & 010) != 0 |
| setgid := (mode & os.ModeSetgid) != 0 |
| if gx && setgid { |
| gxc = 's' |
| } else if setgid { |
| gxc = 'S' |
| } else if gx { |
| gxc = 'x' |
| } |
| |
| // all / others |
| arc := '-' |
| if (mode & 04) != 0 { |
| arc = 'r' |
| } |
| awc := '-' |
| if (mode & 02) != 0 { |
| awc = 'w' |
| } |
| axc := '-' |
| ax := (mode & 01) != 0 |
| sticky := (mode & os.ModeSticky) != 0 |
| if ax && sticky { |
| axc = 't' |
| } else if sticky { |
| axc = 'T' |
| } else if ax { |
| axc = 'x' |
| } |
| |
| return fmt.Sprintf("%c%c%c%c%c%c%c%c%c%c", tc, orc, owc, oxc, grc, gwc, gxc, arc, awc, axc) |
| } |