diff --git a/encoding/ssh/filexfer/extended_packets.go b/encoding/ssh/filexfer/extended_packets.go new file mode 100644 index 0000000..6646f44 --- /dev/null +++ b/encoding/ssh/filexfer/extended_packets.go @@ -0,0 +1,151 @@ +package filexfer + +import ( + "encoding" +) + +// ExtendedData aliases the untyped interface composition of encoding.BinaryMarshaler and encoding.BinaryUnmarshaler. +type ExtendedData = interface { + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler +} + +// ExtendedPacket defines the SSH_FXP_CLOSE packet. +type ExtendedPacket struct { + RequestID uint32 + ExtendedRequest string + + Data ExtendedData +} + +// MarshalPacket returns p as a two-part binary encoding of p. +// +// The Data is marshaled into binary, and returned as the payload. +func (p *ExtendedPacket) MarshalPacket() (header, payload []byte, err error) { + size := 1 + 4 + // byte(type) + uint32(request-id) + 4 + len(p.ExtendedRequest) // string(extended-request) + + b := NewMarshalBuffer(size) + b.AppendUint8(uint8(PacketTypeExtended)) + b.AppendUint32(p.RequestID) + + b.AppendString(p.ExtendedRequest) + + if p.Data != nil { + payload, err = p.Data.MarshalBinary() + if err != nil { + return nil, nil, err + } + } + + b.PutLength(size + len(payload)) + + return b.Bytes(), payload, nil +} + +// MarshalBinary returns p as the binary encoding of p. +func (p *ExtendedPacket) MarshalBinary() ([]byte, error) { + return ComposePacket(p.MarshalPacket()) +} + +// UnmarshalPacketBody unmarshals the packet body from the given Buffer. +// It is assumed that the uint32(request-id) has already been consumed. +// +// If p.Data is nil, and there is request-specific-data, +// then the request-specific-data will be wrapped in a Buffer and assigned to p.Data. +func (p *ExtendedPacket) UnmarshalPacketBody(buf *Buffer) (err error) { + if p.ExtendedRequest, err = buf.ConsumeString(); err != nil { + return err + } + + if buf.Len() > 0 { + if p.Data == nil { + p.Data = new(Buffer) + } + + if err := p.Data.UnmarshalBinary(buf.Bytes()); err != nil { + return err + } + } + + return nil +} + +// UnmarshalBinary unmarshals a full raw packet out of the given data. +// It is assumed that the uint32(length) has already been consumed to receive the data. +// It is also assumed that the uint8(type) has already been consumed to which packet to unmarshal into. +func (p *ExtendedPacket) UnmarshalBinary(data []byte) (err error) { + buf := NewBuffer(data) + + if p.RequestID, err = buf.ConsumeUint32(); err != nil { + return err + } + + return p.UnmarshalPacketBody(buf) +} + +// ExtendedReplyPacket defines the SSH_FXP_CLOSE packet. +type ExtendedReplyPacket struct { + RequestID uint32 + + Data ExtendedData +} + +// MarshalPacket returns p as a two-part binary encoding of p. +// +// The Data is marshaled into binary, and returned as the payload. +func (p *ExtendedReplyPacket) MarshalPacket() (header, payload []byte, err error) { + size := 1 + 4 // byte(type) + uint32(request-id) + + b := NewMarshalBuffer(size) + b.AppendUint8(uint8(PacketTypeExtendedReply)) + b.AppendUint32(p.RequestID) + + if p.Data != nil { + payload, err = p.Data.MarshalBinary() + if err != nil { + return nil, nil, err + } + } + + b.PutLength(size + len(payload)) + + return b.Bytes(), payload, nil +} + +// MarshalBinary returns p as the binary encoding of p. +func (p *ExtendedReplyPacket) MarshalBinary() ([]byte, error) { + return ComposePacket(p.MarshalPacket()) +} + +// UnmarshalPacketBody unmarshals the packet body from the given Buffer. +// It is assumed that the uint32(request-id) has already been consumed. +// +// If p.Data is nil, and there is request-specific-data, +// then the request-specific-data will be wrapped in a Buffer and assigned to p.Data. +func (p *ExtendedReplyPacket) UnmarshalPacketBody(buf *Buffer) (err error) { + if buf.Len() > 0 { + if p.Data == nil { + p.Data = new(Buffer) + } + + if err := p.Data.UnmarshalBinary(buf.Bytes()); err != nil { + return err + } + } + + return nil +} + +// UnmarshalBinary unmarshals a full raw packet out of the given data. +// It is assumed that the uint32(length) has already been consumed to receive the data. +// It is also assumed that the uint8(type) has already been consumed to which packet to unmarshal into. +func (p *ExtendedReplyPacket) UnmarshalBinary(data []byte) (err error) { + buf := NewBuffer(data) + + if p.RequestID, err = buf.ConsumeUint32(); err != nil { + return err + } + + return p.UnmarshalPacketBody(buf) +} diff --git a/encoding/ssh/filexfer/extended_packets_test.go b/encoding/ssh/filexfer/extended_packets_test.go new file mode 100644 index 0000000..e9ca94d --- /dev/null +++ b/encoding/ssh/filexfer/extended_packets_test.go @@ -0,0 +1,265 @@ +package filexfer + +import ( + "bytes" + "testing" +) + +type testExtendedData struct { + value uint8 +} + +func (d *testExtendedData) MarshalBinary() ([]byte, error) { + buf := NewBuffer(make([]byte, 0, 4)) + + buf.AppendUint8(d.value ^ 0x2a) + + return buf.Bytes(), nil +} + +func (d *testExtendedData) UnmarshalBinary(data []byte) error { + buf := NewBuffer(data) + + v, err := buf.ConsumeUint8() + if err != nil { + return err + } + + d.value = v ^ 0x2a + + return nil +} + +func TestExtendedPacketNoData(t *testing.T) { + const ( + id = 42 + extendedRequest = "foo@example" + ) + + p := &ExtendedPacket{ + RequestID: id, + ExtendedRequest: extendedRequest, + } + + data, err := p.MarshalBinary() + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 20, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 11, 'f', 'o', 'o', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', + } + + if !bytes.Equal(data, want) { + t.Fatalf("Marshal() = %X, but wanted %X", data, want) + } + + *p = ExtendedPacket{} + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } + + if p.ExtendedRequest != extendedRequest { + t.Errorf("UnmarshalBinary(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest) + } +} + +func TestExtendedPacketTestData(t *testing.T) { + const ( + id = 42 + extendedRequest = "foo@example" + textValue = 13 + ) + + const value = 13 + + p := &ExtendedPacket{ + RequestID: id, + ExtendedRequest: extendedRequest, + Data: &testExtendedData{ + value: textValue, + }, + } + + data, err := p.MarshalBinary() + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 21, + 200, + 0x00, 0x00, 0x00, 42, + 0x00, 0x00, 0x00, 11, 'f', 'o', 'o', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x27, + } + + if !bytes.Equal(data, want) { + t.Fatalf("Marshal() = %X, but wanted %X", data, want) + } + + *p = ExtendedPacket{ + Data: new(testExtendedData), + } + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } + + if p.ExtendedRequest != extendedRequest { + t.Errorf("UnmarshalBinary(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest) + } + + if data, ok := p.Data.(*testExtendedData); !ok { + t.Errorf("UnmarshalBinary(): Data was type %T, but expected %T", p.Data, data) + + } else if data.value != value { + t.Errorf("UnmarshalBinary(): Data.value was %#x, but expected %#x", data.value, value) + } + + *p = ExtendedPacket{} + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } + + if p.ExtendedRequest != extendedRequest { + t.Errorf("UnmarshalBinary(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest) + } + + wantBuffer := []byte{0x27} + + if data, ok := p.Data.(*Buffer); !ok { + t.Errorf("UnmarshalBinary(): Data was type %T, but expected %T", p.Data, data) + + } else if !bytes.Equal(data.b, wantBuffer) { + t.Errorf("UnmarshalBinary(): Data was %X, but expected %X", data.b, wantBuffer) + } +} + +func TestExtendedReplyNoData(t *testing.T) { + const ( + id = 42 + ) + + p := &ExtendedReplyPacket{ + RequestID: id, + } + + data, err := p.MarshalBinary() + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 5, + 201, + 0x00, 0x00, 0x00, 42, + } + + if !bytes.Equal(data, want) { + t.Fatalf("Marshal() = %X, but wanted %X", data, want) + } + + *p = ExtendedReplyPacket{} + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } +} + +func TestExtendedReplyPacketTestData(t *testing.T) { + const ( + id = 42 + textValue = 13 + ) + + const value = 13 + + p := &ExtendedReplyPacket{ + RequestID: id, + Data: &testExtendedData{ + value: textValue, + }, + } + + data, err := p.MarshalBinary() + if err != nil { + t.Fatal("unexpected error:", err) + } + + want := []byte{ + 0x00, 0x00, 0x00, 6, + 201, + 0x00, 0x00, 0x00, 42, + 0x27, + } + + if !bytes.Equal(data, want) { + t.Fatalf("Marshal() = %X, but wanted %X", data, want) + } + + *p = ExtendedReplyPacket{ + Data: new(testExtendedData), + } + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } + + if data, ok := p.Data.(*testExtendedData); !ok { + t.Errorf("UnmarshalBinary(): Data was type %T, but expected %T", p.Data, data) + + } else if data.value != value { + t.Errorf("UnmarshalBinary(): Data.value was %#x, but expected %#x", data.value, value) + } + + *p = ExtendedReplyPacket{} + + // UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed. + if err := p.UnmarshalBinary(data[5:]); err != nil { + t.Fatal("unexpected error:", err) + } + + if p.RequestID != uint32(id) { + t.Errorf("UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id) + } + + wantBuffer := []byte{0x27} + + if data, ok := p.Data.(*Buffer); !ok { + t.Errorf("UnmarshalBinary(): Data was type %T, but expected %T", p.Data, data) + + } else if !bytes.Equal(data.b, wantBuffer) { + t.Errorf("UnmarshalBinary(): Data was %X, but expected %X", data.b, wantBuffer) + } +}