package sftp // sftp server counterpart import ( "encoding" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "strconv" "sync" "syscall" "time" ) 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 https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt type Server struct { *serverConn debugStream io.Writer readOnly bool pktMgr *packetManager openFiles map[string]*os.File openFilesLock sync.RWMutex handleCount int workDir string } 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 } } // WithServerWorkingDirectory sets a working directory to use as base // for relative paths. // If unset the default is current working directory (os.Getwd). func WithServerWorkingDirectory(workDir string) ServerOption { return func(s *Server) error { s.workDir = cleanPath(workDir) 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(s.toLocalPath(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(s.toLocalPath(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(s.toLocalPath(p.Path), 0o755) rpkt = statusFromError(p.ID, err) case *sshFxpRmdirPacket: err := os.Remove(s.toLocalPath(p.Path)) rpkt = statusFromError(p.ID, err) case *sshFxpRemovePacket: err := os.Remove(s.toLocalPath(p.Filename)) rpkt = statusFromError(p.ID, err) case *sshFxpRenamePacket: err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath)) rpkt = statusFromError(p.ID, err) case *sshFxpSymlinkPacket: err := os.Symlink(s.toLocalPath(p.Targetpath), s.toLocalPath(p.Linkpath)) rpkt = statusFromError(p.ID, err) case *sshFxpClosePacket: rpkt = statusFromError(p.ID, s.closeHandle(p.Handle)) case *sshFxpReadlinkPacket: f, err := os.Readlink(s.toLocalPath(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(s.toLocalPath(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: lp := s.toLocalPath(p.Path) if stat, err := os.Stat(lp); err != nil { rpkt = statusFromError(p.ID, err) } else if !stat.IsDir() { rpkt = statusFromError(p.ID, &os.PathError{ Path: lp, 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 fmt.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. It returns nil if the server exits cleanly. 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 { // Check whether the connection terminated cleanly in-between packets. if err == io.EOF { 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 { case errors.Is(err, 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(svr.toLocalPath(p.Path), osFlags, 0o644) 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) } dirents, err := f.Readdir(128) if err != nil { return statusFromError(p.ID, err) } idLookup := osIDLookup{} ret := &sshFxpNamePacket{ID: p.ID} for _, dirent := range dirents { ret.NameAttrs = append(ret.NameAttrs, &sshFxpNameAttr{ Name: dirent.Name(), LongName: runLs(idLookup, 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 p.Path = svr.toLocalPath(p.Path) 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 } if errors.Is(err, io.EOF) { ret.StatusError.Code = sshFxEOF return ret } var e fxerr if errors.As(err, &e) { ret.StatusError.Code = uint32(e) return ret } return ret }