Merge pull request #194 from pkg/move-handle-ptr-request
Move Request.handle to *Request
diff --git a/packet-manager.go b/packet-manager.go
index 367d75e..906b3ee 100644
--- a/packet-manager.go
+++ b/packet-manager.go
@@ -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-server.go b/request-server.go
index b0cad92..0fc3f92 100644
--- a/request-server.go
+++ b/request-server.go
@@ -3,9 +3,9 @@
import (
"encoding"
"io"
- "os"
"path/filepath"
"strconv"
+ "strings"
"sync"
"syscall"
@@ -130,7 +130,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}
@@ -180,28 +180,30 @@
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 cleanPath(path string) string {
+ cleanSlashPath := filepath.ToSlash(filepath.Clean(path))
+ if !strings.HasPrefix(cleanSlashPath, "/") {
+ return "/" + cleanSlashPath
+ }
+ return cleanSlashPath
+}
+
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)
}
return rpkt
@@ -220,12 +222,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 3fa438e..b7a75a4 100644
--- a/request.go
+++ b/request.go
@@ -56,16 +56,16 @@
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)}
+ request := Request{Method: method, Filepath: cleanPath(path)}
request.packets = make(chan packet_data, SftpServerWorkerCount)
request.state = &state{}
request.stateLock = &sync.RWMutex{}
@@ -268,7 +268,7 @@
switch r.Method {
case "List":
pd := r.popPacket()
- dirname := path.Base(r.Filepath)
+ dirname := filepath.ToSlash(path.Base(r.Filepath))
ret := &sshFxpNamePacket{ID: pd.id}
for _, fi := range finfo {
diff --git a/request_test.go b/request_test.go
index 078f1dc..74150b4 100644
--- a/request_test.go
+++ b/request_test.go
@@ -67,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
diff --git a/server.go b/server.go
index 13fd7ff..41b98e8 100644
--- a/server.go
+++ b/server.go
@@ -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())