package sftp
import (
"encoding"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
)
const (
SftpServerWorkerCount = 8
)
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
}
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
}
type ServerOption func (*Server ) error
func WithDebug (w io .Writer ) ServerOption {
return func (s *Server ) error {
s .debugStream = w
return nil
}
}
func ReadOnly () ServerOption {
return func (s *Server ) error {
s .readOnly = true
return nil
}
}
func WithAllocator () ServerOption {
return func (s *Server ) error {
alloc := newAllocator ()
s .pktMgr .alloc = alloc
s .conn .alloc = alloc
return nil
}
}
func WithServerWorkingDirectory (workDir string ) ServerOption {
return func (s *Server ) error {
s .workDir = cleanPath (workDir )
return nil
}
}
type rxPacket struct {
pktType fxp
pktBytes []byte
}
func (svr *Server ) sftpServerWorker (pktChan chan orderedRequest ) error {
for pkt := range pktChan {
readonly := true
switch pkt := pkt .requestPacket .(type ) {
case notReadOnly :
readonly = false
case *sshFxpOpenPacket :
readonly = pkt .readonly ()
case *sshFxpExtendedPacket :
readonly = pkt .readonly ()
}
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 :
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 :
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 :
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 ],
}
}
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
}
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 ()
}
}()
}
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 {
if err == io .EOF {
err = nil
}
break
}
pkt , err = makePacket (rxPacket {fxp (pktType ), pktBytes })
if err != nil {
switch {
case errors .Is (err , errUnknownExtendedPacket ):
default :
debug ("makePacket err: %v" , err )
svr .conn .Close ()
break
}
}
pktChan <- svr .pktMgr .newOrderedRequest (pkt )
}
close (pktChan )
wg .Wait ()
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
}
type ider interface {
id() uint32
}
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
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 {
return statusFromError (p .ID , syscall .EINVAL )
}
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 {
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 )
}
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 {
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
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .