blob: 60b16a0ef4c13ea9441e8795c5e4fd957ffa6ae7 [file] [edit]
package sftp
import (
"context"
"errors"
"io"
"net"
"sync"
"testing"
"time"
sshfx "github.com/pkg/sftp/v2/encoding/ssh/filexfer"
)
// testFile is an in-memory FileHandler that implements none of the optional FSETSTAT interfaces.
type testFile struct {
mu sync.Mutex
name string
data []byte
}
func (f *testFile) Close() error { return nil }
func (f *testFile) Name() string { return f.name }
func (f *testFile) Handle() string { return f.name }
func (f *testFile) Sync() error { return nil }
func (f *testFile) Stat() (*sshfx.Attributes, error) {
f.mu.Lock()
defer f.mu.Unlock()
attrs := new(sshfx.Attributes)
attrs.SetSize(uint64(len(f.data)))
return attrs, nil
}
func (f *testFile) ReadAt(b []byte, off int64) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
if off >= int64(len(f.data)) {
return 0, io.EOF
}
n := copy(b, f.data[off:])
if n < len(b) {
return n, io.EOF
}
return n, nil
}
func (f *testFile) WriteAt(b []byte, off int64) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
if end := off + int64(len(b)); end > int64(len(f.data)) {
f.data = append(f.data, make([]byte, end-int64(len(f.data)))...)
}
return copy(f.data[off:], b), nil
}
func (f *testFile) contents() string {
f.mu.Lock()
defer f.mu.Unlock()
return string(f.data)
}
// truncatingFile only implements TruncateFileHandler out of the optional FSETSTAT interfaces.
type truncatingFile struct {
*testFile
}
func (f truncatingFile) Truncate(size int64) error {
f.mu.Lock()
defer f.mu.Unlock()
f.data = f.data[:size]
return nil
}
type testHandler struct {
UnimplementedServerHandler
file FileHandler
}
func (h *testHandler) Open(_ context.Context, req *sshfx.OpenPacket) (FileHandler, error) {
if h.file == nil {
return nil, sshfx.StatusNoSuchFile
}
return h.file, nil
}
// serveTestClient starts srv on one end of a pipe, and returns a client connected to the other end.
func serveTestClient(t *testing.T, srv *Server) *Client {
t.Helper()
srvConn, clConn := net.Pipe()
done := make(chan error, 1)
go func() {
done <- srv.Serve(srvConn)
}()
cl, err := NewClientPipe(t.Context(), clConn, clConn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cl.Close()
<-done
})
return cl
}
func TestServerFSetStatFallback(t *testing.T) {
f := &testFile{name: "foo", data: []byte("hello world")}
cl := serveTestClient(t, &Server{
Handler: &testHandler{file: truncatingFile{f}},
})
file, err := cl.OpenFile("foo", OpenFlagReadWrite, 0)
if err != nil {
t.Fatal(err)
}
defer file.Close()
if err := file.Truncate(5); err != nil {
t.Fatal("Truncate:", err)
}
if got := f.contents(); got != "hello" {
t.Errorf("after Truncate(5), file contains %q, but wanted %q", got, "hello")
}
if err := file.Chmod(0o600); !errors.Is(err, sshfx.StatusOpUnsupported) {
t.Errorf("Chmod on a file without ChmodFileHandler returned %v, but wanted %v", err, sshfx.StatusOpUnsupported)
}
}
func TestServerHijack(t *testing.T) {
srv := &Server{
Handler: &testHandler{},
}
var got string
err := Hijack(srv, func(_ context.Context, req *sshfx.MkdirPacket) error {
got = req.Path
return nil
})
if err != nil {
t.Fatal("Hijack:", err)
}
cl := serveTestClient(t, srv)
if err := cl.Mkdir("foo", 0o755); err != nil {
t.Fatal("Mkdir:", err)
}
if got != "foo" {
t.Errorf("hijacked mkdir got path %q, but wanted %q", got, "foo")
}
}
func TestServerHijackWithResponse(t *testing.T) {
srv := &Server{
Handler: &testHandler{},
}
err := HijackWithResponse(srv, func(_ context.Context, req *sshfx.StatPacket) (*sshfx.AttrsPacket, error) {
if req.Path != "foo" {
return nil, sshfx.StatusNoSuchFile
}
resp := new(sshfx.AttrsPacket)
resp.Attrs.SetSize(42)
return resp, nil
})
if err != nil {
t.Fatal("HijackWithResponse:", err)
}
cl := serveTestClient(t, srv)
fi, err := cl.Stat("foo")
if err != nil {
t.Fatal("Stat:", err)
}
if fi.Size() != 42 {
t.Errorf("hijacked stat returned size %d, but wanted 42", fi.Size())
}
if _, err := cl.Stat("bar"); !errors.Is(err, sshfx.StatusNoSuchFile) {
t.Errorf("hijacked stat of a missing file returned %v, but wanted %v", err, sshfx.StatusNoSuchFile)
}
}
func TestServerGracefulStopBeforeServe(t *testing.T) {
srv := &Server{
Handler: &testHandler{},
}
if err := srv.GracefulStop(); err != nil {
t.Error("GracefulStop:", err)
}
}
// expectWithin fails the test if fn does not return within a second.
func expectWithin(t *testing.T, what string, fn func()) {
t.Helper()
done := make(chan struct{})
go func() {
defer close(done)
fn()
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal(what, "did not return")
}
}
func TestServerServeTwice(t *testing.T) {
srv := &Server{
Handler: &testHandler{},
}
cl := serveTestClient(t, srv)
conn, _ := net.Pipe()
defer conn.Close()
if err := srv.Serve(conn); err == nil {
t.Fatal("expected an error from serving twice")
}
// The first Serve is still running, so the server must still answer requests.
if _, err := cl.RealPath("."); !errors.Is(err, sshfx.StatusOpUnsupported) {
t.Fatalf("RealPath returned %v, but wanted %v", err, sshfx.StatusOpUnsupported)
}
cl.Close()
expectWithin(t, "GracefulStop", func() {
srv.GracefulStop()
})
}
func TestServerGracefulStopWithOpenFile(t *testing.T) {
srv := &Server{
Handler: &testHandler{file: &testFile{name: "foo"}},
}
cl := serveTestClient(t, srv)
if _, err := cl.Open("foo"); err != nil {
t.Fatal(err)
}
// Leave the file open, and end the session so that Serve returns.
cl.Close()
expectWithin(t, "GracefulStop", func() {
if err := srv.GracefulStop(); err != nil {
t.Error("GracefulStop:", err)
}
})
}