Skip to content

Commit e5d20f5

Browse files
authored
feat: WithSubsystem (#224)
* feat: better integration with pkg/sftp This would allow users to more easily provide both SCP and SFTP servers to their users. closes #40 Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: fix Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: add tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * feat: sftp refactory * fix: aymans suggestions Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: aymans suggestions Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: make sftp an example instead Signed-off-by: Carlos Alexandro Becker <[email protected]> * chore: update docs * fix: tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: unexport Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: unexport Signed-off-by: Carlos Alexandro Becker <[email protected]> --------- Signed-off-by: Carlos Alexandro Becker <[email protected]>
1 parent 1fbb5ec commit e5d20f5

File tree

7 files changed

+154
-3
lines changed

7 files changed

+154
-3
lines changed

examples/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ require (
99
github.com/charmbracelet/log v0.3.1
1010
github.com/charmbracelet/ssh v0.0.0-20240118173142-6d7cf11c8371
1111
github.com/charmbracelet/wish v0.5.0
12+
github.com/muesli/termenv v0.15.2
13+
github.com/pkg/sftp v1.13.6
1214
github.com/spf13/cobra v1.8.0
1315
golang.org/x/crypto v0.18.0
1416
)
@@ -36,14 +38,14 @@ require (
3638
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3739
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
3840
github.com/kevinburke/ssh_config v1.2.0 // indirect
41+
github.com/kr/fs v0.1.0 // indirect
3942
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
4043
github.com/mattn/go-isatty v0.0.18 // indirect
4144
github.com/mattn/go-localereader v0.0.1 // indirect
4245
github.com/mattn/go-runewidth v0.0.15 // indirect
4346
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
4447
github.com/muesli/cancelreader v0.2.2 // indirect
4548
github.com/muesli/reflow v0.3.0 // indirect
46-
github.com/muesli/termenv v0.15.2 // indirect
4749
github.com/pjbgf/sha1cd v0.3.0 // indirect
4850
github.com/rivo/uniseg v0.2.0 // indirect
4951
github.com/sergi/go-diff v1.1.0 // indirect

examples/go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
6464
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
6565
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
6666
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
67+
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
68+
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
6769
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
6870
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
6971
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -92,6 +94,8 @@ github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
9294
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
9395
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
9496
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
97+
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
98+
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
9599
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
96100
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
97101
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -109,8 +113,11 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
109113
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
110114
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
111115
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
116+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
112117
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
113118
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
119+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
120+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
114121
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
115122
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
116123
github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
@@ -121,6 +128,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
121128
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
122129
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
123130
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
131+
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
124132
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
125133
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
126134
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
@@ -135,6 +143,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
135143
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
136144
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
137145
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
146+
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
138147
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
139148
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
140149
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
@@ -163,6 +172,7 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
163172
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
164173
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
165174
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
175+
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
166176
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
167177
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
168178
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
@@ -191,5 +201,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
191201
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
192202
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
193203
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
204+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
194205
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
195206
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

examples/scp/main.go

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"io"
10+
"io/fs"
911
"os"
1012
"os/signal"
13+
"path/filepath"
1114
"syscall"
1215
"time"
1316

1417
"github.com/charmbracelet/log"
1518
"github.com/charmbracelet/ssh"
1619
"github.com/charmbracelet/wish"
1720
"github.com/charmbracelet/wish/scp"
21+
"github.com/pkg/sftp"
1822
)
1923

2024
const (
@@ -23,10 +27,12 @@ const (
2327
)
2428

2529
func main() {
26-
handler := scp.NewFileSystemHandler("./examples/scp/testdata")
30+
root, _ := filepath.Abs("./examples/scp/testdata")
31+
handler := scp.NewFileSystemHandler(root)
2732
s, err := wish.NewServer(
2833
wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
2934
wish.WithHostKeyPath(".ssh/term_info_ed25519"),
35+
wish.WithSubsystem("sftp", sftpSubsystem(root)),
3036
wish.WithMiddleware(
3137
scp.Middleware(handler, handler),
3238
),
@@ -53,3 +59,109 @@ func main() {
5359
log.Error("could not stop server", "error", err)
5460
}
5561
}
62+
63+
func sftpSubsystem(root string) ssh.SubsystemHandler {
64+
return func(s ssh.Session) {
65+
log.Info("sftp", "root", root)
66+
fs := &sftpHandler{root}
67+
srv := sftp.NewRequestServer(s, sftp.Handlers{
68+
FileList: fs,
69+
FileGet: fs,
70+
})
71+
if err := srv.Serve(); err == io.EOF {
72+
if err := srv.Close(); err != nil {
73+
wish.Fatalln(s, "sftp:", err)
74+
}
75+
} else if err != nil {
76+
wish.Fatalln(s, "sftp:", err)
77+
}
78+
}
79+
}
80+
81+
// Example readonly handler implementation for sftp.
82+
//
83+
// other example implementations:
84+
// - https://github.com/gravitational/teleport/blob/f57dc2fe2a9900ec198779aae747ac4f833b278d/tool/teleport/common/sftp.go
85+
// - https://github.com/minio/minio/blob/c66c5828eacb4a7fa9a49b4c890c77dd8684b171/cmd/sftp-server.go
86+
type sftpHandler struct {
87+
root string
88+
}
89+
90+
var (
91+
_ sftp.FileLister = &sftpHandler{}
92+
_ sftp.FileReader = &sftpHandler{}
93+
)
94+
95+
type listerAt []fs.FileInfo
96+
97+
func (l listerAt) ListAt(ls []fs.FileInfo, offset int64) (int, error) {
98+
if offset >= int64(len(l)) {
99+
return 0, io.EOF
100+
}
101+
n := copy(ls, l[offset:])
102+
if n < len(ls) {
103+
return n, io.EOF
104+
}
105+
return n, nil
106+
}
107+
108+
// Fileread implements sftp.FileReader.
109+
func (s *sftpHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
110+
var flags int
111+
pflags := r.Pflags()
112+
if pflags.Append {
113+
flags |= os.O_APPEND
114+
}
115+
if pflags.Creat {
116+
flags |= os.O_CREATE
117+
}
118+
if pflags.Excl {
119+
flags |= os.O_EXCL
120+
}
121+
if pflags.Trunc {
122+
flags |= os.O_TRUNC
123+
}
124+
125+
if pflags.Read && pflags.Write {
126+
flags |= os.O_RDWR
127+
} else if pflags.Read {
128+
flags |= os.O_RDONLY
129+
} else if pflags.Write {
130+
flags |= os.O_WRONLY
131+
}
132+
133+
f, err := os.OpenFile(filepath.Join(s.root, r.Filepath), flags, 0600)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
return f, nil
139+
}
140+
141+
// Filelist implements sftp.FileLister.
142+
func (s *sftpHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
143+
switch r.Method {
144+
case "List":
145+
entries, err := os.ReadDir(filepath.Join(s.root, r.Filepath))
146+
if err != nil {
147+
return nil, fmt.Errorf("sftp: %w", err)
148+
}
149+
infos := make([]fs.FileInfo, len(entries))
150+
for i, entry := range entries {
151+
info, err := entry.Info()
152+
if err != nil {
153+
return nil, err
154+
}
155+
infos[i] = info
156+
}
157+
return listerAt(infos), nil
158+
case "Stat":
159+
fi, err := os.Stat(filepath.Join(s.root, r.Filepath))
160+
if err != nil {
161+
return nil, err
162+
}
163+
return listerAt{fi}, nil
164+
default:
165+
return nil, sftp.ErrSSHFxOpUnsupported
166+
}
167+
}

options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,15 @@ func WithMaxTimeout(d time.Duration) ssh.Option {
199199
return nil
200200
}
201201
}
202+
203+
// WithSubsystem returns an ssh.Option that sets the subsystem
204+
// handler for a given protocol.
205+
func WithSubsystem(key string, h ssh.SubsystemHandler) ssh.Option {
206+
return func(s *ssh.Server) error {
207+
if s.SubsystemHandlers == nil {
208+
s.SubsystemHandlers = map[string]ssh.SubsystemHandler{}
209+
}
210+
s.SubsystemHandlers[key] = h
211+
return nil
212+
}
213+
}

options_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ import (
1313
gossh "golang.org/x/crypto/ssh"
1414
)
1515

16+
func TestWithSubsystem(t *testing.T) {
17+
srv := &ssh.Server{
18+
Handler: func(s ssh.Session) {},
19+
}
20+
requireNoError(t, WithSubsystem("foo", func(s ssh.Session) {})(srv))
21+
if srv.SubsystemHandlers == nil {
22+
t.Fatalf("should not have been nil")
23+
}
24+
if _, ok := srv.SubsystemHandlers["foo"]; !ok {
25+
t.Fatalf("should have set the foo subsystem handler")
26+
}
27+
}
28+
1629
func TestWithBanner(t *testing.T) {
1730
const banner = "a banner"
1831
var got string

scp/filesystem.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/charmbracelet/ssh"
1313
)
1414

15+
// fileSystemHandler is a Handler implementation for a given root path.
1516
type fileSystemHandler struct{ root string }
1617

1718
var _ Handler = &fileSystemHandler{}

scp/filesystem_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func TestFilesystem(t *testing.T) {
262262
}
263263

264264
func chtimesTree(tb testing.TB, dir string, atime, mtime time.Time) {
265-
is.New(tb).NoErr(filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
265+
is.New(tb).NoErr(filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error {
266266
if err != nil {
267267
return err
268268
}

0 commit comments

Comments
 (0)