Merge branch 'master' into master
diff --git a/example_test.go b/example_test.go
index c6c9009..483f68f 100644
--- a/example_test.go
+++ b/example_test.go
@@ -104,14 +104,26 @@
}
defer client.Close()
- ssh_fx_failure := uint32(4)
+ sshFxFailure := uint32(4)
mkdirParents := func(client *sftp.Client, dir string) (err error) {
var parents string
+
+ if path.IsAbs(dir) {
+ // Otherwise, an absolute path given below would be turned in to a relative one
+ // by splitting on "/"
+ parents = "/"
+ }
+
for _, name := range strings.Split(dir, "/") {
+ if name == "" {
+ // Paths with double-/ in them should just move along
+ // this will also catch the case of the first character being a "/", i.e. an absolute path
+ continue
+ }
parents = path.Join(parents, name)
err = client.Mkdir(parents)
if status, ok := err.(*sftp.StatusError); ok {
- if status.Code == ssh_fx_failure {
+ if status.Code == sshFxFailure {
var fi os.FileInfo
fi, err = client.Stat(parents)
if err == nil {
diff --git a/packet-manager.go b/packet-manager.go
index 367d75e..8c6691c 100644
--- a/packet-manager.go
+++ b/packet-manager.go
@@ -23,8 +23,8 @@
working *sync.WaitGroup
}
-func newPktMgr(sender packetSender) packetManager {
- s := packetManager{
+func newPktMgr(sender packetSender) *packetManager {
+ s := &packetManager{
requests: make(chan requestPacket, SftpServerWorkerCount),
responses: make(chan responsePacket, SftpServerWorkerCount),
fini: make(chan struct{}),
@@ -39,19 +39,19 @@
// register incoming packets to be handled
// send id of 0 for packets without id
-func (s packetManager) incomingPacket(pkt requestPacket) {
+func (s *packetManager) incomingPacket(pkt requestPacket) {
s.working.Add(1)
s.requests <- pkt // buffer == SftpServerWorkerCount
}
// register outgoing packets as being ready
-func (s packetManager) readyPacket(pkt responsePacket) {
+func (s *packetManager) readyPacket(pkt responsePacket) {
s.responses <- pkt
s.working.Done()
}
// shut down packetManager controller
-func (s packetManager) close() {
+func (s *packetManager) close() {
// pause until current packets are processed
s.working.Wait()
close(s.fini)
@@ -147,10 +147,10 @@
}
}
-func outfilter(o []responsePacket) []uint32 {
- res := make([]uint32, 0, len(o))
- for _, v := range o {
- res = append(res, v.id())
- }
- return res
-}
+//func outfilter(o []responsePacket) []uint32 {
+// res := make([]uint32, 0, len(o))
+// for _, v := range o {
+// res = append(res, v.id())
+// }
+// return res
+//}
diff --git a/request-example.go b/request-example.go
index c355fbd..f66b060 100644
--- a/request-example.go
+++ b/request-example.go
@@ -25,7 +25,7 @@
}
// Handlers
-func (fs *root) Fileread(r Request) (io.ReaderAt, error) {
+func (fs *root) Fileread(r *Request) (io.ReaderAt, error) {
fs.filesLock.Lock()
defer fs.filesLock.Unlock()
file, err := fs.fetch(r.Filepath)
@@ -41,7 +41,7 @@
return file.ReaderAt()
}
-func (fs *root) Filewrite(r Request) (io.WriterAt, error) {
+func (fs *root) Filewrite(r *Request) (io.WriterAt, error) {
fs.filesLock.Lock()
defer fs.filesLock.Unlock()
file, err := fs.fetch(r.Filepath)
@@ -59,7 +59,7 @@
return file.WriterAt()
}
-func (fs *root) Filecmd(r Request) error {
+func (fs *root) Filecmd(r *Request) error {
fs.filesLock.Lock()
defer fs.filesLock.Unlock()
switch r.Method {
@@ -115,7 +115,7 @@
return n, nil
}
-func (fs *root) Filelist(r Request) (ListerAt, error) {
+func (fs *root) Filelist(r *Request) (ListerAt, error) {
fs.filesLock.Lock()
defer fs.filesLock.Unlock()
diff --git a/request-interfaces.go b/request-interfaces.go
index f7afb14..05a9dac 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -10,22 +10,22 @@
// FileReader should return an io.Reader for the filepath
type FileReader interface {
- Fileread(Request) (io.ReaderAt, error)
+ Fileread(*Request) (io.ReaderAt, error)
}
// FileWriter should return an io.Writer for the filepath
type FileWriter interface {
- Filewrite(Request) (io.WriterAt, error)
+ Filewrite(*Request) (io.WriterAt, error)
}
// FileCmder should return an error (rename, remove, setstate, etc.)
type FileCmder interface {
- Filecmd(Request) error
+ Filecmd(*Request) error
}
// FileLister should return file info interface and errors (readdir, stat)
type FileLister interface {
- Filelist(Request) (ListerAt, error)
+ Filelist(*Request) (ListerAt, error)
}
// ListerAt does for file lists what io.ReaderAt does for files.
diff --git a/request-server.go b/request-server.go
index b0cad92..73899fa 100644
--- a/request-server.go
+++ b/request-server.go
@@ -3,9 +3,9 @@
import (
"encoding"
"io"
- "os"
"path/filepath"
"strconv"
+ "strings"
"sync"
"syscall"
@@ -28,7 +28,7 @@
type RequestServer struct {
*serverConn
Handlers Handlers
- pktMgr packetManager
+ pktMgr *packetManager
openRequests map[string]Request
openRequestLock sync.RWMutex
handleCount int
@@ -51,20 +51,22 @@
}
}
-func (rs *RequestServer) nextRequest(r Request) string {
+// Note that we are explicitly saving the Request as a value.
+func (rs *RequestServer) nextRequest(r *Request) string {
rs.openRequestLock.Lock()
defer rs.openRequestLock.Unlock()
rs.handleCount++
handle := strconv.Itoa(rs.handleCount)
- rs.openRequests[handle] = r
+ rs.openRequests[handle] = *r
return handle
}
-func (rs *RequestServer) getRequest(handle string) (Request, bool) {
+// Returns pointer to new copy of Request object
+func (rs *RequestServer) getRequest(handle string) (*Request, bool) {
rs.openRequestLock.RLock()
defer rs.openRequestLock.RUnlock()
r, ok := rs.openRequests[handle]
- return r, ok
+ return &r, ok
}
func (rs *RequestServer) closeRequest(handle string) {
@@ -130,7 +132,7 @@
rs.closeRequest(handle)
rpkt = statusFromError(pkt, nil)
case *sshFxpRealpathPacket:
- rpkt = cleanPath(pkt)
+ rpkt = cleanPacketPath(pkt)
case isOpener:
handle := rs.nextRequest(requestFromPacket(pkt))
rpkt = sshFxpHandlePacket{pkt.id(), handle}
@@ -142,7 +144,7 @@
} else {
request = requestFromPacket(
&sshFxpStatPacket{ID: pkt.id(), Path: request.Filepath})
- rpkt = rs.handle(request, pkt)
+ rpkt = request.handle(rs.Handlers)
}
case *sshFxpFsetstatPacket:
handle := pkt.getHandle()
@@ -154,7 +156,7 @@
&sshFxpSetstatPacket{ID: pkt.id(), Path: request.Filepath,
Flags: pkt.Flags, Attrs: pkt.Attrs,
})
- rpkt = rs.handle(request, pkt)
+ rpkt = request.handle(rs.Handlers)
}
case hasHandle:
handle := pkt.getHandle()
@@ -163,11 +165,11 @@
if !ok {
rpkt = statusFromError(pkt, syscall.EBADF)
} else {
- rpkt = rs.handle(request, pkt)
+ rpkt = request.handle(rs.Handlers)
}
case hasPath:
request := requestFromPacket(pkt)
- rpkt = rs.handle(request, pkt)
+ rpkt = request.handle(rs.Handlers)
default:
return errors.Errorf("unexpected packet type %T", pkt)
}
@@ -180,31 +182,24 @@
return nil
}
-func cleanPath(pkt *sshFxpRealpathPacket) responsePacket {
- path := pkt.getPath()
- if !filepath.IsAbs(path) {
- path = "/" + path
- } // all paths are absolute
-
- cleaned_path := filepath.Clean(path)
+func cleanPacketPath(pkt *sshFxpRealpathPacket) responsePacket {
+ path := cleanPath(pkt.getPath())
return &sshFxpNamePacket{
ID: pkt.id(),
NameAttrs: []sshFxpNameAttr{{
- Name: cleaned_path,
- LongName: cleaned_path,
+ Name: path,
+ LongName: path,
Attrs: emptyFileStat,
}},
}
}
-func (rs *RequestServer) handle(request Request, pkt requestPacket) responsePacket {
- // fmt.Println("Request Method: ", request.Method)
- rpkt, err := request.handle(rs.Handlers)
- if err != nil {
- err = errorAdapter(err)
- rpkt = statusFromError(pkt, err)
+func cleanPath(path string) string {
+ cleanSlashPath := filepath.ToSlash(filepath.Clean(path))
+ if !strings.HasPrefix(cleanSlashPath, "/") {
+ return "/" + cleanSlashPath
}
- return rpkt
+ return cleanSlashPath
}
// Wrap underlying connection methods to use packetManager
@@ -220,12 +215,3 @@
func (rs *RequestServer) sendError(p ider, err error) error {
return rs.sendPacket(statusFromError(p, err))
}
-
-// os.ErrNotExist should convert to ssh_FX_NO_SUCH_FILE, but is not recognized
-// by statusFromError. So we convert to syscall.ENOENT which it does.
-func errorAdapter(err error) error {
- if err == os.ErrNotExist {
- return syscall.ENOENT
- }
- return err
-}
diff --git a/request-server_test.go b/request-server_test.go
index 60aed2a..0c25ecf 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -328,3 +328,20 @@
names := []string{di[18].Name(), di[81].Name()}
assert.Equal(t, []string{"foo_18", "foo_81"}, names)
}
+
+func TestCleanPath(t *testing.T) {
+ assert.Equal(t, "/", cleanPath("/"))
+ assert.Equal(t, "/", cleanPath("//"))
+ assert.Equal(t, "/a", cleanPath("/a/"))
+ assert.Equal(t, "/a", cleanPath("a/"))
+ assert.Equal(t, "/a/b/c", cleanPath("/a//b//c/"))
+
+ // filepath.ToSlash does not touch \ as char on unix systems, so os.PathSeparator is used for windows compatible tests
+ bslash := string(os.PathSeparator)
+ assert.Equal(t, "/", cleanPath(bslash))
+ assert.Equal(t, "/", cleanPath(bslash+bslash))
+ assert.Equal(t, "/a", cleanPath(bslash+"a"+bslash))
+ assert.Equal(t, "/a", cleanPath("a"+bslash))
+ assert.Equal(t, "/a/b/c", cleanPath(bslash+"a"+bslash+bslash+"b"+bslash+bslash+"c"+bslash))
+
+}
diff --git a/request.go b/request.go
index e6bdf13..c8af66f 100644
--- a/request.go
+++ b/request.go
@@ -40,14 +40,18 @@
}
type packet_data struct {
- id uint32
+ _id uint32
data []byte
length uint32
offset int64
}
+func (pd packet_data) id() uint32 {
+ return pd._id
+}
+
// New Request initialized based on packet data
-func requestFromPacket(pkt hasPath) Request {
+func requestFromPacket(pkt hasPath) *Request {
method := requestMethod(pkt)
request := NewRequest(method, pkt.getPath())
request.pkt_id = pkt.id()
@@ -56,38 +60,42 @@
request.Flags = p.Flags
request.Attrs = p.Attrs.([]byte)
case *sshFxpRenamePacket:
- request.Target = filepath.Clean(p.Newpath)
+ request.Target = cleanPath(p.Newpath)
case *sshFxpSymlinkPacket:
- request.Target = filepath.Clean(p.Linkpath)
+ request.Target = cleanPath(p.Linkpath)
}
return request
}
// NewRequest creates a new Request object.
-func NewRequest(method, path string) Request {
- request := Request{Method: method, Filepath: filepath.Clean(path)}
+func NewRequest(method, path string) *Request {
+ request := &Request{Method: method, Filepath: cleanPath(path)}
request.packets = make(chan packet_data, SftpServerWorkerCount)
request.state = &state{}
request.stateLock = &sync.RWMutex{}
return request
}
+func (r *Request) id() uint32 {
+ return r.pkt_id
+}
+
// Returns current offset for file list
-func (r Request) lsNext() int64 {
+func (r *Request) lsNext() int64 {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
return r.state.lsoffset
}
// Increases next offset
-func (r Request) lsInc(offset int64) {
+func (r *Request) lsInc(offset int64) {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
r.state.lsoffset = r.state.lsoffset + offset
}
// manage file read/write state
-func (r Request) setFileState(s interface{}) {
+func (r *Request) setFileState(s interface{}) {
r.stateLock.Lock()
defer r.stateLock.Unlock()
switch s := s.(type) {
@@ -102,19 +110,19 @@
}
}
-func (r Request) getWriter() io.WriterAt {
+func (r *Request) getWriter() io.WriterAt {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
return r.state.writerAt
}
-func (r Request) getReader() io.ReaderAt {
+func (r *Request) getReader() io.ReaderAt {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
return r.state.readerAt
}
-func (r Request) getLister() ListerAt {
+func (r *Request) getLister() ListerAt {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
return r.state.listerAt
@@ -122,20 +130,20 @@
// For backwards compatibility. The Handler didn't have batch handling at
// first, and just always assumed 1 batch. This preserves that behavior.
-func (r Request) setEOD(eod bool) {
+func (r *Request) setEOD(eod bool) {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
r.state.endofdir = eod
}
-func (r Request) getEOD() bool {
+func (r *Request) getEOD() bool {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
return r.state.endofdir
}
// Close reader/writer if possible
-func (r Request) close() {
+func (r *Request) close() {
rd := r.getReader()
if c, ok := rd.(io.Closer); ok {
c.Close()
@@ -147,7 +155,7 @@
}
// push packet_data into fifo
-func (r Request) pushPacket(pd packet_data) {
+func (r *Request) pushPacket(pd packet_data) {
r.packets <- pd
}
@@ -157,58 +165,56 @@
}
// called from worker to handle packet/request
-func (r Request) handle(handlers Handlers) (responsePacket, error) {
- var err error
- var rpkt responsePacket
+func (r *Request) handle(handlers Handlers) responsePacket {
switch r.Method {
case "Get":
- rpkt, err = fileget(handlers.FileGet, r)
+ return fileget(handlers.FileGet, r)
case "Put": // add "Append" to this to handle append only file writes
- rpkt, err = fileput(handlers.FilePut, r)
+ return fileput(handlers.FilePut, r)
case "Setstat", "Rename", "Rmdir", "Mkdir", "Symlink", "Remove":
- rpkt, err = filecmd(handlers.FileCmd, r)
+ return filecmd(handlers.FileCmd, r)
case "List", "Stat", "Readlink":
- rpkt, err = filelist(handlers.FileList, r)
+ return filelist(handlers.FileList, r)
default:
- return rpkt, errors.Errorf("unexpected method: %s", r.Method)
+ return statusFromError(r,
+ errors.Errorf("unexpected method: %s", r.Method))
}
- return rpkt, err
}
// wrap FileReader handler
-func fileget(h FileReader, r Request) (responsePacket, error) {
+func fileget(h FileReader, r *Request) responsePacket {
var err error
reader := r.getReader()
+ pd := r.popPacket()
if reader == nil {
reader, err = h.Fileread(r)
if err != nil {
- return nil, err
+ return statusFromError(pd, err)
}
r.setFileState(reader)
}
- pd := r.popPacket()
data := make([]byte, clamp(pd.length, maxTxPacket))
n, err := reader.ReadAt(data, pd.offset)
// only return EOF erro if no data left to read
if err != nil && (err != io.EOF || n == 0) {
- return nil, err
+ return statusFromError(pd, err)
}
return &sshFxpDataPacket{
- ID: pd.id,
+ ID: pd.id(),
Length: uint32(n),
Data: data[:n],
- }, nil
+ }
}
// wrap FileWriter handler
-func fileput(h FileWriter, r Request) (responsePacket, error) {
+func fileput(h FileWriter, r *Request) responsePacket {
var err error
writer := r.getWriter()
if writer == nil {
writer, err = h.Filewrite(r)
if err != nil {
- return nil, err
+ return statusFromError(r, err)
}
r.setFileState(writer)
}
@@ -216,36 +222,36 @@
pd := r.popPacket()
_, err = writer.WriteAt(pd.data, pd.offset)
if err != nil {
- return nil, err
+ return statusFromError(pd, err)
}
return &sshFxpStatusPacket{
- ID: pd.id,
+ ID: pd.id(),
StatusError: StatusError{
Code: ssh_FX_OK,
- }}, nil
+ }}
}
// wrap FileCmder handler
-func filecmd(h FileCmder, r Request) (responsePacket, error) {
+func filecmd(h FileCmder, r *Request) responsePacket {
err := h.Filecmd(r)
if err != nil {
- return nil, err
+ return statusFromError(r, err)
}
return &sshFxpStatusPacket{
ID: r.pkt_id,
StatusError: StatusError{
Code: ssh_FX_OK,
- }}, nil
+ }}
}
// wrap FileLister handler
-func filelist(h FileLister, r Request) (responsePacket, error) {
+func filelist(h FileLister, r *Request) responsePacket {
var err error
lister := r.getLister()
if lister == nil {
lister, err = h.Filelist(r)
if err != nil {
- return nil, err
+ return statusFromError(r, err)
}
r.setFileState(lister)
}
@@ -255,28 +261,19 @@
n, err := lister.ListAt(finfo, offset)
r.lsInc(int64(n))
// ignore EOF as we only return it when there are no results
- if err != nil && err != io.EOF {
- return nil, err
- }
finfo = finfo[:n] // avoid need for nil tests below
- // no results
- if n == 0 {
- switch r.Method {
- case "List":
- return nil, io.EOF
- case "Stat", "Readlink":
- err = &os.PathError{Op: "readlink", Path: r.Filepath,
- Err: syscall.ENOENT}
- return nil, err
- }
- }
-
switch r.Method {
case "List":
pd := r.popPacket()
- dirname := path.Base(r.Filepath)
- ret := &sshFxpNamePacket{ID: pd.id}
+ if err != nil && err != io.EOF {
+ return statusFromError(pd, err)
+ }
+ if n == 0 {
+ return statusFromError(pd, io.EOF)
+ }
+ dirname := filepath.ToSlash(path.Base(r.Filepath))
+ ret := &sshFxpNamePacket{ID: pd.id()}
for _, fi := range finfo {
ret.NameAttrs = append(ret.NameAttrs, sshFxpNameAttr{
@@ -285,13 +282,29 @@
Attrs: []interface{}{fi},
})
}
- return ret, nil
+ return ret
case "Stat":
+ if err != nil && err != io.EOF {
+ return statusFromError(r, err)
+ }
+ if n == 0 {
+ err = &os.PathError{Op: "stat", Path: r.Filepath,
+ Err: syscall.ENOENT}
+ return statusFromError(r, err)
+ }
return &sshFxpStatResponse{
ID: r.pkt_id,
info: finfo[0],
- }, nil
+ }
case "Readlink":
+ if err != nil && err != io.EOF {
+ return statusFromError(r, err)
+ }
+ if n == 0 {
+ err = &os.PathError{Op: "readlink", Path: r.Filepath,
+ Err: syscall.ENOENT}
+ return statusFromError(r, err)
+ }
filename := finfo[0].Name()
return &sshFxpNamePacket{
ID: r.pkt_id,
@@ -300,15 +313,16 @@
LongName: filename,
Attrs: emptyFileStat,
}},
- }, nil
+ }
default:
- return nil, errors.Errorf("unexpected method: %s", r.Method)
+ err = errors.Errorf("unexpected method: %s", r.Method)
+ return statusFromError(r, err)
}
}
// file data for additional read/write packets
func (r *Request) update(p hasHandle) error {
- pd := packet_data{id: p.id()}
+ pd := packet_data{_id: p.id()}
switch p := p.(type) {
case *sshFxpReadPacket:
r.Method = "Get"
diff --git a/request_test.go b/request_test.go
index efcedcd..aff257e 100644
--- a/request_test.go
+++ b/request_test.go
@@ -18,28 +18,25 @@
err error // dummy error, should be file related
}
-func (t *testHandler) Fileread(r Request) (io.ReaderAt, error) {
+func (t *testHandler) Fileread(r *Request) (io.ReaderAt, error) {
if t.err != nil {
return nil, t.err
}
return bytes.NewReader(t.filecontents), nil
}
-func (t *testHandler) Filewrite(r Request) (io.WriterAt, error) {
+func (t *testHandler) Filewrite(r *Request) (io.WriterAt, error) {
if t.err != nil {
return nil, t.err
}
return io.WriterAt(t.output), nil
}
-func (t *testHandler) Filecmd(r Request) error {
- if t.err != nil {
- return t.err
- }
- return nil
+func (t *testHandler) Filecmd(r *Request) error {
+ return t.err
}
-func (t *testHandler) Filelist(r Request) (ListerAt, error) {
+func (t *testHandler) Filelist(r *Request) (ListerAt, error) {
if t.err != nil {
return nil, t.err
}
@@ -59,8 +56,8 @@
var filecontents = []byte("file-data.")
-func testRequest(method string) Request {
- request := Request{
+func testRequest(method string) *Request {
+ request := &Request{
Filepath: "./request_test.go",
Method: method,
Attrs: []byte("foo"),
@@ -70,8 +67,8 @@
stateLock: &sync.RWMutex{},
}
for _, p := range []packet_data{
- packet_data{id: 1, data: filecontents[:5], length: 5},
- packet_data{id: 2, data: filecontents[5:], length: 5, offset: 5}} {
+ {_id: 1, data: filecontents[:5], length: 5},
+ {_id: 2, data: filecontents[5:], length: 5, offset: 5}} {
request.packets <- p
}
return request
@@ -125,8 +122,7 @@
request := testRequest("Get")
// req.length is 5, so we test reads in 5 byte chunks
for i, txt := range []string{"file-", "data."} {
- pkt, err := request.handle(handlers)
- assert.Nil(t, err)
+ pkt := request.handle(handlers)
dpkt := pkt.(*sshFxpDataPacket)
assert.Equal(t, dpkt.id(), uint32(i+1))
assert.Equal(t, string(dpkt.Data), txt)
@@ -136,11 +132,9 @@
func TestRequestPut(t *testing.T) {
handlers := newTestHandlers()
request := testRequest("Put")
- pkt, err := request.handle(handlers)
- assert.Nil(t, err)
+ pkt := request.handle(handlers)
statusOk(t, pkt)
- pkt, err = request.handle(handlers)
- assert.Nil(t, err)
+ pkt = request.handle(handlers)
statusOk(t, pkt)
assert.Equal(t, "file-data.", handlers.getOutString())
}
@@ -148,14 +142,12 @@
func TestRequestCmdr(t *testing.T) {
handlers := newTestHandlers()
request := testRequest("Mkdir")
- pkt, err := request.handle(handlers)
- assert.Nil(t, err)
+ pkt := request.handle(handlers)
statusOk(t, pkt)
handlers.returnError()
- pkt, err = request.handle(handlers)
- assert.Nil(t, pkt)
- assert.Equal(t, err, errTest)
+ pkt = request.handle(handlers)
+ assert.Equal(t, pkt, statusFromError(pkt, errTest))
}
func TestRequestInfoList(t *testing.T) { testInfoMethod(t, "List") }
@@ -163,8 +155,7 @@
func TestRequestInfoStat(t *testing.T) {
handlers := newTestHandlers()
request := testRequest("Stat")
- pkt, err := request.handle(handlers)
- assert.Nil(t, err)
+ pkt := request.handle(handlers)
spkt, ok := pkt.(*sshFxpStatResponse)
assert.True(t, ok)
assert.Equal(t, spkt.info.Name(), "request_test.go")
@@ -173,8 +164,7 @@
func testInfoMethod(t *testing.T, method string) {
handlers := newTestHandlers()
request := testRequest(method)
- pkt, err := request.handle(handlers)
- assert.Nil(t, err)
+ pkt := request.handle(handlers)
npkt, ok := pkt.(*sshFxpNamePacket)
assert.True(t, ok)
assert.IsType(t, sshFxpNameAttr{}, npkt.NameAttrs[0])
diff --git a/server.go b/server.go
index 13fd7ff..be14f62 100644
--- a/server.go
+++ b/server.go
@@ -29,7 +29,7 @@
*serverConn
debugStream io.Writer
readOnly bool
- pktMgr packetManager
+ pktMgr *packetManager
openFiles map[string]*os.File
openFilesLock sync.RWMutex
handleCount int
@@ -230,8 +230,7 @@
if err != nil {
return s.sendError(p, err)
}
- f = filepath.Clean(f)
- f = filepath.ToSlash(f) // make path more Unix like on windows servers
+ f = cleanPath(f)
return s.sendPacket(sshFxpNamePacket{
ID: p.ID,
NameAttrs: []sshFxpNameAttr{{
@@ -555,6 +554,8 @@
ret.StatusError.msg = err.Error()
if err == io.EOF {
ret.StatusError.Code = ssh_FX_EOF
+ } else if err == os.ErrNotExist {
+ ret.StatusError.Code = ssh_FX_NO_SUCH_FILE
} else if errno, ok := err.(syscall.Errno); ok {
ret.StatusError.Code = translateErrno(errno)
} else if pathError, ok := err.(*os.PathError); ok {
@@ -573,3 +574,92 @@
}
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)
+}
diff --git a/server_stubs.go b/server_stubs.go
index 3b1ddbd..a14c734 100644
--- a/server_stubs.go
+++ b/server_stubs.go
@@ -4,9 +4,29 @@
import (
"os"
- "path"
+ "time"
+ "fmt"
)
func runLs(dirname string, dirent os.FileInfo) string {
- return path.Join(dirname, dirent.Name())
+ typeword := runLsTypeWord(dirent)
+ numLinks := 1
+ if dirent.IsDir() {
+ numLinks = 0
+ }
+ username := "root"
+ groupname := "root"
+ mtime := dirent.ModTime()
+ monthStr := mtime.Month().String()[0:3]
+ day := mtime.Day()
+ year := mtime.Year()
+ now := time.Now()
+ isOld := mtime.Before(now.Add(-time.Hour * 24 * 365 / 2))
+
+ yearOrTime := fmt.Sprintf("%02d:%02d", mtime.Hour(), mtime.Minute())
+ if isOld {
+ yearOrTime = fmt.Sprintf("%d", year)
+ }
+
+ return fmt.Sprintf("%s %4d %-8s %-8s %8d %s %2d %5s %s", typeword, numLinks, username, groupname, dirent.Size(), monthStr, day, yearOrTime, dirent.Name())
}
diff --git a/server_test.go b/server_test.go
index 721acc6..f2f034a 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1,11 +1,159 @@
package sftp
import (
+ "testing"
+ "os"
+ "regexp"
+ "time"
"io"
"sync"
- "testing"
)
+const (
+ typeDirectory = "d"
+ typeFile = "[^d]"
+)
+
+func TestRunLsWithExamplesDirectory(t *testing.T) {
+ path := "examples"
+ item, _ := os.Stat(path)
+ result := runLs(path, item)
+ runLsTestHelper(t, result, typeDirectory, path)
+}
+
+func TestRunLsWithLicensesFile(t *testing.T) {
+ path := "LICENSE"
+ item, _ := os.Stat(path)
+ result := runLs(path, item)
+ runLsTestHelper(t, result, typeFile, path)
+}
+
+/*
+ The format of the `longname' field is unspecified by this protocol.
+ It MUST be suitable for use in the output of a directory listing
+ command (in fact, the recommended operation for a directory listing
+ command is to simply display this data). However, clients SHOULD NOT
+ attempt to parse the longname field for file attributes; they SHOULD
+ use the attrs field instead.
+
+ The recommended format for the longname field is as follows:
+
+ -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
+ 1234567890 123 12345678 12345678 12345678 123456789012
+
+ Here, the first line is sample output, and the second field indicates
+ widths of the various fields. Fields are separated by spaces. The
+ first field lists file permissions for user, group, and others; the
+ second field is link count; the third field is the name of the user
+ who owns the file; the fourth field is the name of the group that
+ owns the file; the fifth field is the size of the file in bytes; the
+ sixth field (which actually may contain spaces, but is fixed to 12
+ characters) is the file modification time, and the seventh field is
+ the file name. Each field is specified to be a minimum of certain
+ number of character positions (indicated by the second line above),
+ but may also be longer if the data does not fit in the specified
+ length.
+
+ The SSH_FXP_ATTRS response has the following format:
+
+ uint32 id
+ ATTRS attrs
+
+ where `id' is the request identifier, and `attrs' is the returned
+ file attributes as described in Section ``File Attributes''.
+ */
+func runLsTestHelper(t *testing.T, result, expectedType, path string) {
+ // using regular expressions to make tests work on all systems
+ // a virtual file system (like afero) would be needed to mock valid filesystem checks
+ // expected layout is:
+ // drwxr-xr-x 8 501 20 272 Aug 9 19:46 examples
+
+ // permissions (len 10, "drwxr-xr-x")
+ got := result[0:10]
+ if ok, err := regexp.MatchString("^"+expectedType+"[rwx-]{9}$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): permission field mismatch, expected dir, got: %#v, err: %#v", path, got, err)
+ }
+
+ // space
+ got = result[10:11]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 1 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // link count (len 3, number)
+ got = result[12:15]
+ if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): link count field mismatch, got: %#v, err: %#v", path, got, err)
+ }
+
+ // spacer
+ got = result[15:16]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 2 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // username / uid (len 8, number or string)
+ got = result[16:24]
+ if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): username / uid mismatch, expected user, got: %#v, err: %#v", path, got, err)
+ }
+
+ // spacer
+ got = result[24:25]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 3 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // groupname / gid (len 8, number or string)
+ got = result[25:33]
+ if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): groupname / gid mismatch, expected group, got: %#v, err: %#v", path, got, err)
+ }
+
+ // spacer
+ got = result[33:34]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 4 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // filesize (len 8)
+ got = result[34:42]
+ if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", path, got, err)
+ }
+
+ // spacer
+ got = result[42:43]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 5 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // mod time (len 12, e.g. Aug 9 19:46)
+ got = result[43:55]
+ layout := "Jan 2 15:04"
+ _, err := time.Parse(layout, got)
+
+ if err != nil {
+ layout = "Jan 2 2006"
+ _, err = time.Parse(layout, got)
+ }
+ if err != nil {
+ t.Errorf("runLs(%#v, *FileInfo): mod time field mismatch, expected date layout %s, got: %#v, err: %#v", path, layout, got, err)
+ }
+
+ // spacer
+ got = result[55:56]
+ if ok, err := regexp.MatchString("^\\s$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): spacer 6 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
+ }
+
+ // filename
+ got = result[56:]
+ if ok, err := regexp.MatchString("^"+path+"$", got); !ok {
+ t.Errorf("runLs(%#v, *FileInfo): name field mismatch, expected examples, got: %#v, err: %#v", path, got, err)
+ }
+}
+
func clientServerPair(t *testing.T) (*Client, *Server) {
cr, sw := io.Pipe()
sr, cw := io.Pipe()
diff --git a/server_unix.go b/server_unix.go
index 8c3f0b4..8d0c138 100644
--- a/server_unix.go
+++ b/server_unix.go
@@ -11,96 +11,7 @@
"time"
)
-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)
-}
-
-func runLsStatt(dirname string, dirent os.FileInfo, statt *syscall.Stat_t) string {
+func runLsStatt(dirent os.FileInfo, statt *syscall.Stat_t) string {
// example from openssh sftp server:
// crw-rw-rw- 1 root wheel 0 Jul 31 20:52 ttyvd
// format:
@@ -136,7 +47,7 @@
if dsys == nil {
} else if statt, ok := dsys.(*syscall.Stat_t); !ok {
} else {
- return runLsStatt(dirname, dirent, statt)
+ return runLsStatt(dirent, statt)
}
return path.Join(dirname, dirent.Name())