From 0ee29e31ddcc81f541de7459b0a5e40dfa552672 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 19 Nov 2021 19:54:49 +0100 Subject: Working on SFTP --- internal/encoding/ssh/filexfer/openssh/fsync.go | 73 ++++++ .../encoding/ssh/filexfer/openssh/fsync_test.go | 62 +++++ internal/encoding/ssh/filexfer/openssh/hardlink.go | 79 +++++++ .../encoding/ssh/filexfer/openssh/hardlink_test.go | 69 ++++++ internal/encoding/ssh/filexfer/openssh/openssh.go | 2 + .../encoding/ssh/filexfer/openssh/posix-rename.go | 79 +++++++ .../ssh/filexfer/openssh/posix-rename_test.go | 69 ++++++ internal/encoding/ssh/filexfer/openssh/statvfs.go | 256 +++++++++++++++++++++ .../encoding/ssh/filexfer/openssh/statvfs_test.go | 239 +++++++++++++++++++ 9 files changed, 928 insertions(+) create mode 100644 internal/encoding/ssh/filexfer/openssh/fsync.go create mode 100644 internal/encoding/ssh/filexfer/openssh/fsync_test.go create mode 100644 internal/encoding/ssh/filexfer/openssh/hardlink.go create mode 100644 internal/encoding/ssh/filexfer/openssh/hardlink_test.go create mode 100644 internal/encoding/ssh/filexfer/openssh/openssh.go create mode 100644 internal/encoding/ssh/filexfer/openssh/posix-rename.go create mode 100644 internal/encoding/ssh/filexfer/openssh/posix-rename_test.go create mode 100644 internal/encoding/ssh/filexfer/openssh/statvfs.go create mode 100644 internal/encoding/ssh/filexfer/openssh/statvfs_test.go (limited to 'internal/encoding/ssh/filexfer/openssh') diff --git a/internal/encoding/ssh/filexfer/openssh/fsync.go b/internal/encoding/ssh/filexfer/openssh/fsync.go new file mode 100644 index 0000000..7ecfb0c --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/fsync.go @@ -0,0 +1,73 @@ +package openssh + +import ( + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +const extensionFSync = "fsync@openssh.com" + +// RegisterExtensionFSync registers the "fsync@openssh.com" extended packet with the encoding/ssh/filexfer package. +func RegisterExtensionFSync() { + sshfx.RegisterExtendedPacketType(extensionFSync, func() sshfx.ExtendedData { + return new(FSyncExtendedPacket) + }) +} + +// ExtensionFSync returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket. +func ExtensionFSync() *sshfx.ExtensionPair { + return &sshfx.ExtensionPair{ + Name: extensionFSync, + Data: "1", + } +} + +// FSyncExtendedPacket defines the fsync@openssh.com extend packet. +type FSyncExtendedPacket struct { + Handle string +} + +// Type returns the SSH_FXP_EXTENDED packet type. +func (ep *FSyncExtendedPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtended +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended packet. +func (ep *FSyncExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedPacket{ + ExtendedRequest: extensionFSync, + + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// MarshalInto encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data. +func (ep *FSyncExtendedPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendString(ep.Handle) +} + +// MarshalBinary encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet. +func (ep *FSyncExtendedPacket) MarshalBinary() ([]byte, error) { + // string(handle) + size := 4 + len(ep.Handle) + + buf := sshfx.NewBuffer(make([]byte, 0, size)) + ep.MarshalInto(buf) + return buf.Bytes(), nil +} + +// UnmarshalFrom decodes the fsync@openssh.com extended packet-specific data from buf. +func (ep *FSyncExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.Handle, err = buf.ConsumeString(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the fsync@openssh.com extended packet-specific data into ep. +func (ep *FSyncExtendedPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} diff --git a/internal/encoding/ssh/filexfer/openssh/fsync_test.go b/internal/encoding/ssh/filexfer/openssh/fsync_test.go new file mode 100644 index 0000000..f9e878f --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/fsync_test.go @@ -0,0 +1,62 @@ +package openssh + +import ( + "bytes" + "testing" + + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +var _ sshfx.PacketMarshaller = &FSyncExtendedPacket{} + +func init() { + RegisterExtensionFSync() +} + +func TestFSyncExtendedPacket(t *testing.T) { + const ( + id = 42 + handle = "somehandle" + ) + + ep := &FSyncExtendedPacket{ + Handle: handle, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 40, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 17, 'f', 's', 'y', 'n', 'c', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e', + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + var p sshfx.ExtendedPacket + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.ExtendedRequest != extensionFSync { + t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFSync) + } + + ep, ok := p.Data.(*FSyncExtendedPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FSyncExtendedPacket", p.Data) + } + + if ep.Handle != handle { + t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", ep.Handle, handle) + } +} diff --git a/internal/encoding/ssh/filexfer/openssh/hardlink.go b/internal/encoding/ssh/filexfer/openssh/hardlink.go new file mode 100644 index 0000000..17c3499 --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/hardlink.go @@ -0,0 +1,79 @@ +package openssh + +import ( + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +const extensionHardlink = "hardlink@openssh.com" + +// RegisterExtensionHardlink registers the "hardlink@openssh.com" extended packet with the encoding/ssh/filexfer package. +func RegisterExtensionHardlink() { + sshfx.RegisterExtendedPacketType(extensionHardlink, func() sshfx.ExtendedData { + return new(HardlinkExtendedPacket) + }) +} + +// ExtensionHardlink returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket. +func ExtensionHardlink() *sshfx.ExtensionPair { + return &sshfx.ExtensionPair{ + Name: extensionHardlink, + Data: "1", + } +} + +// HardlinkExtendedPacket defines the hardlink@openssh.com extend packet. +type HardlinkExtendedPacket struct { + OldPath string + NewPath string +} + +// Type returns the SSH_FXP_EXTENDED packet type. +func (ep *HardlinkExtendedPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtended +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended packet. +func (ep *HardlinkExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedPacket{ + ExtendedRequest: extensionHardlink, + + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data. +func (ep *HardlinkExtendedPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendString(ep.OldPath) + buf.AppendString(ep.NewPath) +} + +// MarshalBinary encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet. +func (ep *HardlinkExtendedPacket) MarshalBinary() ([]byte, error) { + // string(oldpath) + string(newpath) + size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath) + + buf := sshfx.NewBuffer(make([]byte, 0, size)) + ep.MarshalInto(buf) + return buf.Bytes(), nil +} + +// UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf. +func (ep *HardlinkExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.OldPath, err = buf.ConsumeString(); err != nil { + return err + } + + if ep.NewPath, err = buf.ConsumeString(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep. +func (ep *HardlinkExtendedPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} diff --git a/internal/encoding/ssh/filexfer/openssh/hardlink_test.go b/internal/encoding/ssh/filexfer/openssh/hardlink_test.go new file mode 100644 index 0000000..5d3be06 --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/hardlink_test.go @@ -0,0 +1,69 @@ +package openssh + +import ( + "bytes" + "testing" + + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +var _ sshfx.PacketMarshaller = &HardlinkExtendedPacket{} + +func init() { + RegisterExtensionHardlink() +} + +func TestHardlinkExtendedPacket(t *testing.T) { + const ( + id = 42 + oldpath = "/foo" + newpath = "/bar" + ) + + ep := &HardlinkExtendedPacket{ + OldPath: oldpath, + NewPath: newpath, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 45, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 20, 'h', 'a', 'r', 'd', 'l', 'i', 'n', 'k', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o', + 0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r', + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + var p sshfx.ExtendedPacket + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.ExtendedRequest != extensionHardlink { + t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionHardlink) + } + + ep, ok := p.Data.(*HardlinkExtendedPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *HardlinkExtendedPacket", p.Data) + } + + if ep.OldPath != oldpath { + t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath) + } + + if ep.NewPath != newpath { + t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath) + } +} diff --git a/internal/encoding/ssh/filexfer/openssh/openssh.go b/internal/encoding/ssh/filexfer/openssh/openssh.go new file mode 100644 index 0000000..f93ff17 --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/openssh.go @@ -0,0 +1,2 @@ +// Package openssh implements the openssh secsh-filexfer extensions as described in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL +package openssh diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename.go b/internal/encoding/ssh/filexfer/openssh/posix-rename.go new file mode 100644 index 0000000..a3d3de5 --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/posix-rename.go @@ -0,0 +1,79 @@ +package openssh + +import ( + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +const extensionPosixRename = "posix-rename@openssh.com" + +// RegisterExtensionPosixRename registers the "posix-rename@openssh.com" extended packet with the encoding/ssh/filexfer package. +func RegisterExtensionPosixRename() { + sshfx.RegisterExtendedPacketType(extensionPosixRename, func() sshfx.ExtendedData { + return new(PosixRenameExtendedPacket) + }) +} + +// ExtensionPosixRename returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket. +func ExtensionPosixRename() *sshfx.ExtensionPair { + return &sshfx.ExtensionPair{ + Name: extensionPosixRename, + Data: "1", + } +} + +// PosixRenameExtendedPacket defines the posix-rename@openssh.com extend packet. +type PosixRenameExtendedPacket struct { + OldPath string + NewPath string +} + +// Type returns the SSH_FXP_EXTENDED packet type. +func (ep *PosixRenameExtendedPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtended +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended packet. +func (ep *PosixRenameExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedPacket{ + ExtendedRequest: extensionPosixRename, + + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data. +func (ep *PosixRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendString(ep.OldPath) + buf.AppendString(ep.NewPath) +} + +// MarshalBinary encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet. +func (ep *PosixRenameExtendedPacket) MarshalBinary() ([]byte, error) { + // string(oldpath) + string(newpath) + size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath) + + buf := sshfx.NewBuffer(make([]byte, 0, size)) + ep.MarshalInto(buf) + return buf.Bytes(), nil +} + +// UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf. +func (ep *PosixRenameExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.OldPath, err = buf.ConsumeString(); err != nil { + return err + } + + if ep.NewPath, err = buf.ConsumeString(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep. +func (ep *PosixRenameExtendedPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} diff --git a/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go new file mode 100644 index 0000000..6bdb10d --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go @@ -0,0 +1,69 @@ +package openssh + +import ( + "bytes" + "testing" + + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +var _ sshfx.PacketMarshaller = &PosixRenameExtendedPacket{} + +func init() { + RegisterExtensionPosixRename() +} + +func TestPosixRenameExtendedPacket(t *testing.T) { + const ( + id = 42 + oldpath = "/foo" + newpath = "/bar" + ) + + ep := &PosixRenameExtendedPacket{ + OldPath: oldpath, + NewPath: newpath, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 49, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 24, 'p', 'o', 's', 'i', 'x', '-', 'r', 'e', 'n', 'a', 'm', 'e', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o', + 0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r', + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + var p sshfx.ExtendedPacket + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.ExtendedRequest != extensionPosixRename { + t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionPosixRename) + } + + ep, ok := p.Data.(*PosixRenameExtendedPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *PosixRenameExtendedPacket", p.Data) + } + + if ep.OldPath != oldpath { + t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath) + } + + if ep.NewPath != newpath { + t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath) + } +} diff --git a/internal/encoding/ssh/filexfer/openssh/statvfs.go b/internal/encoding/ssh/filexfer/openssh/statvfs.go new file mode 100644 index 0000000..3e9015f --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/statvfs.go @@ -0,0 +1,256 @@ +package openssh + +import ( + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +const extensionStatVFS = "statvfs@openssh.com" + +// RegisterExtensionStatVFS registers the "statvfs@openssh.com" extended packet with the encoding/ssh/filexfer package. +func RegisterExtensionStatVFS() { + sshfx.RegisterExtendedPacketType(extensionStatVFS, func() sshfx.ExtendedData { + return new(StatVFSExtendedPacket) + }) +} + +// ExtensionStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket. +func ExtensionStatVFS() *sshfx.ExtensionPair { + return &sshfx.ExtensionPair{ + Name: extensionStatVFS, + Data: "2", + } +} + +// StatVFSExtendedPacket defines the statvfs@openssh.com extend packet. +type StatVFSExtendedPacket struct { + Path string +} + +// Type returns the SSH_FXP_EXTENDED packet type. +func (ep *StatVFSExtendedPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtended +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended packet. +func (ep *StatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedPacket{ + ExtendedRequest: extensionStatVFS, + + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data. +func (ep *StatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendString(ep.Path) +} + +// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet. +func (ep *StatVFSExtendedPacket) MarshalBinary() ([]byte, error) { + size := 4 + len(ep.Path) // string(path) + + buf := sshfx.NewBuffer(make([]byte, 0, size)) + + ep.MarshalInto(buf) + + return buf.Bytes(), nil +} + +// UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep. +func (ep *StatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.Path, err = buf.ConsumeString(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep. +func (ep *StatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} + +const extensionFStatVFS = "fstatvfs@openssh.com" + +// RegisterExtensionFStatVFS registers the "fstatvfs@openssh.com" extended packet with the encoding/ssh/filexfer package. +func RegisterExtensionFStatVFS() { + sshfx.RegisterExtendedPacketType(extensionFStatVFS, func() sshfx.ExtendedData { + return new(FStatVFSExtendedPacket) + }) +} + +// ExtensionFStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket. +func ExtensionFStatVFS() *sshfx.ExtensionPair { + return &sshfx.ExtensionPair{ + Name: extensionFStatVFS, + Data: "2", + } +} + +// FStatVFSExtendedPacket defines the fstatvfs@openssh.com extend packet. +type FStatVFSExtendedPacket struct { + Path string +} + +// Type returns the SSH_FXP_EXTENDED packet type. +func (ep *FStatVFSExtendedPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtended +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended packet. +func (ep *FStatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedPacket{ + ExtendedRequest: extensionFStatVFS, + + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data. +func (ep *FStatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendString(ep.Path) +} + +// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet. +func (ep *FStatVFSExtendedPacket) MarshalBinary() ([]byte, error) { + size := 4 + len(ep.Path) // string(path) + + buf := sshfx.NewBuffer(make([]byte, 0, size)) + + ep.MarshalInto(buf) + + return buf.Bytes(), nil +} + +// UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep. +func (ep *FStatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.Path, err = buf.ConsumeString(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep. +func (ep *FStatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} + +// The values for the MountFlags field. +// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL +const ( + MountFlagsReadOnly = 0x1 // SSH_FXE_STATVFS_ST_RDONLY + MountFlagsNoSUID = 0x2 // SSH_FXE_STATVFS_ST_NOSUID +) + +// StatVFSExtendedReplyPacket defines the extended reply packet for statvfs@openssh.com and fstatvfs@openssh.com requests. +type StatVFSExtendedReplyPacket struct { + BlockSize uint64 /* f_bsize: file system block size */ + FragmentSize uint64 /* f_frsize: fundamental fs block size / fagment size */ + Blocks uint64 /* f_blocks: number of blocks (unit f_frsize) */ + BlocksFree uint64 /* f_bfree: free blocks in filesystem */ + BlocksAvail uint64 /* f_bavail: free blocks for non-root */ + Files uint64 /* f_files: total file inodes */ + FilesFree uint64 /* f_ffree: free file inodes */ + FilesAvail uint64 /* f_favail: free file inodes for to non-root */ + FilesystemID uint64 /* f_fsid: file system id */ + MountFlags uint64 /* f_flag: bit mask of mount flag values */ + MaxNameLength uint64 /* f_namemax: maximum filename length */ +} + +// Type returns the SSH_FXP_EXTENDED_REPLY packet type. +func (ep *StatVFSExtendedReplyPacket) Type() sshfx.PacketType { + return sshfx.PacketTypeExtendedReply +} + +// MarshalPacket returns ep as a two-part binary encoding of the full extended reply packet. +func (ep *StatVFSExtendedReplyPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) { + p := &sshfx.ExtendedReplyPacket{ + Data: ep, + } + return p.MarshalPacket(reqid, b) +} + +// UnmarshalPacketBody returns ep as a two-part binary encoding of the full extended reply packet. +func (ep *StatVFSExtendedReplyPacket) UnmarshalPacketBody(buf *sshfx.Buffer) (err error) { + p := &sshfx.ExtendedReplyPacket{ + Data: ep, + } + return p.UnmarshalPacketBody(buf) +} + +// MarshalInto encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data. +func (ep *StatVFSExtendedReplyPacket) MarshalInto(buf *sshfx.Buffer) { + buf.AppendUint64(ep.BlockSize) + buf.AppendUint64(ep.FragmentSize) + buf.AppendUint64(ep.Blocks) + buf.AppendUint64(ep.BlocksFree) + buf.AppendUint64(ep.BlocksAvail) + buf.AppendUint64(ep.Files) + buf.AppendUint64(ep.FilesFree) + buf.AppendUint64(ep.FilesAvail) + buf.AppendUint64(ep.FilesystemID) + buf.AppendUint64(ep.MountFlags) + buf.AppendUint64(ep.MaxNameLength) +} + +// MarshalBinary encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data. +// +// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended reply packet. +func (ep *StatVFSExtendedReplyPacket) MarshalBinary() ([]byte, error) { + size := 11 * 8 // 11 × uint64(various) + + b := sshfx.NewBuffer(make([]byte, 0, size)) + ep.MarshalInto(b) + return b.Bytes(), nil +} + +// UnmarshalFrom decodes the fstatvfs@openssh.com extended reply packet-specific data into ep. +func (ep *StatVFSExtendedReplyPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) { + if ep.BlockSize, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.FragmentSize, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.Blocks, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.BlocksFree, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.BlocksAvail, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.Files, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.FilesFree, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.FilesAvail, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.FilesystemID, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.MountFlags, err = buf.ConsumeUint64(); err != nil { + return err + } + if ep.MaxNameLength, err = buf.ConsumeUint64(); err != nil { + return err + } + + return nil +} + +// UnmarshalBinary decodes the fstatvfs@openssh.com extended reply packet-specific data into ep. +func (ep *StatVFSExtendedReplyPacket) UnmarshalBinary(data []byte) (err error) { + return ep.UnmarshalFrom(sshfx.NewBuffer(data)) +} diff --git a/internal/encoding/ssh/filexfer/openssh/statvfs_test.go b/internal/encoding/ssh/filexfer/openssh/statvfs_test.go new file mode 100644 index 0000000..014aa63 --- /dev/null +++ b/internal/encoding/ssh/filexfer/openssh/statvfs_test.go @@ -0,0 +1,239 @@ +package openssh + +import ( + "bytes" + "testing" + + sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" +) + +var _ sshfx.PacketMarshaller = &StatVFSExtendedPacket{} + +func init() { + RegisterExtensionStatVFS() +} + +func TestStatVFSExtendedPacket(t *testing.T) { + const ( + id = 42 + path = "/foo" + ) + + ep := &StatVFSExtendedPacket{ + Path: path, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 36, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 19, 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o', + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + var p sshfx.ExtendedPacket + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.ExtendedRequest != extensionStatVFS { + t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionStatVFS) + } + + ep, ok := p.Data.(*StatVFSExtendedPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedPacket", p.Data) + } + + if ep.Path != path { + t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path) + } +} + +var _ sshfx.PacketMarshaller = &FStatVFSExtendedPacket{} + +func init() { + RegisterExtensionFStatVFS() +} + +func TestFStatVFSExtendedPacket(t *testing.T) { + const ( + id = 42 + path = "/foo" + ) + + ep := &FStatVFSExtendedPacket{ + Path: path, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 37, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 20, 'f', 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm', + 0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o', + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + var p sshfx.ExtendedPacket + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.ExtendedRequest != extensionFStatVFS { + t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFStatVFS) + } + + ep, ok := p.Data.(*FStatVFSExtendedPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FStatVFSExtendedPacket", p.Data) + } + + if ep.Path != path { + t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path) + } +} + +var _ sshfx.Packet = &StatVFSExtendedReplyPacket{} + +func TestStatVFSExtendedReplyPacket(t *testing.T) { + const ( + id = 42 + path = "/foo" + ) + + const ( + BlockSize = uint64(iota + 13) + FragmentSize + Blocks + BlocksFree + BlocksAvail + Files + FilesFree + FilesAvail + FilesystemID + MountFlags + MaxNameLength + ) + + ep := &StatVFSExtendedReplyPacket{ + BlockSize: BlockSize, + FragmentSize: FragmentSize, + Blocks: Blocks, + BlocksFree: BlocksFree, + BlocksAvail: BlocksAvail, + Files: Files, + FilesFree: FilesFree, + FilesAvail: FilesAvail, + FilesystemID: FilesystemID, + MountFlags: MountFlags, + MaxNameLength: MaxNameLength, + } + + data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil)) + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 93, + 201, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23, + } + + if !bytes.Equal(data, want) { + t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want) + } + + *ep = StatVFSExtendedReplyPacket{} + + p := sshfx.ExtendedReplyPacket{ + Data: ep, + } + + // UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed. + if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil { + t.Fatal("unexpected error:", err) + } + + ep, ok := p.Data.(*StatVFSExtendedReplyPacket) + if !ok { + t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedReplyPacket", p.Data) + } + + if ep.BlockSize != BlockSize { + t.Errorf("UnmarshalPacketBody(): BlockSize was %d, but expected %d", ep.BlockSize, BlockSize) + } + + if ep.FragmentSize != FragmentSize { + t.Errorf("UnmarshalPacketBody(): FragmentSize was %d, but expected %d", ep.FragmentSize, FragmentSize) + } + + if ep.Blocks != Blocks { + t.Errorf("UnmarshalPacketBody(): Blocks was %d, but expected %d", ep.Blocks, Blocks) + } + + if ep.BlocksFree != BlocksFree { + t.Errorf("UnmarshalPacketBody(): BlocksFree was %d, but expected %d", ep.BlocksFree, BlocksFree) + } + + if ep.BlocksAvail != BlocksAvail { + t.Errorf("UnmarshalPacketBody(): BlocksAvail was %d, but expected %d", ep.BlocksAvail, BlocksAvail) + } + + if ep.Files != Files { + t.Errorf("UnmarshalPacketBody(): Files was %d, but expected %d", ep.Files, Files) + } + + if ep.FilesFree != FilesFree { + t.Errorf("UnmarshalPacketBody(): FilesFree was %d, but expected %d", ep.FilesFree, FilesFree) + } + + if ep.FilesAvail != FilesAvail { + t.Errorf("UnmarshalPacketBody(): FilesAvail was %d, but expected %d", ep.FilesAvail, FilesAvail) + } + + if ep.FilesystemID != FilesystemID { + t.Errorf("UnmarshalPacketBody(): FilesystemID was %d, but expected %d", ep.FilesystemID, FilesystemID) + } + + if ep.MountFlags != MountFlags { + t.Errorf("UnmarshalPacketBody(): MountFlags was %d, but expected %d", ep.MountFlags, MountFlags) + } + + if ep.MaxNameLength != MaxNameLength { + t.Errorf("UnmarshalPacketBody(): MaxNameLength was %d, but expected %d", ep.MaxNameLength, MaxNameLength) + } +} -- cgit v1.2.3