649 lines
16 KiB
Go
649 lines
16 KiB
Go
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.Abs(p.Path)
|
|
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
|
|
}
|