Skip to content

Commit cde05dd

Browse files
authored
NLST: return paths relative to the working directory (#368)
NLST: return paths relative to the working directory Based on RFC 959 NLST is intended to return information that can be used by a program to further process the files automatically. Also use path.IsAbs/path.Join to build absolute paths
1 parent df4aba0 commit cde05dd

File tree

4 files changed

+120
-19
lines changed

4 files changed

+120
-19
lines changed

client_handler.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type clientHandler struct {
8787
reader *bufio.Reader // Reader on the TCP connection
8888
user string // Authenticated user
8989
path string // Current path
90+
listPath string // Path for NLST/LIST requests
9091
clnt string // Identified client
9192
command string // Command received on the connection
9293
connectedAt time.Time // Date of connection
@@ -151,6 +152,22 @@ func (c *clientHandler) SetPath(value string) {
151152
c.path = value
152153
}
153154

155+
// getListPath returns the path for the last LIST/NLST request
156+
func (c *clientHandler) getListPath() string {
157+
c.paramsMutex.RLock()
158+
defer c.paramsMutex.RUnlock()
159+
160+
return c.listPath
161+
}
162+
163+
// SetListPath changes the path for the last LIST/NLST request
164+
func (c *clientHandler) SetListPath(value string) {
165+
c.paramsMutex.Lock()
166+
defer c.paramsMutex.Unlock()
167+
168+
c.listPath = value
169+
}
170+
154171
// Debug defines if we will list all interaction
155172
func (c *clientHandler) Debug() bool {
156173
c.paramsMutex.RLock()

driver.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ type ClientContext interface {
141141
// Calling this method after the authentication step could lead to undefined behavior
142142
SetPath(value string)
143143

144+
// SetListPath allows to change the path for the last LIST/NLST request.
145+
// This method is useful if the driver expands wildcards and so the returned results
146+
// refer to a path different from the requested one.
147+
// The value must be cleaned using path.Clean
148+
SetListPath(value string)
149+
144150
// SetDebug activates the debugging of this connection commands
145151
SetDebug(debug bool)
146152

handle_dirs.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"io/fs"
78
"os"
89
"path"
910
"strings"
@@ -19,11 +20,42 @@ var errFileList = errors.New("listing a file isn't allowed")
1920
var supportedlistArgs = []string{"-al", "-la", "-a", "-l"}
2021

2122
func (c *clientHandler) absPath(p string) string {
22-
if strings.HasPrefix(p, "/") {
23+
if path.IsAbs(p) {
2324
return path.Clean(p)
2425
}
2526

26-
return path.Clean(c.Path() + "/" + p)
27+
return path.Join(c.Path(), p)
28+
}
29+
30+
// getRelativePath returns the specified path as relative to the
31+
// current working directory. The specified path must be cleaned
32+
func (c *clientHandler) getRelativePath(p string) string {
33+
var sb strings.Builder
34+
base := c.Path()
35+
36+
for {
37+
if base == p {
38+
return sb.String()
39+
}
40+
41+
if !strings.HasSuffix(base, "/") {
42+
base += "/"
43+
}
44+
45+
if strings.HasPrefix(p, base) {
46+
sb.WriteString(strings.TrimPrefix(p, base))
47+
48+
return sb.String()
49+
}
50+
51+
if base == "/" || base == "./" {
52+
return p
53+
}
54+
55+
sb.WriteString("../")
56+
57+
base = path.Dir(path.Clean(base))
58+
}
2759
}
2860

2961
func (c *clientHandler) handleCWD(param string) error {
@@ -156,7 +188,7 @@ func (c *clientHandler) checkLISTArgs(args string) string {
156188
func (c *clientHandler) handleLIST(param string) error {
157189
info := fmt.Sprintf("LIST %v", param)
158190

159-
if files, err := c.getFileList(param, true); err == nil || err == io.EOF {
191+
if files, _, err := c.getFileList(param, true); err == nil || err == io.EOF {
160192
if tr, errTr := c.TransferOpen(info); errTr == nil {
161193
err = c.dirTransferLIST(tr, files)
162194
c.TransferClose(err)
@@ -175,9 +207,9 @@ func (c *clientHandler) handleLIST(param string) error {
175207
func (c *clientHandler) handleNLST(param string) error {
176208
info := fmt.Sprintf("NLST %v", param)
177209

178-
if files, err := c.getFileList(param, true); err == nil || err == io.EOF {
210+
if files, parentDir, err := c.getFileList(param, true); err == nil || err == io.EOF {
179211
if tr, errTrOpen := c.TransferOpen(info); errTrOpen == nil {
180-
err = c.dirTransferNLST(tr, files)
212+
err = c.dirTransferNLST(tr, files, parentDir)
181213
c.TransferClose(err)
182214

183215
return nil
@@ -191,15 +223,18 @@ func (c *clientHandler) handleNLST(param string) error {
191223
return nil
192224
}
193225

194-
func (c *clientHandler) dirTransferNLST(w io.Writer, files []os.FileInfo) error {
226+
func (c *clientHandler) dirTransferNLST(w io.Writer, files []os.FileInfo, parentDir string) error {
195227
if len(files) == 0 {
196228
_, err := w.Write([]byte(""))
197229

198230
return err
199231
}
200232

201233
for _, file := range files {
202-
if _, err := fmt.Fprintf(w, "%s\r\n", file.Name()); err != nil {
234+
// Based on RFC 959 NLST is intended to return information that can be used
235+
// by a program to further process the files automatically.
236+
// So we return paths relative to the current working directory
237+
if _, err := fmt.Fprintf(w, "%s\r\n", path.Join(c.getRelativePath(parentDir), file.Name())); err != nil {
203238
return err
204239
}
205240
}
@@ -216,7 +251,7 @@ func (c *clientHandler) handleMLSD(param string) error {
216251

217252
info := fmt.Sprintf("MLSD %v", param)
218253

219-
if files, err := c.getFileList(param, false); err == nil || err == io.EOF {
254+
if files, _, err := c.getFileList(param, false); err == nil || err == io.EOF {
220255
if tr, errTr := c.TransferOpen(info); errTr == nil {
221256
err = c.dirTransferMLSD(tr, files)
222257
c.TransferClose(err)
@@ -312,39 +347,46 @@ func (c *clientHandler) writeMLSxEntry(w io.Writer, file os.FileInfo) error {
312347
return err
313348
}
314349

315-
func (c *clientHandler) getFileList(param string, filePathAllowed bool) ([]os.FileInfo, error) {
350+
func (c *clientHandler) getFileList(param string, filePathAllowed bool) ([]os.FileInfo, string, error) {
316351
if !c.server.settings.DisableLISTArgs {
317352
param = c.checkLISTArgs(param)
318353
}
319354
// directory or filePath
320355
listPath := c.absPath(param)
356+
c.SetListPath(listPath)
321357

322358
// return list of single file if directoryPath points to file and filePathAllowed
323359
info, err := c.driver.Stat(listPath)
324360
if err != nil {
325-
return nil, err
361+
return nil, "", err
326362
}
327363

328364
if !info.IsDir() {
329365
if filePathAllowed {
330-
return []os.FileInfo{info}, nil
366+
return []os.FileInfo{info}, path.Dir(c.getListPath()), nil
331367
}
332368

333-
return nil, errFileList
369+
return nil, "", errFileList
334370
}
335371

372+
var files []fs.FileInfo
373+
336374
if fileList, ok := c.driver.(ClientDriverExtensionFileList); ok {
337-
return fileList.ReadDir(listPath)
375+
files, err = fileList.ReadDir(listPath)
376+
377+
return files, c.getListPath(), err
338378
}
339379

340380
directory, errOpenFile := c.driver.Open(listPath)
341381
if errOpenFile != nil {
342-
return nil, errOpenFile
382+
return nil, "", errOpenFile
343383
}
344384

345385
defer c.closeDirectory(listPath, directory)
346386

347-
return directory.Readdir(-1)
387+
files, err = directory.Readdir(-1)
388+
389+
return files, c.getListPath(), err
348390
}
349391

350392
func (c *clientHandler) closeDirectory(directoryPath string, directory afero.File) {

handle_dirs_test.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ftpserver
33
import (
44
"crypto/tls"
55
"fmt"
6+
"io"
67
"net"
78
"path"
89
"testing"
@@ -14,6 +15,29 @@ import (
1415

1516
const DirKnown = "known"
1617

18+
func TestGetRelativePaths(t *testing.T) {
19+
type relativePathTest struct {
20+
workingDir, path, result string
21+
}
22+
var tests = []relativePathTest{
23+
{"/", "/p", "p"},
24+
{"/", "/", ""},
25+
{"/p", "/p", ""},
26+
{"/p", "/p1", "../p1"},
27+
{"/p", "/p/p1", "p1"},
28+
{"/p/p1", "/p/p2/p3", "../p2/p3"},
29+
{"/", "p", "p"},
30+
}
31+
32+
handler := clientHandler{}
33+
34+
for _, test := range tests {
35+
handler.SetPath(test.workingDir)
36+
result := handler.getRelativePath(test.path)
37+
require.Equal(t, test.result, result)
38+
}
39+
}
40+
1741
func TestDirListing(t *testing.T) {
1842
// MLSD is disabled we relies on LIST of files listing
1943
s := NewTestServerWithDriver(t, &TestServerDriver{Debug: false, Settings: &Settings{DisableMLSD: true}})
@@ -278,13 +302,19 @@ func TestDirListingWithSpace(t *testing.T) {
278302
require.Equal(t, StatusFileOK, rc)
279303
require.Equal(t, fmt.Sprintf("CD worked on /%s", dirName), response)
280304

281-
_, err = raw.PrepareDataConn()
305+
dcGetter, err := raw.PrepareDataConn()
282306
require.NoError(t, err)
283307

284308
rc, response, err = raw.SendCommand("NLST /")
285309
require.NoError(t, err)
286310
require.Equal(t, StatusFileStatusOK, rc, response)
287311

312+
dc, err := dcGetter()
313+
require.NoError(t, err)
314+
resp, err := io.ReadAll(dc)
315+
require.NoError(t, err)
316+
require.Equal(t, "../"+dirName+"\r\n", string(resp))
317+
288318
rc, _, err = raw.ReadResponse()
289319
require.NoError(t, err)
290320
require.Equal(t, StatusClosingDataConn, rc)
@@ -561,16 +591,22 @@ func TestMLSDAndNLSTFilePathError(t *testing.T) {
561591
_, err = c.ReadDir(fileName)
562592
require.Error(t, err, "MLSD is enabled, MLSD for filePath must fail")
563593

564-
// NLST shouldn't work for filePath
594+
// NLST should work for filePath
565595
raw, err := c.OpenRawConn()
566596
require.NoError(t, err, "Couldn't open raw connection")
567597

568598
defer func() { require.NoError(t, raw.Close()) }()
569599

570-
_, err = raw.PrepareDataConn()
600+
dcGetter, err := raw.PrepareDataConn()
571601
require.NoError(t, err)
572602

573-
rc, response, err := raw.SendCommand("NLST /" + fileName)
603+
rc, response, err := raw.SendCommand("NLST /../" + fileName)
574604
require.NoError(t, err)
575605
require.Equal(t, StatusFileStatusOK, rc, response)
606+
607+
dc, err := dcGetter()
608+
require.NoError(t, err)
609+
resp, err := io.ReadAll(dc)
610+
require.NoError(t, err)
611+
require.Equal(t, fileName+"\r\n", string(resp))
576612
}

0 commit comments

Comments
 (0)