Import upstream version 1.13.5+git20221123.1.707787e
Debian Janitor
1 year, 4 months ago
0 | name: CI | |
1 | ||
2 | on: | |
3 | push: | |
4 | branches: [master] | |
5 | pull_request: | |
6 | ||
7 | jobs: | |
8 | run-tests: | |
9 | name: Run test cases | |
10 | runs-on: ${{ matrix.os }} | |
11 | strategy: | |
12 | matrix: | |
13 | os: [ubuntu-latest, macos-latest] | |
14 | go: [1.17, 1.16] | |
15 | exclude: | |
16 | - os: macos-latest | |
17 | go: 1.16 | |
18 | ||
19 | steps: | |
20 | - uses: actions/checkout@v2 | |
21 | ||
22 | - name: Set up Go | |
23 | uses: actions/setup-go@v2 | |
24 | with: | |
25 | go-version: ${{ matrix.go }} | |
26 | ||
27 | - name: Run tests | |
28 | run: | | |
29 | make integration | |
30 | make integration_w_race | |
31 | ||
32 | - name: Run tests on 32-bit arch | |
33 | if: startsWith(matrix.os, 'ubuntu-') | |
34 | run: | | |
35 | make integration | |
36 | env: | |
37 | GOARCH: 386 |
0 | .*.swo | |
1 | .*.swp | |
2 | ||
3 | server_standalone/server_standalone | |
4 | ||
5 | examples/*/id_rsa | |
6 | examples/*/id_rsa.pub | |
7 | ||
8 | memprofile.out | |
9 | memprofile.svg |
3 | 3 | "bytes" |
4 | 4 | "encoding/binary" |
5 | 5 | "errors" |
6 | "fmt" | |
6 | 7 | "io" |
7 | 8 | "math" |
8 | 9 | "os" |
225 | 226 | |
226 | 227 | if err := sftp.sendInit(); err != nil { |
227 | 228 | wr.Close() |
228 | return nil, err | |
229 | } | |
229 | return nil, fmt.Errorf("error sending init packet to server: %w", err) | |
230 | } | |
231 | ||
230 | 232 | if err := sftp.recvVersion(); err != nil { |
231 | 233 | wr.Close() |
232 | return nil, err | |
234 | return nil, fmt.Errorf("error receiving version packet from server: %w", err) | |
233 | 235 | } |
234 | 236 | |
235 | 237 | sftp.clientConn.wg.Add(1) |
236 | go sftp.loop() | |
238 | go func() { | |
239 | defer sftp.clientConn.wg.Done() | |
240 | ||
241 | if err := sftp.clientConn.recv(); err != nil { | |
242 | sftp.clientConn.broadcastErr(err) | |
243 | } | |
244 | }() | |
237 | 245 | |
238 | 246 | return sftp, nil |
239 | 247 | } |
266 | 274 | func (c *Client) recvVersion() error { |
267 | 275 | typ, data, err := c.recvPacket(0) |
268 | 276 | if err != nil { |
277 | if err == io.EOF { | |
278 | return fmt.Errorf("server unexpectedly closed connection: %w", io.ErrUnexpectedEOF) | |
279 | } | |
280 | ||
269 | 281 | return err |
270 | 282 | } |
283 | ||
271 | 284 | if typ != sshFxpVersion { |
272 | 285 | return &unexpectedPacketErr{sshFxpVersion, typ} |
273 | 286 | } |
276 | 289 | if err != nil { |
277 | 290 | return err |
278 | 291 | } |
292 | ||
279 | 293 | if version != sftpProtocolVersion { |
280 | 294 | return &unexpectedVersionErr{sftpProtocolVersion, version} |
281 | 295 | } |
58 | 58 | func (c *clientConn) Close() error { |
59 | 59 | defer c.wg.Wait() |
60 | 60 | return c.conn.Close() |
61 | } | |
62 | ||
63 | func (c *clientConn) loop() { | |
64 | defer c.wg.Done() | |
65 | err := c.recv() | |
66 | if err != nil { | |
67 | c.broadcastErr(err) | |
68 | } | |
69 | 61 | } |
70 | 62 | |
71 | 63 | // recv continuously reads from the server and forwards responses to the |
3 | 3 | |
4 | 4 | require ( |
5 | 5 | github.com/kr/fs v0.1.0 |
6 | github.com/stretchr/testify v1.7.0 | |
7 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 | |
8 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect | |
6 | github.com/stretchr/testify v1.8.0 | |
7 | golang.org/x/crypto v0.1.0 | |
9 | 8 | ) |
0 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | |
1 | 0 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
2 | 3 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= |
3 | 4 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= |
4 | 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
5 | 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
6 | 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
7 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= | |
8 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
9 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= | |
10 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | |
11 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | |
8 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | |
9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
10 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= | |
11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | |
12 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |
13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
14 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | |
15 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= | |
16 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= | |
17 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | |
18 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
19 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | |
20 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | |
21 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= | |
22 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
23 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
12 | 25 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
13 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
14 | 26 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
15 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= | |
16 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
17 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= | |
27 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
28 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
29 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= | |
30 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
18 | 31 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
32 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | |
33 | golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= | |
34 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | |
35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
36 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
37 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | |
38 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | |
20 | 39 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
40 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
41 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | |
42 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
21 | 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
22 | 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | |
24 | 45 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
46 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
322 | 322 | // |
323 | 323 | // The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed. |
324 | 324 | // Unfortunately, the reversal was not noticed until the server was widely deployed. |
325 | // Covered in Section 3.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL | |
325 | // Covered in Section 4.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL | |
326 | 326 | type SymlinkPacket struct { |
327 | 327 | LinkPath string |
328 | 328 | TargetPath string |
521 | 521 | } |
522 | 522 | |
523 | 523 | type sshFxpSymlinkPacket struct { |
524 | ID uint32 | |
524 | ID uint32 | |
525 | ||
526 | // The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed. | |
527 | // Unfortunately, the reversal was not noticed until the server was widely deployed. | |
528 | // Covered in Section 4.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL | |
529 | ||
525 | 530 | Targetpath string |
526 | 531 | Linkpath string |
527 | 532 | } |
1241 | 1246 | } |
1242 | 1247 | |
1243 | 1248 | func (p *sshFxpExtendedPacketPosixRename) respond(s *Server) responsePacket { |
1244 | err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath)) | |
1249 | err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath)) | |
1245 | 1250 | return statusFromError(p.ID, err) |
1246 | 1251 | } |
1247 | 1252 | |
1270 | 1275 | } |
1271 | 1276 | |
1272 | 1277 | func (p *sshFxpExtendedPacketHardlink) respond(s *Server) responsePacket { |
1273 | err := os.Link(toLocalPath(p.Oldpath), toLocalPath(p.Newpath)) | |
1278 | err := os.Link(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath)) | |
1274 | 1279 | return statusFromError(p.ID, err) |
1275 | 1280 | } |
35 | 35 | testFs, _ := unmarshalFileStat(fl, at) |
36 | 36 | assert.Equal(t, fa, *testFs) |
37 | 37 | // Size and Mode |
38 | fa = FileStat{Mode: 700, Size: 99} | |
38 | fa = FileStat{Mode: 0700, Size: 99} | |
39 | 39 | fl = uint32(sshFileXferAttrSize | sshFileXferAttrPermissions) |
40 | 40 | at = []byte{} |
41 | 41 | at = marshalUint64(at, 99) |
42 | at = marshalUint32(at, 700) | |
42 | at = marshalUint32(at, 0700) | |
43 | 43 | testFs, _ = unmarshalFileStat(fl, at) |
44 | 44 | assert.Equal(t, fa, *testFs) |
45 | 45 | // FileMode |
46 | 46 | assert.True(t, testFs.FileMode().IsRegular()) |
47 | 47 | assert.False(t, testFs.FileMode().IsDir()) |
48 | assert.Equal(t, testFs.FileMode().Perm(), os.FileMode(700).Perm()) | |
48 | assert.Equal(t, testFs.FileMode().Perm(), os.FileMode(0700).Perm()) | |
49 | 49 | } |
50 | 50 | |
51 | 51 | func TestRequestAttributesEmpty(t *testing.T) { |
390 | 390 | return nil, err |
391 | 391 | } |
392 | 392 | return listerat{file}, nil |
393 | ||
394 | case "Readlink": | |
395 | symlink, err := fs.readlink(r.Filepath) | |
396 | if err != nil { | |
397 | return nil, err | |
398 | } | |
399 | ||
400 | // SFTP-v2: The server will respond with a SSH_FXP_NAME packet containing only | |
401 | // one name and a dummy attributes value. | |
402 | return listerat{ | |
403 | &memFile{ | |
404 | name: symlink, | |
405 | err: os.ErrNotExist, // prevent accidental use as a reader/writer. | |
406 | }, | |
407 | }, nil | |
408 | 393 | } |
409 | 394 | |
410 | 395 | return nil, errors.New("unsupported") |
433 | 418 | return files, nil |
434 | 419 | } |
435 | 420 | |
436 | func (fs *root) readlink(pathname string) (string, error) { | |
421 | func (fs *root) Readlink(pathname string) (string, error) { | |
437 | 422 | file, err := fs.lfetch(pathname) |
438 | 423 | if err != nil { |
439 | 424 | return "", err |
463 | 448 | return listerat{file}, nil |
464 | 449 | } |
465 | 450 | |
466 | // implements RealpathFileLister interface | |
467 | func (fs *root) Realpath(p string) string { | |
468 | if fs.startDirectory == "" || fs.startDirectory == "/" { | |
469 | return cleanPath(p) | |
470 | } | |
471 | return cleanPathWithBase(fs.startDirectory, p) | |
472 | } | |
473 | ||
474 | 451 | // In memory file-system-y thing that the Hanlders live on |
475 | 452 | type root struct { |
476 | rootFile *memFile | |
477 | mockErr error | |
478 | startDirectory string | |
453 | rootFile *memFile | |
454 | mockErr error | |
479 | 455 | |
480 | 456 | mu sync.Mutex |
481 | 457 | files map[string]*memFile |
533 | 509 | return err != os.ErrNotExist |
534 | 510 | } |
535 | 511 | |
536 | func (fs *root) fetch(path string) (*memFile, error) { | |
537 | file, err := fs.lfetch(path) | |
512 | func (fs *root) fetch(pathname string) (*memFile, error) { | |
513 | file, err := fs.lfetch(pathname) | |
538 | 514 | if err != nil { |
539 | 515 | return nil, err |
540 | 516 | } |
545 | 521 | return nil, errTooManySymlinks |
546 | 522 | } |
547 | 523 | |
548 | file, err = fs.lfetch(file.symlink) | |
524 | linkTarget := file.symlink | |
525 | if !path.IsAbs(linkTarget) { | |
526 | linkTarget = path.Join(path.Dir(file.name), linkTarget) | |
527 | } | |
528 | ||
529 | file, err = fs.lfetch(linkTarget) | |
549 | 530 | if err != nil { |
550 | 531 | return nil, err |
551 | 532 | } |
73 | 73 | // FileLister should return an object that fulfils the ListerAt interface |
74 | 74 | // Note in cases of an error, the error text will be sent to the client. |
75 | 75 | // Called for Methods: List, Stat, Readlink |
76 | // | |
77 | // Since Filelist returns an os.FileInfo, this can make it non-ideal for impelmenting Readlink. | |
78 | // This is because the Name receiver method defined by that interface defines that it should only return the base name. | |
79 | // However, Readlink is required to be capable of returning essentially any arbitrary valid path relative or absolute. | |
80 | // In order to implement this more expressive requirement, implement [ReadlinkFileLister] which will then be used instead. | |
76 | 81 | type FileLister interface { |
77 | 82 | Filelist(*Request) (ListerAt, error) |
78 | 83 | } |
86 | 91 | } |
87 | 92 | |
88 | 93 | // RealPathFileLister is a FileLister that implements the Realpath method. |
89 | // We use "/" as start directory for relative paths, implementing this | |
90 | // interface you can customize the start directory. | |
94 | // The built-in RealPath implementation does not resolve symbolic links. | |
95 | // By implementing this interface you can customize the returned path | |
96 | // and, for example, resolve symbolinc links if needed for your use case. | |
91 | 97 | // You have to return an absolute POSIX path. |
92 | 98 | // |
93 | // Deprecated: if you want to set a start directory use WithStartDirectory RequestServerOption instead. | |
99 | // Up to v1.13.5 the signature for the RealPath method was: | |
100 | // | |
101 | // # RealPath(string) string | |
102 | // | |
103 | // we have added a legacyRealPathFileLister that implements the old method | |
104 | // to ensure that your code does not break. | |
105 | // You should use the new method signature to avoid future issues | |
94 | 106 | type RealPathFileLister interface { |
107 | FileLister | |
108 | RealPath(string) (string, error) | |
109 | } | |
110 | ||
111 | // ReadlinkFileLister is a FileLister that implements the Readlink method. | |
112 | // By implementing the Readlink method, it is possible to return any arbitrary valid path relative or absolute. | |
113 | // This allows giving a better response than via the default FileLister (which is limited to os.FileInfo, whose Name method should only return the base name of a file) | |
114 | type ReadlinkFileLister interface { | |
115 | FileLister | |
116 | Readlink(string) (string, error) | |
117 | } | |
118 | ||
119 | // This interface is here for backward compatibility only | |
120 | type legacyRealPathFileLister interface { | |
95 | 121 | FileLister |
96 | 122 | RealPath(string) string |
97 | 123 | } |
2 | 2 | package sftp |
3 | 3 | |
4 | 4 | import ( |
5 | "path" | |
6 | "path/filepath" | |
7 | 5 | "syscall" |
8 | 6 | ) |
9 | 7 | |
14 | 12 | func testOsSys(sys interface{}) error { |
15 | 13 | return nil |
16 | 14 | } |
17 | ||
18 | func toLocalPath(p string) string { | |
19 | lp := filepath.FromSlash(p) | |
20 | ||
21 | if path.IsAbs(p) { | |
22 | tmp := lp[1:] | |
23 | ||
24 | if filepath.IsAbs(tmp) { | |
25 | // If the FromSlash without any starting slashes is absolute, | |
26 | // then we have a filepath encoded with a prefix '/'. | |
27 | // e.g. "/#s/boot" to "#s/boot" | |
28 | return tmp | |
29 | } | |
30 | } | |
31 | ||
32 | return lp | |
33 | } |
218 | 218 | rpkt = statusFromError(pkt.ID, rs.closeRequest(handle)) |
219 | 219 | case *sshFxpRealpathPacket: |
220 | 220 | var realPath string |
221 | if realPather, ok := rs.Handlers.FileList.(RealPathFileLister); ok { | |
222 | realPath = realPather.RealPath(pkt.getPath()) | |
221 | var err error | |
222 | ||
223 | switch pather := rs.Handlers.FileList.(type) { | |
224 | case RealPathFileLister: | |
225 | realPath, err = pather.RealPath(pkt.getPath()) | |
226 | case legacyRealPathFileLister: | |
227 | realPath = pather.RealPath(pkt.getPath()) | |
228 | default: | |
229 | realPath = cleanPathWithBase(rs.startDirectory, pkt.getPath()) | |
230 | } | |
231 | if err != nil { | |
232 | rpkt = statusFromError(pkt.ID, err) | |
223 | 233 | } else { |
224 | realPath = cleanPathWithBase(rs.startDirectory, pkt.getPath()) | |
225 | } | |
226 | rpkt = cleanPacketPath(pkt, realPath) | |
234 | rpkt = cleanPacketPath(pkt, realPath) | |
235 | } | |
227 | 236 | case *sshFxpOpendirPacket: |
228 | 237 | request := requestFromPacket(ctx, pkt, rs.startDirectory) |
229 | 238 | handle := rs.nextRequest(request) |
36 | 36 | |
37 | 37 | const sock = "/tmp/rstest.sock" |
38 | 38 | |
39 | func clientRequestServerPair(t *testing.T, options ...RequestServerOption) *csPair { | |
39 | func clientRequestServerPairWithHandlers(t *testing.T, handlers Handlers, options ...RequestServerOption) *csPair { | |
40 | 40 | skipIfWindows(t) |
41 | 41 | skipIfPlan9(t) |
42 | 42 | |
60 | 60 | fd, err := l.Accept() |
61 | 61 | require.NoError(t, err) |
62 | 62 | |
63 | handlers := InMemHandler() | |
64 | 63 | if *testAllocator { |
65 | 64 | options = append(options, WithRSAllocator()) |
66 | 65 | } |
87 | 86 | pair.svr = server |
88 | 87 | pair.cli = client |
89 | 88 | return pair |
89 | } | |
90 | ||
91 | func clientRequestServerPair(t *testing.T, options ...RequestServerOption) *csPair { | |
92 | return clientRequestServerPairWithHandlers(t, InMemHandler(), options...) | |
90 | 93 | } |
91 | 94 | |
92 | 95 | func checkRequestServerAllocator(t *testing.T, p *csPair) { |
564 | 567 | p := clientRequestServerPair(t) |
565 | 568 | defer p.Close() |
566 | 569 | |
567 | _, err := putTestFile(p.cli, "/foo", "hello") | |
568 | require.NoError(t, err) | |
569 | ||
570 | err = p.cli.Symlink("/foo", "/bar") | |
571 | require.NoError(t, err) | |
572 | err = p.cli.Symlink("/bar", "/baz") | |
573 | require.NoError(t, err) | |
574 | ||
570 | const CONTENT_FOO = "hello" | |
571 | const CONTENT_DIR_FILE_TXT = "file" | |
572 | const CONTENT_SUB_FILE_TXT = "file-in-sub" | |
573 | ||
574 | // prepare all files | |
575 | _, err := putTestFile(p.cli, "/foo", CONTENT_FOO) | |
576 | require.NoError(t, err) | |
577 | err = p.cli.Mkdir("/dir") | |
578 | require.NoError(t, err) | |
579 | err = p.cli.Mkdir("/dir/sub") | |
580 | require.NoError(t, err) | |
581 | _, err = putTestFile(p.cli, "/dir/file.txt", CONTENT_DIR_FILE_TXT) | |
582 | require.NoError(t, err) | |
583 | _, err = putTestFile(p.cli, "/dir/sub/file-in-sub.txt", CONTENT_SUB_FILE_TXT) | |
584 | require.NoError(t, err) | |
585 | ||
586 | type symlink struct { | |
587 | name string // this is the filename of the symbolic link | |
588 | target string // this is the file or directory the link points to | |
589 | ||
590 | //for testing | |
591 | expectsNotExist bool | |
592 | expectedFileContent string | |
593 | } | |
594 | ||
595 | symlinks := []symlink{ | |
596 | {name: "/bar", target: "/foo", expectedFileContent: CONTENT_FOO}, | |
597 | {name: "/baz", target: "/bar", expectedFileContent: CONTENT_FOO}, | |
598 | {name: "/link-to-non-existent-file", target: "non-existent-file", expectsNotExist: true}, | |
599 | {name: "/dir/rel-link.txt", target: "file.txt", expectedFileContent: CONTENT_DIR_FILE_TXT}, | |
600 | {name: "/dir/abs-link.txt", target: "/dir/file.txt", expectedFileContent: CONTENT_DIR_FILE_TXT}, | |
601 | {name: "/dir/rel-subdir-link.txt", target: "sub/file-in-sub.txt", expectedFileContent: CONTENT_SUB_FILE_TXT}, | |
602 | {name: "/dir/abs-subdir-link.txt", target: "/dir/sub/file-in-sub.txt", expectedFileContent: CONTENT_SUB_FILE_TXT}, | |
603 | {name: "/dir/sub/parentdir-link.txt", target: "../file.txt", expectedFileContent: CONTENT_DIR_FILE_TXT}, | |
604 | } | |
605 | ||
606 | for _, s := range symlinks { | |
607 | err := p.cli.Symlink(s.target, s.name) | |
608 | require.NoError(t, err, "Creating symlink %q with target %q failed", s.name, s.target) | |
609 | ||
610 | rl, err := p.cli.ReadLink(s.name) | |
611 | require.NoError(t, err, "ReadLink(%q) failed", s.name) | |
612 | require.Equal(t, s.target, rl, "Unexpected result when reading symlink %q", s.name) | |
613 | } | |
614 | ||
615 | // test fetching via symlink | |
575 | 616 | r := p.testHandler() |
576 | 617 | |
577 | fi, err := r.lfetch("/bar") | |
578 | require.NoError(t, err) | |
579 | assert.True(t, fi.Mode()&os.ModeSymlink == os.ModeSymlink) | |
580 | ||
581 | fi, err = r.lfetch("/baz") | |
582 | require.NoError(t, err) | |
583 | assert.True(t, fi.Mode()&os.ModeSymlink == os.ModeSymlink) | |
584 | ||
585 | content, err := getTestFile(p.cli, "/baz") | |
586 | require.NoError(t, err) | |
587 | assert.Equal(t, []byte("hello"), content) | |
618 | for _, s := range symlinks { | |
619 | fi, err := r.lfetch(s.name) | |
620 | require.NoError(t, err, "lfetch(%q) failed", s.name) | |
621 | require.True(t, fi.Mode()&os.ModeSymlink == os.ModeSymlink, "Expected %q to be a symlink but it is not.", s.name) | |
622 | ||
623 | content, err := getTestFile(p.cli, s.name) | |
624 | if s.expectsNotExist { | |
625 | require.True(t, os.IsNotExist(err), "Reading symlink %q expected os.ErrNotExist", s.name) | |
626 | } else { | |
627 | require.NoError(t, err, "getTestFile(%q) failed", s.name) | |
628 | require.Equal(t, []byte(s.expectedFileContent), content, "Reading symlink %q returned unexpected content", s.name) | |
629 | } | |
630 | } | |
588 | 631 | |
589 | 632 | checkRequestServerAllocator(t, p) |
590 | 633 | } |
710 | 753 | require.NoError(t, err) |
711 | 754 | err = p.cli.Symlink("/foo", "/bar") |
712 | 755 | require.NoError(t, err) |
756 | ||
713 | 757 | rl, err := p.cli.ReadLink("/bar") |
714 | 758 | assert.NoError(t, err) |
715 | assert.Equal(t, "foo", rl) | |
759 | assert.Equal(t, "/foo", rl) | |
760 | ||
761 | _, err = p.cli.ReadLink("/foo") | |
762 | assert.Error(t, err, "Readlink on non-symlink should fail") | |
763 | ||
764 | _, err = p.cli.ReadLink("/does-not-exist") | |
765 | assert.Error(t, err, "Readlink on non-existent file should fail") | |
766 | ||
716 | 767 | checkRequestServerAllocator(t, p) |
717 | 768 | } |
718 | 769 | |
839 | 890 | } |
840 | 891 | |
841 | 892 | func TestRealPath(t *testing.T) { |
842 | root := &root{ | |
843 | rootFile: &memFile{name: "/", modtime: time.Now(), isdir: true}, | |
844 | files: make(map[string]*memFile), | |
845 | startDirectory: "/apath", | |
846 | } | |
847 | ||
848 | p := root.Realpath(".") | |
849 | assert.Equal(t, root.startDirectory, p) | |
850 | p = root.Realpath("/") | |
851 | assert.Equal(t, "/", p) | |
852 | p = root.Realpath("..") | |
853 | assert.Equal(t, "/", p) | |
854 | p = root.Realpath("../../..") | |
855 | assert.Equal(t, "/", p) | |
856 | p = root.Realpath("relpath") | |
857 | assert.Equal(t, path.Join(root.startDirectory, "relpath"), p) | |
893 | startDir := "/startdir" | |
894 | // the default InMemHandler does not implement the RealPathFileLister interface | |
895 | // so we are using the builtin implementation here | |
896 | p := clientRequestServerPair(t, WithStartDirectory(startDir)) | |
897 | defer p.Close() | |
898 | ||
899 | realPath, err := p.cli.RealPath(".") | |
900 | require.NoError(t, err) | |
901 | assert.Equal(t, startDir, realPath) | |
902 | realPath, err = p.cli.RealPath("/") | |
903 | require.NoError(t, err) | |
904 | assert.Equal(t, "/", realPath) | |
905 | realPath, err = p.cli.RealPath("..") | |
906 | require.NoError(t, err) | |
907 | assert.Equal(t, "/", realPath) | |
908 | realPath, err = p.cli.RealPath("../../..") | |
909 | require.NoError(t, err) | |
910 | assert.Equal(t, "/", realPath) | |
911 | // test a relative path | |
912 | realPath, err = p.cli.RealPath("relpath") | |
913 | require.NoError(t, err) | |
914 | assert.Equal(t, path.Join(startDir, "relpath"), realPath) | |
915 | } | |
916 | ||
917 | // In memory file-system which implements RealPathFileLister | |
918 | type rootWithRealPather struct { | |
919 | root | |
920 | } | |
921 | ||
922 | // implements RealpathFileLister interface | |
923 | func (fs *rootWithRealPather) RealPath(p string) (string, error) { | |
924 | if fs.mockErr != nil { | |
925 | return "", fs.mockErr | |
926 | } | |
927 | return cleanPath(p), nil | |
928 | } | |
929 | ||
930 | func TestRealPathFileLister(t *testing.T) { | |
931 | root := &rootWithRealPather{ | |
932 | root: root{ | |
933 | rootFile: &memFile{name: "/", modtime: time.Now(), isdir: true}, | |
934 | files: make(map[string]*memFile), | |
935 | }, | |
936 | } | |
937 | handlers := Handlers{root, root, root, root} | |
938 | p := clientRequestServerPairWithHandlers(t, handlers) | |
939 | defer p.Close() | |
940 | ||
941 | realPath, err := p.cli.RealPath(".") | |
942 | require.NoError(t, err) | |
943 | assert.Equal(t, "/", realPath) | |
944 | realPath, err = p.cli.RealPath("relpath") | |
945 | require.NoError(t, err) | |
946 | assert.Equal(t, "/relpath", realPath) | |
947 | // test an error | |
948 | root.returnErr(ErrSSHFxPermissionDenied) | |
949 | _, err = p.cli.RealPath("/") | |
950 | require.ErrorIs(t, err, os.ErrPermission) | |
951 | } | |
952 | ||
953 | // In memory file-system which implements legacyRealPathFileLister | |
954 | type rootWithLegacyRealPather struct { | |
955 | root | |
956 | } | |
957 | ||
958 | // implements RealpathFileLister interface | |
959 | func (fs *rootWithLegacyRealPather) RealPath(p string) string { | |
960 | return cleanPath(p) | |
961 | } | |
962 | ||
963 | func TestLegacyRealPathFileLister(t *testing.T) { | |
964 | root := &rootWithLegacyRealPather{ | |
965 | root: root{ | |
966 | rootFile: &memFile{name: "/", modtime: time.Now(), isdir: true}, | |
967 | files: make(map[string]*memFile), | |
968 | }, | |
969 | } | |
970 | handlers := Handlers{root, root, root, root} | |
971 | p := clientRequestServerPairWithHandlers(t, handlers) | |
972 | defer p.Close() | |
973 | ||
974 | realPath, err := p.cli.RealPath(".") | |
975 | require.NoError(t, err) | |
976 | assert.Equal(t, "/", realPath) | |
977 | realPath, err = p.cli.RealPath("..") | |
978 | require.NoError(t, err) | |
979 | assert.Equal(t, "/", realPath) | |
980 | realPath, err = p.cli.RealPath("relpath") | |
981 | require.NoError(t, err) | |
982 | assert.Equal(t, "/relpath", realPath) | |
858 | 983 | } |
859 | 984 | |
860 | 985 | func TestCleanPath(t *testing.T) { |
186 | 186 | // NOTE: given a POSIX compliant signature: symlink(target, linkpath string) |
187 | 187 | // this makes Request.Target the linkpath, and Request.Filepath the target. |
188 | 188 | request.Target = cleanPathWithBase(baseDir, p.Linkpath) |
189 | request.Filepath = p.Targetpath | |
189 | 190 | case *sshFxpExtendedPacketHardlink: |
190 | 191 | request.Target = cleanPathWithBase(baseDir, p.Newpath) |
191 | 192 | } |
293 | 294 | return filecmd(handlers.FileCmd, r, pkt) |
294 | 295 | case "List": |
295 | 296 | return filelist(handlers.FileList, r, pkt) |
296 | case "Stat", "Lstat", "Readlink": | |
297 | case "Stat", "Lstat": | |
298 | return filestat(handlers.FileList, r, pkt) | |
299 | case "Readlink": | |
300 | if readlinkFileLister, ok := handlers.FileList.(ReadlinkFileLister); ok { | |
301 | return readlink(readlinkFileLister, r, pkt) | |
302 | } | |
297 | 303 | return filestat(handlers.FileList, r, pkt) |
298 | 304 | default: |
299 | 305 | return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method)) |
594 | 600 | default: |
595 | 601 | err = fmt.Errorf("unexpected method: %s", r.Method) |
596 | 602 | return statusFromError(pkt.id(), err) |
603 | } | |
604 | } | |
605 | ||
606 | func readlink(readlinkFileLister ReadlinkFileLister, r *Request, pkt requestPacket) responsePacket { | |
607 | resolved, err := readlinkFileLister.Readlink(r.Filepath) | |
608 | if err != nil { | |
609 | return statusFromError(pkt.id(), err) | |
610 | } | |
611 | return &sshFxpNamePacket{ | |
612 | ID: pkt.id(), | |
613 | NameAttrs: []*sshFxpNameAttr{ | |
614 | { | |
615 | Name: resolved, | |
616 | LongName: resolved, | |
617 | Attrs: emptyFileStat, | |
618 | }, | |
619 | }, | |
597 | 620 | } |
598 | 621 | } |
599 | 622 |
0 | 0 | package sftp |
1 | 1 | |
2 | 2 | import ( |
3 | "path" | |
4 | "path/filepath" | |
5 | 3 | "syscall" |
6 | 4 | ) |
7 | 5 | |
12 | 10 | func testOsSys(sys interface{}) error { |
13 | 11 | return nil |
14 | 12 | } |
15 | ||
16 | func toLocalPath(p string) string { | |
17 | lp := filepath.FromSlash(p) | |
18 | ||
19 | if path.IsAbs(p) { | |
20 | tmp := lp | |
21 | for len(tmp) > 0 && tmp[0] == '\\' { | |
22 | tmp = tmp[1:] | |
23 | } | |
24 | ||
25 | if filepath.IsAbs(tmp) { | |
26 | // If the FromSlash without any starting slashes is absolute, | |
27 | // then we have a filepath encoded with a prefix '/'. | |
28 | // e.g. "/C:/Windows" to "C:\\Windows" | |
29 | return tmp | |
30 | } | |
31 | ||
32 | tmp += "\\" | |
33 | ||
34 | if filepath.IsAbs(tmp) { | |
35 | // If the FromSlash without any starting slashes but with extra end slash is absolute, | |
36 | // then we have a filepath encoded with a prefix '/' and a dropped '/' at the end. | |
37 | // e.g. "/C:" to "C:\\" | |
38 | return tmp | |
39 | } | |
40 | } | |
41 | ||
42 | return lp | |
43 | } |
32 | 32 | openFiles map[string]*os.File |
33 | 33 | openFilesLock sync.RWMutex |
34 | 34 | handleCount int |
35 | workDir string | |
35 | 36 | } |
36 | 37 | |
37 | 38 | func (svr *Server) nextHandle(f *os.File) string { |
123 | 124 | alloc := newAllocator() |
124 | 125 | s.pktMgr.alloc = alloc |
125 | 126 | s.conn.alloc = alloc |
127 | return nil | |
128 | } | |
129 | } | |
130 | ||
131 | // WithServerWorkingDirectory sets a working directory to use as base | |
132 | // for relative paths. | |
133 | // If unset the default is current working directory (os.Getwd). | |
134 | func WithServerWorkingDirectory(workDir string) ServerOption { | |
135 | return func(s *Server) error { | |
136 | s.workDir = cleanPath(workDir) | |
126 | 137 | return nil |
127 | 138 | } |
128 | 139 | } |
173 | 184 | } |
174 | 185 | case *sshFxpStatPacket: |
175 | 186 | // stat the requested file |
176 | info, err := os.Stat(toLocalPath(p.Path)) | |
187 | info, err := os.Stat(s.toLocalPath(p.Path)) | |
177 | 188 | rpkt = &sshFxpStatResponse{ |
178 | 189 | ID: p.ID, |
179 | 190 | info: info, |
183 | 194 | } |
184 | 195 | case *sshFxpLstatPacket: |
185 | 196 | // stat the requested file |
186 | info, err := os.Lstat(toLocalPath(p.Path)) | |
197 | info, err := os.Lstat(s.toLocalPath(p.Path)) | |
187 | 198 | rpkt = &sshFxpStatResponse{ |
188 | 199 | ID: p.ID, |
189 | 200 | info: info, |
207 | 218 | } |
208 | 219 | case *sshFxpMkdirPacket: |
209 | 220 | // TODO FIXME: ignore flags field |
210 | err := os.Mkdir(toLocalPath(p.Path), 0755) | |
221 | err := os.Mkdir(s.toLocalPath(p.Path), 0o755) | |
211 | 222 | rpkt = statusFromError(p.ID, err) |
212 | 223 | case *sshFxpRmdirPacket: |
213 | err := os.Remove(toLocalPath(p.Path)) | |
224 | err := os.Remove(s.toLocalPath(p.Path)) | |
214 | 225 | rpkt = statusFromError(p.ID, err) |
215 | 226 | case *sshFxpRemovePacket: |
216 | err := os.Remove(toLocalPath(p.Filename)) | |
227 | err := os.Remove(s.toLocalPath(p.Filename)) | |
217 | 228 | rpkt = statusFromError(p.ID, err) |
218 | 229 | case *sshFxpRenamePacket: |
219 | err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath)) | |
230 | err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath)) | |
220 | 231 | rpkt = statusFromError(p.ID, err) |
221 | 232 | case *sshFxpSymlinkPacket: |
222 | err := os.Symlink(toLocalPath(p.Targetpath), toLocalPath(p.Linkpath)) | |
233 | err := os.Symlink(s.toLocalPath(p.Targetpath), s.toLocalPath(p.Linkpath)) | |
223 | 234 | rpkt = statusFromError(p.ID, err) |
224 | 235 | case *sshFxpClosePacket: |
225 | 236 | rpkt = statusFromError(p.ID, s.closeHandle(p.Handle)) |
226 | 237 | case *sshFxpReadlinkPacket: |
227 | f, err := os.Readlink(toLocalPath(p.Path)) | |
238 | f, err := os.Readlink(s.toLocalPath(p.Path)) | |
228 | 239 | rpkt = &sshFxpNamePacket{ |
229 | 240 | ID: p.ID, |
230 | 241 | NameAttrs: []*sshFxpNameAttr{ |
239 | 250 | rpkt = statusFromError(p.ID, err) |
240 | 251 | } |
241 | 252 | case *sshFxpRealpathPacket: |
242 | f, err := filepath.Abs(toLocalPath(p.Path)) | |
253 | f, err := filepath.Abs(s.toLocalPath(p.Path)) | |
243 | 254 | f = cleanPath(f) |
244 | 255 | rpkt = &sshFxpNamePacket{ |
245 | 256 | ID: p.ID, |
255 | 266 | rpkt = statusFromError(p.ID, err) |
256 | 267 | } |
257 | 268 | case *sshFxpOpendirPacket: |
258 | p.Path = toLocalPath(p.Path) | |
259 | ||
260 | if stat, err := os.Stat(p.Path); err != nil { | |
269 | lp := s.toLocalPath(p.Path) | |
270 | ||
271 | if stat, err := os.Stat(lp); err != nil { | |
261 | 272 | rpkt = statusFromError(p.ID, err) |
262 | 273 | } else if !stat.IsDir() { |
263 | 274 | rpkt = statusFromError(p.ID, &os.PathError{ |
264 | Path: p.Path, Err: syscall.ENOTDIR}) | |
275 | Path: lp, Err: syscall.ENOTDIR, | |
276 | }) | |
265 | 277 | } else { |
266 | 278 | rpkt = (&sshFxpOpenPacket{ |
267 | 279 | ID: p.ID, |
445 | 457 | osFlags |= os.O_EXCL |
446 | 458 | } |
447 | 459 | |
448 | f, err := os.OpenFile(toLocalPath(p.Path), osFlags, 0644) | |
460 | f, err := os.OpenFile(svr.toLocalPath(p.Path), osFlags, 0o644) | |
449 | 461 | if err != nil { |
450 | 462 | return statusFromError(p.ID, err) |
451 | 463 | } |
483 | 495 | b := p.Attrs.([]byte) |
484 | 496 | var err error |
485 | 497 | |
486 | p.Path = toLocalPath(p.Path) | |
498 | p.Path = svr.toLocalPath(p.Path) | |
487 | 499 | |
488 | 500 | debug("setstat name \"%s\"", p.Path) |
489 | 501 | if (p.Flags & sshFileXferAttrSize) != 0 { |
0 | //go:build !windows | |
1 | // +build !windows | |
2 | ||
3 | package sftp | |
4 | ||
5 | import ( | |
6 | "testing" | |
7 | ) | |
8 | ||
9 | func TestServer_toLocalPath(t *testing.T) { | |
10 | tests := []struct { | |
11 | name string | |
12 | withWorkDir string | |
13 | p string | |
14 | want string | |
15 | }{ | |
16 | { | |
17 | name: "empty path with no workdir", | |
18 | p: "", | |
19 | want: "", | |
20 | }, | |
21 | { | |
22 | name: "relative path with no workdir", | |
23 | p: "file", | |
24 | want: "file", | |
25 | }, | |
26 | { | |
27 | name: "absolute path with no workdir", | |
28 | p: "/file", | |
29 | want: "/file", | |
30 | }, | |
31 | { | |
32 | name: "workdir and empty path", | |
33 | withWorkDir: "/home/user", | |
34 | p: "", | |
35 | want: "/home/user", | |
36 | }, | |
37 | { | |
38 | name: "workdir and relative path", | |
39 | withWorkDir: "/home/user", | |
40 | p: "file", | |
41 | want: "/home/user/file", | |
42 | }, | |
43 | { | |
44 | name: "workdir and relative path with .", | |
45 | withWorkDir: "/home/user", | |
46 | p: ".", | |
47 | want: "/home/user", | |
48 | }, | |
49 | { | |
50 | name: "workdir and relative path with . and file", | |
51 | withWorkDir: "/home/user", | |
52 | p: "./file", | |
53 | want: "/home/user/file", | |
54 | }, | |
55 | { | |
56 | name: "workdir and absolute path", | |
57 | withWorkDir: "/home/user", | |
58 | p: "/file", | |
59 | want: "/file", | |
60 | }, | |
61 | { | |
62 | name: "workdir and non-unixy path prefixes workdir", | |
63 | withWorkDir: "/home/user", | |
64 | p: "C:\\file", | |
65 | // This may look like a bug but it is the result of passing | |
66 | // invalid input (a non-unixy path) to the server. | |
67 | want: "/home/user/C:\\file", | |
68 | }, | |
69 | } | |
70 | for _, tt := range tests { | |
71 | t.Run(tt.name, func(t *testing.T) { | |
72 | // We don't need to initialize the Server further to test | |
73 | // toLocalPath behavior. | |
74 | s := &Server{} | |
75 | if tt.withWorkDir != "" { | |
76 | if err := WithServerWorkingDirectory(tt.withWorkDir)(s); err != nil { | |
77 | t.Fatal(err) | |
78 | } | |
79 | } | |
80 | ||
81 | if got := s.toLocalPath(tt.p); got != tt.want { | |
82 | t.Errorf("Server.toLocalPath() = %q, want %q", got, tt.want) | |
83 | } | |
84 | }) | |
85 | } | |
86 | } |
0 | package sftp | |
1 | ||
2 | import ( | |
3 | "path" | |
4 | "path/filepath" | |
5 | ) | |
6 | ||
7 | func (s *Server) toLocalPath(p string) string { | |
8 | if s.workDir != "" && !path.IsAbs(p) { | |
9 | p = path.Join(s.workDir, p) | |
10 | } | |
11 | ||
12 | lp := filepath.FromSlash(p) | |
13 | ||
14 | if path.IsAbs(p) { | |
15 | tmp := lp[1:] | |
16 | ||
17 | if filepath.IsAbs(tmp) { | |
18 | // If the FromSlash without any starting slashes is absolute, | |
19 | // then we have a filepath encoded with a prefix '/'. | |
20 | // e.g. "/#s/boot" to "#s/boot" | |
21 | return tmp | |
22 | } | |
23 | } | |
24 | ||
25 | return lp | |
26 | } |
0 | //go:build !windows && !plan9 | |
1 | // +build !windows,!plan9 | |
2 | ||
3 | package sftp | |
4 | ||
5 | import ( | |
6 | "path" | |
7 | ) | |
8 | ||
9 | func (s *Server) toLocalPath(p string) string { | |
10 | if s.workDir != "" && !path.IsAbs(p) { | |
11 | p = path.Join(s.workDir, p) | |
12 | } | |
13 | ||
14 | return p | |
15 | } |
0 | package sftp | |
1 | ||
2 | import ( | |
3 | "path" | |
4 | "path/filepath" | |
5 | ) | |
6 | ||
7 | func (s *Server) toLocalPath(p string) string { | |
8 | if s.workDir != "" && !path.IsAbs(p) { | |
9 | p = path.Join(s.workDir, p) | |
10 | } | |
11 | ||
12 | lp := filepath.FromSlash(p) | |
13 | ||
14 | if path.IsAbs(p) { | |
15 | tmp := lp | |
16 | for len(tmp) > 0 && tmp[0] == '\\' { | |
17 | tmp = tmp[1:] | |
18 | } | |
19 | ||
20 | if filepath.IsAbs(tmp) { | |
21 | // If the FromSlash without any starting slashes is absolute, | |
22 | // then we have a filepath encoded with a prefix '/'. | |
23 | // e.g. "/C:/Windows" to "C:\\Windows" | |
24 | return tmp | |
25 | } | |
26 | ||
27 | tmp += "\\" | |
28 | ||
29 | if filepath.IsAbs(tmp) { | |
30 | // If the FromSlash without any starting slashes but with extra end slash is absolute, | |
31 | // then we have a filepath encoded with a prefix '/' and a dropped '/' at the end. | |
32 | // e.g. "/C:" to "C:\\" | |
33 | return tmp | |
34 | } | |
35 | } | |
36 | ||
37 | return lp | |
38 | } |
0 | package sftp | |
1 | ||
2 | import ( | |
3 | "testing" | |
4 | ) | |
5 | ||
6 | func TestServer_toLocalPath(t *testing.T) { | |
7 | tests := []struct { | |
8 | name string | |
9 | withWorkDir string | |
10 | p string | |
11 | want string | |
12 | }{ | |
13 | { | |
14 | name: "empty path with no workdir", | |
15 | p: "", | |
16 | want: "", | |
17 | }, | |
18 | { | |
19 | name: "relative path with no workdir", | |
20 | p: "file", | |
21 | want: "file", | |
22 | }, | |
23 | { | |
24 | name: "absolute path with no workdir", | |
25 | p: "/file", | |
26 | want: "\\file", | |
27 | }, | |
28 | { | |
29 | name: "workdir and empty path", | |
30 | withWorkDir: "C:\\Users\\User", | |
31 | p: "", | |
32 | want: "C:\\Users\\User", | |
33 | }, | |
34 | { | |
35 | name: "workdir and relative path", | |
36 | withWorkDir: "C:\\Users\\User", | |
37 | p: "file", | |
38 | want: "C:\\Users\\User\\file", | |
39 | }, | |
40 | { | |
41 | name: "workdir and relative path with .", | |
42 | withWorkDir: "C:\\Users\\User", | |
43 | p: ".", | |
44 | want: "C:\\Users\\User", | |
45 | }, | |
46 | { | |
47 | name: "workdir and relative path with . and file", | |
48 | withWorkDir: "C:\\Users\\User", | |
49 | p: "./file", | |
50 | want: "C:\\Users\\User\\file", | |
51 | }, | |
52 | { | |
53 | name: "workdir and absolute path", | |
54 | withWorkDir: "C:\\Users\\User", | |
55 | p: "/C:/file", | |
56 | want: "C:\\file", | |
57 | }, | |
58 | { | |
59 | name: "workdir and non-unixy path prefixes workdir", | |
60 | withWorkDir: "C:\\Users\\User", | |
61 | p: "C:\\file", | |
62 | // This may look like a bug but it is the result of passing | |
63 | // invalid input (a non-unixy path) to the server. | |
64 | want: "C:\\Users\\User\\C:\\file", | |
65 | }, | |
66 | } | |
67 | for _, tt := range tests { | |
68 | t.Run(tt.name, func(t *testing.T) { | |
69 | // We don't need to initialize the Server further to test | |
70 | // toLocalPath behavior. | |
71 | s := &Server{} | |
72 | if tt.withWorkDir != "" { | |
73 | if err := WithServerWorkingDirectory(tt.withWorkDir)(s); err != nil { | |
74 | t.Fatal(err) | |
75 | } | |
76 | } | |
77 | ||
78 | if got := s.toLocalPath(tt.p); got != tt.want { | |
79 | t.Errorf("Server.toLocalPath() = %q, want %q", got, tt.want) | |
80 | } | |
81 | }) | |
82 | } | |
83 | } |