| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- 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
- }
|