Codebase list golang-github-pkg-sftp / e7db020
Import upstream version 1.13.5+git20221123.1.707787e Debian Janitor 1 year, 4 months ago
24 changed file(s) with 588 addition(s) and 230 deletion(s). Raw diff Collapse all Expand all
+0
-2
.github/workflows/.editorconfig less more
0 [*.yml]
1 indent_size = 2
+0
-38
.github/workflows/CI.yml less more
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
-10
.gitignore less more
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
33 "bytes"
44 "encoding/binary"
55 "errors"
6 "fmt"
67 "io"
78 "math"
89 "os"
225226
226227 if err := sftp.sendInit(); err != nil {
227228 wr.Close()
228 return nil, err
229 }
229 return nil, fmt.Errorf("error sending init packet to server: %w", err)
230 }
231
230232 if err := sftp.recvVersion(); err != nil {
231233 wr.Close()
232 return nil, err
234 return nil, fmt.Errorf("error receiving version packet from server: %w", err)
233235 }
234236
235237 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 }()
237245
238246 return sftp, nil
239247 }
266274 func (c *Client) recvVersion() error {
267275 typ, data, err := c.recvPacket(0)
268276 if err != nil {
277 if err == io.EOF {
278 return fmt.Errorf("server unexpectedly closed connection: %w", io.ErrUnexpectedEOF)
279 }
280
269281 return err
270282 }
283
271284 if typ != sshFxpVersion {
272285 return &unexpectedPacketErr{sshFxpVersion, typ}
273286 }
276289 if err != nil {
277290 return err
278291 }
292
279293 if version != sftpProtocolVersion {
280294 return &unexpectedVersionErr{sftpProtocolVersion, version}
281295 }
5858 func (c *clientConn) Close() error {
5959 defer c.wg.Wait()
6060 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 }
6961 }
7062
7163 // recv continuously reads from the server and forwards responses to the
33
44 require (
55 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
98 )
0 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
10 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=
23 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
34 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
45 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
56 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
67 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=
1225 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=
1426 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=
1831 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=
2039 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=
2143 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2244 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=
2445 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=
322322 //
323323 // The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed.
324324 // 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
326326 type SymlinkPacket struct {
327327 LinkPath string
328328 TargetPath string
521521 }
522522
523523 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
525530 Targetpath string
526531 Linkpath string
527532 }
12411246 }
12421247
12431248 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))
12451250 return statusFromError(p.ID, err)
12461251 }
12471252
12701275 }
12711276
12721277 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))
12741279 return statusFromError(p.ID, err)
12751280 }
3535 testFs, _ := unmarshalFileStat(fl, at)
3636 assert.Equal(t, fa, *testFs)
3737 // Size and Mode
38 fa = FileStat{Mode: 700, Size: 99}
38 fa = FileStat{Mode: 0700, Size: 99}
3939 fl = uint32(sshFileXferAttrSize | sshFileXferAttrPermissions)
4040 at = []byte{}
4141 at = marshalUint64(at, 99)
42 at = marshalUint32(at, 700)
42 at = marshalUint32(at, 0700)
4343 testFs, _ = unmarshalFileStat(fl, at)
4444 assert.Equal(t, fa, *testFs)
4545 // FileMode
4646 assert.True(t, testFs.FileMode().IsRegular())
4747 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())
4949 }
5050
5151 func TestRequestAttributesEmpty(t *testing.T) {
390390 return nil, err
391391 }
392392 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
408393 }
409394
410395 return nil, errors.New("unsupported")
433418 return files, nil
434419 }
435420
436 func (fs *root) readlink(pathname string) (string, error) {
421 func (fs *root) Readlink(pathname string) (string, error) {
437422 file, err := fs.lfetch(pathname)
438423 if err != nil {
439424 return "", err
463448 return listerat{file}, nil
464449 }
465450
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
474451 // In memory file-system-y thing that the Hanlders live on
475452 type root struct {
476 rootFile *memFile
477 mockErr error
478 startDirectory string
453 rootFile *memFile
454 mockErr error
479455
480456 mu sync.Mutex
481457 files map[string]*memFile
533509 return err != os.ErrNotExist
534510 }
535511
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)
538514 if err != nil {
539515 return nil, err
540516 }
545521 return nil, errTooManySymlinks
546522 }
547523
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)
549530 if err != nil {
550531 return nil, err
551532 }
7373 // FileLister should return an object that fulfils the ListerAt interface
7474 // Note in cases of an error, the error text will be sent to the client.
7575 // 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.
7681 type FileLister interface {
7782 Filelist(*Request) (ListerAt, error)
7883 }
8691 }
8792
8893 // 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.
9197 // You have to return an absolute POSIX path.
9298 //
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
94106 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 {
95121 FileLister
96122 RealPath(string) string
97123 }
22 package sftp
33
44 import (
5 "path"
6 "path/filepath"
75 "syscall"
86 )
97
1412 func testOsSys(sys interface{}) error {
1513 return nil
1614 }
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 }
218218 rpkt = statusFromError(pkt.ID, rs.closeRequest(handle))
219219 case *sshFxpRealpathPacket:
220220 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)
223233 } else {
224 realPath = cleanPathWithBase(rs.startDirectory, pkt.getPath())
225 }
226 rpkt = cleanPacketPath(pkt, realPath)
234 rpkt = cleanPacketPath(pkt, realPath)
235 }
227236 case *sshFxpOpendirPacket:
228237 request := requestFromPacket(ctx, pkt, rs.startDirectory)
229238 handle := rs.nextRequest(request)
3636
3737 const sock = "/tmp/rstest.sock"
3838
39 func clientRequestServerPair(t *testing.T, options ...RequestServerOption) *csPair {
39 func clientRequestServerPairWithHandlers(t *testing.T, handlers Handlers, options ...RequestServerOption) *csPair {
4040 skipIfWindows(t)
4141 skipIfPlan9(t)
4242
6060 fd, err := l.Accept()
6161 require.NoError(t, err)
6262
63 handlers := InMemHandler()
6463 if *testAllocator {
6564 options = append(options, WithRSAllocator())
6665 }
8786 pair.svr = server
8887 pair.cli = client
8988 return pair
89 }
90
91 func clientRequestServerPair(t *testing.T, options ...RequestServerOption) *csPair {
92 return clientRequestServerPairWithHandlers(t, InMemHandler(), options...)
9093 }
9194
9295 func checkRequestServerAllocator(t *testing.T, p *csPair) {
564567 p := clientRequestServerPair(t)
565568 defer p.Close()
566569
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
575616 r := p.testHandler()
576617
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 }
588631
589632 checkRequestServerAllocator(t, p)
590633 }
710753 require.NoError(t, err)
711754 err = p.cli.Symlink("/foo", "/bar")
712755 require.NoError(t, err)
756
713757 rl, err := p.cli.ReadLink("/bar")
714758 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
716767 checkRequestServerAllocator(t, p)
717768 }
718769
839890 }
840891
841892 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)
858983 }
859984
860985 func TestCleanPath(t *testing.T) {
2020 }
2121 return nil
2222 }
23
24 func toLocalPath(p string) string {
25 return p
26 }
186186 // NOTE: given a POSIX compliant signature: symlink(target, linkpath string)
187187 // this makes Request.Target the linkpath, and Request.Filepath the target.
188188 request.Target = cleanPathWithBase(baseDir, p.Linkpath)
189 request.Filepath = p.Targetpath
189190 case *sshFxpExtendedPacketHardlink:
190191 request.Target = cleanPathWithBase(baseDir, p.Newpath)
191192 }
293294 return filecmd(handlers.FileCmd, r, pkt)
294295 case "List":
295296 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 }
297303 return filestat(handlers.FileList, r, pkt)
298304 default:
299305 return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method))
594600 default:
595601 err = fmt.Errorf("unexpected method: %s", r.Method)
596602 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 },
597620 }
598621 }
599622
00 package sftp
11
22 import (
3 "path"
4 "path/filepath"
53 "syscall"
64 )
75
1210 func testOsSys(sys interface{}) error {
1311 return nil
1412 }
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 }
3232 openFiles map[string]*os.File
3333 openFilesLock sync.RWMutex
3434 handleCount int
35 workDir string
3536 }
3637
3738 func (svr *Server) nextHandle(f *os.File) string {
123124 alloc := newAllocator()
124125 s.pktMgr.alloc = alloc
125126 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)
126137 return nil
127138 }
128139 }
173184 }
174185 case *sshFxpStatPacket:
175186 // stat the requested file
176 info, err := os.Stat(toLocalPath(p.Path))
187 info, err := os.Stat(s.toLocalPath(p.Path))
177188 rpkt = &sshFxpStatResponse{
178189 ID: p.ID,
179190 info: info,
183194 }
184195 case *sshFxpLstatPacket:
185196 // stat the requested file
186 info, err := os.Lstat(toLocalPath(p.Path))
197 info, err := os.Lstat(s.toLocalPath(p.Path))
187198 rpkt = &sshFxpStatResponse{
188199 ID: p.ID,
189200 info: info,
207218 }
208219 case *sshFxpMkdirPacket:
209220 // TODO FIXME: ignore flags field
210 err := os.Mkdir(toLocalPath(p.Path), 0755)
221 err := os.Mkdir(s.toLocalPath(p.Path), 0o755)
211222 rpkt = statusFromError(p.ID, err)
212223 case *sshFxpRmdirPacket:
213 err := os.Remove(toLocalPath(p.Path))
224 err := os.Remove(s.toLocalPath(p.Path))
214225 rpkt = statusFromError(p.ID, err)
215226 case *sshFxpRemovePacket:
216 err := os.Remove(toLocalPath(p.Filename))
227 err := os.Remove(s.toLocalPath(p.Filename))
217228 rpkt = statusFromError(p.ID, err)
218229 case *sshFxpRenamePacket:
219 err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath))
230 err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath))
220231 rpkt = statusFromError(p.ID, err)
221232 case *sshFxpSymlinkPacket:
222 err := os.Symlink(toLocalPath(p.Targetpath), toLocalPath(p.Linkpath))
233 err := os.Symlink(s.toLocalPath(p.Targetpath), s.toLocalPath(p.Linkpath))
223234 rpkt = statusFromError(p.ID, err)
224235 case *sshFxpClosePacket:
225236 rpkt = statusFromError(p.ID, s.closeHandle(p.Handle))
226237 case *sshFxpReadlinkPacket:
227 f, err := os.Readlink(toLocalPath(p.Path))
238 f, err := os.Readlink(s.toLocalPath(p.Path))
228239 rpkt = &sshFxpNamePacket{
229240 ID: p.ID,
230241 NameAttrs: []*sshFxpNameAttr{
239250 rpkt = statusFromError(p.ID, err)
240251 }
241252 case *sshFxpRealpathPacket:
242 f, err := filepath.Abs(toLocalPath(p.Path))
253 f, err := filepath.Abs(s.toLocalPath(p.Path))
243254 f = cleanPath(f)
244255 rpkt = &sshFxpNamePacket{
245256 ID: p.ID,
255266 rpkt = statusFromError(p.ID, err)
256267 }
257268 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 {
261272 rpkt = statusFromError(p.ID, err)
262273 } else if !stat.IsDir() {
263274 rpkt = statusFromError(p.ID, &os.PathError{
264 Path: p.Path, Err: syscall.ENOTDIR})
275 Path: lp, Err: syscall.ENOTDIR,
276 })
265277 } else {
266278 rpkt = (&sshFxpOpenPacket{
267279 ID: p.ID,
445457 osFlags |= os.O_EXCL
446458 }
447459
448 f, err := os.OpenFile(toLocalPath(p.Path), osFlags, 0644)
460 f, err := os.OpenFile(svr.toLocalPath(p.Path), osFlags, 0o644)
449461 if err != nil {
450462 return statusFromError(p.ID, err)
451463 }
483495 b := p.Attrs.([]byte)
484496 var err error
485497
486 p.Path = toLocalPath(p.Path)
498 p.Path = svr.toLocalPath(p.Path)
487499
488500 debug("setstat name \"%s\"", p.Path)
489501 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 }