wire

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

Documentation

Overview

Package wire is the SFTP version 3 codec: packet framing, message types, and the SSH binary encoding they are built from.

Why this is its own package

It knows nothing about filesystems, servers, or SSH transports. It depends on nothing but the standard library. That is deliberate and it is a structural commitment, not a stylistic one: an SFTP *client* speaks exactly these bytes, and a client belongs somewhere else entirely (a `sftp://` source for a downloader, say). Keeping the codec free of any reference to github.com/go-filesystems/interface or to the server package means moving it is a move, not a rewrite.

The same split is why github.com/go-filesystems/nfs keeps `xdr` and `rpc` out of its server package.

The encoding

SFTP is layered on the SSH binary encoding of RFC 4251 §5, not on XDR, and the difference matters in exactly one place: a string is a 4-byte big-endian length followed by that many bytes with *no padding*. Everything is big-endian. There are no alignment rules, so there is no padding to validate — the one XDR hazard this codec does not have.

The protocol version

This implements draft-ietf-secsh-filexfer-02, "SFTP version 3". Later drafts exist (4 through 6, and the version 6 draft is what the IETF stopped at) but essentially nothing speaks them: OpenSSH's client and server, FileZilla, WinSCP, Cyberduck, libssh, JSch, paramiko and Go's own github.com/pkg/sftp all negotiate 3. Implementing a version nobody speaks would be a way to be correct and unreachable at the same time.

Hostile input

Every byte a Decoder reads came off a network. The decoder therefore never allocates on a length it has not first compared against a ceiling — a 4 GiB length prefix on a 40-byte packet costs one comparison — and every method returns an error rather than panicking, because a panic in a per-connection goroutine takes down every other session in the process.

Index

Constants

View Source
const (
	FxpInit     uint8 = 1
	FxpVersion  uint8 = 2
	FxpOpen     uint8 = 3
	FxpClose    uint8 = 4
	FxpRead     uint8 = 5
	FxpWrite    uint8 = 6
	FxpLstat    uint8 = 7
	FxpFstat    uint8 = 8
	FxpSetstat  uint8 = 9
	FxpFsetstat uint8 = 10
	FxpOpendir  uint8 = 11
	FxpReaddir  uint8 = 12
	FxpRemove   uint8 = 13
	FxpMkdir    uint8 = 14
	FxpRmdir    uint8 = 15
	FxpRealpath uint8 = 16
	FxpStat     uint8 = 17
	FxpRename   uint8 = 18
	FxpReadlink uint8 = 19
	FxpSymlink  uint8 = 20

	FxpStatus uint8 = 101
	FxpHandle uint8 = 102
	FxpData   uint8 = 103
	FxpName   uint8 = 104
	FxpAttrs  uint8 = 105

	FxpExtended      uint8 = 200
	FxpExtendedReply uint8 = 201
)

Packet types (draft-ietf-secsh-filexfer-02 §3, §4).

The numbering has a shape worth knowing when reading a hex dump: 1..20 are requests, 101..105 are replies, and 200/201 are the extension escape hatch.

View Source
const (
	FxfRead   uint32 = 0x00000001
	FxfWrite  uint32 = 0x00000002
	FxfAppend uint32 = 0x00000004
	FxfCreat  uint32 = 0x00000008
	FxfTrunc  uint32 = 0x00000010
	FxfExcl   uint32 = 0x00000020
)

Open flags (SSH_FXF_*), the pflags field of SSH_FXP_OPEN.

View Source
const (
	AttrSize        uint32 = 0x00000001
	AttrUIDGID      uint32 = 0x00000002
	AttrPermissions uint32 = 0x00000004
	AttrACModTime   uint32 = 0x00000008
	AttrExtended    uint32 = 0x80000000
)

Attribute presence flags (SSH_FILEXFER_ATTR_*).

Every field of an ATTRS structure is optional and announced by one of these. That is the whole reason Attributes carries a Flags field rather than being a plain struct of values: "size 0" and "size not stated" are different answers, and conflating them is how a client comes to believe a file is empty.

View Source
const (
	ModeFmt   uint32 = 0o170000
	ModeFifo  uint32 = 0o010000
	ModeChar  uint32 = 0o020000
	ModeDir   uint32 = 0o040000
	ModeBlock uint32 = 0o060000
	ModeReg   uint32 = 0o100000
	ModeLink  uint32 = 0o120000
	ModeSock  uint32 = 0o140000

	// ModePerm masks the permission and setuid/setgid/sticky bits.
	ModePerm uint32 = 0o7777
)

POSIX file-type bits, as they appear in the permissions field of an ATTRS structure.

SFTP version 3 has no separate file-type field: a client learns that something is a directory by masking these out of the permissions, which is why a server that sends permissions without type bits produces a listing where nothing is a directory.

View Source
const DefaultLimit = 256 << 10

DefaultLimit is the ceiling applied to a single variable-length item when a Decoder has no explicit one.

256 KiB is comfortably above the largest thing version 3 legitimately carries: OpenSSH's client reads and writes in 32 KiB chunks and its largest is 256 KiB, and a path cannot sensibly approach either. Anything larger is malformed or hostile.

View Source
const MaxPacket = 260 << 10

MaxPacket is the largest SFTP packet this codec will read or write, counting the type byte and the payload but not the 4-byte length prefix.

The value is not arbitrary. OpenSSH's sftp client sends SSH_FXP_WRITE payloads of 32 KiB by default and can be pushed to 256 KiB with -B; the prefix and the fixed fields around the data add well under 4 KiB. 260 KiB therefore accepts everything a real client sends and rejects everything else before allocating for it.

View Source
const Version uint32 = 3

Version is the SFTP protocol version this codec implements: draft-ietf-secsh-filexfer-02. See the package documentation for why 3 and not a later draft.

Variables

View Source
var (
	// ErrShort reports that the buffer ended in the middle of an item.
	ErrShort = errors.New("wire: buffer too short")
	// ErrLimit reports a length prefix larger than the ceiling in force.
	// It is returned *before* any allocation, which is the entire point:
	// an attacker's 4 GiB length prefix must cost a comparison, not 4 GiB.
	ErrLimit = errors.New("wire: length exceeds limit")
	// ErrTrailing reports bytes left over after a message decoded. A
	// well-formed peer sends none, and accepting them would let two
	// implementations disagree about what they just exchanged while both
	// believe they succeeded.
	ErrTrailing = errors.New("wire: trailing bytes after message")
)

Errors returned by a Decoder.

They are deliberately few: a server turns any of them into the single wire answer SSH_FX_BAD_MESSAGE, so splitting "ran out of bytes" from "length prefix is absurd" buys a caller nothing and costs a reader a branch to think about. They are distinct types only so a *test* can tell which rejection it provoked.

View Source
var ErrPacketEmpty = errors.New("wire: empty packet")

ErrPacketEmpty reports a packet whose length prefix is zero, which leaves no room for the mandatory type byte.

View Source
var ErrPacketTooLarge = errors.New("wire: packet too large")

ErrPacketTooLarge reports a length prefix above the ceiling in force.

It is checked before the body is read, so a hostile 4 GiB prefix costs one comparison rather than 4 GiB of buffer and a stalled connection. The connection is not recoverable afterwards — the stream is now misframed — so a server that sees this must close, not reply.

Functions

func ReadPacket

func ReadPacket(r io.Reader, buf []byte, max int) (typ uint8, payload, next []byte, err error)

ReadPacket reads one packet from r, reusing buf when it is large enough.

It returns the packet type, the payload (which aliases buf and is valid only until the next call), and the possibly-grown buffer for the caller to pass back in. Reusing the buffer is what keeps a bulk transfer from allocating once per 32 KiB chunk.

max caps the packet; a non-positive value means MaxPacket.

io.EOF is returned unwrapped when the stream ends cleanly at a packet boundary, which is how a client says goodbye; a stream that ends *inside* a packet yields io.ErrUnexpectedEOF, and the difference is exactly the one a caller needs to decide whether to log an error.

func Send

func Send(w io.Writer, m Message, buf []byte) ([]byte, error)

Send frames m and writes it to w, reusing and returning buf.

func WritePacket

func WritePacket(w io.Writer, typ uint8, payload []byte, out []byte) ([]byte, error)

WritePacket frames payload as a packet of the given type and writes it to w in a single Write call.

The single call matters: SFTP runs inside an SSH channel, and splitting a packet across two writes turns into two SSH_MSG_CHANNEL_DATA messages, each with its own MAC and its own round of framing. It is also what makes the writer safe to reason about when a caller serialises access with one mutex.

out is a scratch buffer; the possibly-grown buffer is returned for reuse.

Types

type Attributes

type Attributes struct {
	// Flags is the bitmask of ATTR_* constants saying which fields below
	// carry meaning.
	Flags uint32
	// Size is the file length in bytes. Valid when AttrSize is set.
	Size uint64
	// UID and GID are the numeric owner ids. Valid when AttrUIDGID is set.
	UID, GID uint32
	// Permissions carries both the POSIX type bits and the mode bits.
	// Version 3 has no separate file-type field, so the type bits here are
	// the *only* way a client learns that something is a directory. Valid
	// when AttrPermissions is set.
	Permissions uint32
	// Atime and Mtime are seconds since the Unix epoch. Version 3 states
	// them as 32-bit, which runs out in 2106; later drafts widened the
	// field, and this codec does not get to fix that on its own.
	// Valid when AttrACModTime is set.
	Atime, Mtime uint32
	// Extended carries vendor extensions as (type, data) pairs. Valid when
	// AttrExtended is set. Nothing here produces any; they are decoded
	// rather than dropped so that a request carrying one round-trips
	// truthfully instead of being silently truncated.
	Extended []ExtendedPair
}

Attributes is an SFTP version 3 ATTRS structure (draft-ietf-secsh-filexfer-02 §5).

Every field is optional and its presence is announced in Flags. That is why this is not a plain struct of values: "size 0" and "size not stated" are different answers, and a decoder that conflates them tells a client a file is empty when the server never claimed anything of the sort. Read a field only after checking its flag; write one only together with setting it.

func DecodeAttributes

func DecodeAttributes(d *Decoder) (Attributes, error)

DecodeAttributes reads an ATTRS structure.

func (Attributes) Encode

func (a Attributes) Encode(e *Encoder)

Encode writes the ATTRS structure.

func (Attributes) IsDir

func (a Attributes) IsDir() bool

IsDir reports whether the permissions say directory. It answers false when permissions were not stated at all, which is the safe direction: a client that wrongly believes something is a directory will try to enumerate it.

func (a Attributes) IsLink() bool

IsLink reports whether the permissions say symbolic link, on the same terms as Attributes.IsDir.

type AttrsReply

type AttrsReply struct {
	ID    uint32
	Attrs Attributes
}

AttrsReply is SSH_FXP_ATTRS: the answer to STAT, LSTAT and FSTAT.

func DecodeAttrs

func DecodeAttrs(payload []byte) (AttrsReply, error)

DecodeAttrs reads an SSH_FXP_ATTRS payload.

func (AttrsReply) EncodePayload

func (m AttrsReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (AttrsReply) Type

func (AttrsReply) Type() uint8

Type implements Message.

type DataReply

type DataReply struct {
	ID   uint32
	Data []byte
}

DataReply is SSH_FXP_DATA, the answer to READ.

func DecodeData

func DecodeData(payload []byte) (DataReply, error)

DecodeData reads an SSH_FXP_DATA payload.

func (DataReply) EncodePayload

func (m DataReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (DataReply) Type

func (DataReply) Type() uint8

Type implements Message.

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

Decoder reads SSH-encoded values from a byte slice.

Every method returns an error rather than panicking: the bytes come from the network, and a panic in a per-connection goroutine would take down every other session the process is serving.

func NewDecoder

func NewDecoder(buf []byte) *Decoder

NewDecoder returns a Decoder reading buf with DefaultLimit.

func (*Decoder) Blob

func (d *Decoder) Blob() ([]byte, error)

Blob decodes a variable-length string as bytes.

The result is a copy. The Decoder's buffer is a reusable per-connection read buffer, so handing out an alias would let the next request rewrite data the caller is still holding — a bug that shows up as one client's filename appearing in another's reply, long after the code that caused it.

func (*Decoder) Byte

func (d *Decoder) Byte() (uint8, error)

Byte decodes a single byte.

func (*Decoder) End

func (d *Decoder) End() error

End reports ErrTrailing if anything is left undecoded, and nil otherwise. Message decoders call it last so that a peer sending extra bytes is caught rather than silently tolerated.

func (*Decoder) Int64

func (d *Decoder) Int64() (int64, error)

Int64 decodes a signed 64-bit integer.

func (*Decoder) Remaining

func (d *Decoder) Remaining() int

Remaining reports how many undecoded bytes are left.

func (*Decoder) SetLimit

func (d *Decoder) SetLimit(n int)

SetLimit caps the accepted length of any single variable-length item. A non-positive value restores DefaultLimit.

func (*Decoder) String

func (d *Decoder) String() (string, error)

String decodes a variable-length string.

The bytes are returned as-is, with no validation of UTF-8 or of path syntax. That is deliberate: this layer's job is framing. Whether a filename may contain a slash is an SFTP question, answered once, in the server's path code — answering it here would silently change what the layer above thinks it received.

func (*Decoder) Uint32

func (d *Decoder) Uint32() (uint32, error)

Uint32 decodes a 32-bit unsigned integer.

func (*Decoder) Uint64

func (d *Decoder) Uint64() (uint64, error)

Uint64 decodes a 64-bit unsigned integer.

type Encoder

type Encoder struct {
	// contains filtered or unexported fields
}

Encoder accumulates SSH-encoded values in a buffer.

No method returns an error, and that is a design choice with a consequence worth stating: every input is already a valid Go value of a type the encoding can represent, and the buffer grows, so encoding genuinely cannot fail. Reply-building code is therefore free of error checks on a path where nothing can go wrong, which is what keeps the message constructors readable.

func NewEncoder

func NewEncoder(buf []byte) *Encoder

NewEncoder returns an Encoder appending to buf, whose existing contents are discarded. Passing a recycled buffer with spare capacity avoids an allocation per reply.

func (*Encoder) Blob

func (e *Encoder) Blob(b []byte)

Bytes writes a variable-length string: a 4-byte length, then the bytes. There is no padding — this is SSH's encoding, not XDR's.

func (*Encoder) Byte

func (e *Encoder) Byte(v uint8)

Byte encodes a single byte. Packet types and nothing else use this.

func (*Encoder) Bytes

func (e *Encoder) Bytes() []byte

Bytes returns the encoded message. The slice aliases the Encoder's buffer and stays valid only until the next write.

func (*Encoder) Int64

func (e *Encoder) Int64(v int64)

Int64 encodes a signed 64-bit integer. SFTP's signed and unsigned 64-bit values share a representation, so this is a two's-complement reinterpret.

func (*Encoder) Len

func (e *Encoder) Len() int

Len reports how many bytes have been encoded so far.

func (*Encoder) String

func (e *Encoder) String(s string)

String writes an SFTP string, which is wire-identical to Encoder.Blob.

Version 3 says nothing about the character set of a filename, and this encoder imposes none: names go out exactly as the filesystem reported them. Guessing at UTF-8 here would corrupt names on the images this is pointed at, where a FAT32 short name is CP437 and an ISO 9660 name is US-ASCII.

func (*Encoder) Uint32

func (e *Encoder) Uint32(v uint32)

Uint32 encodes a 32-bit unsigned integer.

func (*Encoder) Uint64

func (e *Encoder) Uint64(v uint64)

Uint64 encodes a 64-bit unsigned integer.

type ExtendedPair

type ExtendedPair struct {
	Type, Data string
}

ExtendedPair is one vendor-extension attribute.

type ExtendedReply

type ExtendedReply struct {
	ID   uint32
	Data []byte
}

ExtendedReply is SSH_FXP_EXTENDED_REPLY. Like ExtendedRequest, its body is opaque here.

func DecodeExtendedReply

func DecodeExtendedReply(payload []byte) (ExtendedReply, error)

DecodeExtendedReply reads an SSH_FXP_EXTENDED_REPLY payload.

func (ExtendedReply) EncodePayload

func (m ExtendedReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (ExtendedReply) Type

func (ExtendedReply) Type() uint8

Type implements Message.

type ExtendedRequest

type ExtendedRequest struct {
	ID   uint32
	Name string
	Data []byte
}

ExtendedRequest is SSH_FXP_EXTENDED, the escape hatch by which vendors add operations. Data is left uninterpreted: what it means depends entirely on Name, and this codec does not pretend to know every vendor's.

func DecodeExtended

func DecodeExtended(payload []byte) (ExtendedRequest, error)

DecodeExtended reads an SSH_FXP_EXTENDED payload. The remainder after the name is returned raw, since its shape is the extension's business.

func (ExtendedRequest) EncodePayload

func (m ExtendedRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (ExtendedRequest) Type

func (ExtendedRequest) Type() uint8

Type implements Message.

type FsetstatRequest

type FsetstatRequest struct {
	ID     uint32
	Handle string
	Attrs  Attributes
}

FsetstatRequest is SSH_FXP_FSETSTAT: SETSTAT against an open handle.

func DecodeFsetstat

func DecodeFsetstat(payload []byte) (FsetstatRequest, error)

DecodeFsetstat reads an SSH_FXP_FSETSTAT payload.

func (FsetstatRequest) EncodePayload

func (m FsetstatRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (FsetstatRequest) Type

func (FsetstatRequest) Type() uint8

Type implements Message.

type HandleReply

type HandleReply struct {
	ID     uint32
	Handle string
}

HandleReply is SSH_FXP_HANDLE: the opaque token a client presents for every subsequent operation on what it just opened.

func DecodeHandleReply

func DecodeHandleReply(payload []byte) (HandleReply, error)

DecodeHandleReply reads an SSH_FXP_HANDLE payload.

func (HandleReply) EncodePayload

func (m HandleReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (HandleReply) Type

func (HandleReply) Type() uint8

Type implements Message.

type HandleRequest

type HandleRequest struct {
	PacketType uint8
	ID         uint32
	Handle     string
}

HandleRequest is any request that is an id and a handle: SSH_FXP_CLOSE, SSH_FXP_READDIR and SSH_FXP_FSTAT.

func DecodeHandle

func DecodeHandle(typ uint8, payload []byte) (HandleRequest, error)

DecodeHandle reads the payload of any HandleRequest-shaped message.

func (HandleRequest) EncodePayload

func (m HandleRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (HandleRequest) Type

func (m HandleRequest) Type() uint8

Type implements Message.

type InitRequest

type InitRequest struct {
	Version    uint32
	Extensions []ExtendedPair
}

InitRequest is SSH_FXP_INIT, the first packet a client sends. It has no request id: nothing has been established yet for an id to refer to.

func DecodeInit

func DecodeInit(payload []byte) (InitRequest, error)

DecodeInit reads an SSH_FXP_INIT payload.

Trailing extension pairs are read until the payload is exhausted; unlike every other message there is no count, so "ran out" is the terminator and a half-pair is an error rather than a silent truncation.

func (InitRequest) EncodePayload

func (m InitRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (InitRequest) Type

func (InitRequest) Type() uint8

Type implements Message.

type Message

type Message interface {
	// Type returns the SSH_FXP_* number this message is sent as. For the
	// shared shapes (PathRequest, HandleRequest) it is a field, because the
	// wire format cannot tell you which of the seven it was.
	Type() uint8
	// EncodePayload writes everything after the type byte.
	EncodePayload(e *Encoder)
}

Message is anything that can be sent as an SFTP packet: it knows its own packet type and how to write its payload.

type NameItem

type NameItem struct {
	// Filename is the bare name for a READDIR entry, or the resolved path
	// for a REALPATH answer.
	Filename string
	// Longname is a human-readable line in `ls -l` form. Version 3 leaves
	// its format entirely unspecified, which means no client may parse it
	// and every client displays it — so it is presentation, and the
	// authoritative values are in Attrs.
	Longname string
	Attrs    Attributes
}

NameItem is one entry of a NAME reply.

type NameReply

type NameReply struct {
	ID    uint32
	Items []NameItem
}

NameReply is SSH_FXP_NAME: the answer to READDIR and to REALPATH.

func DecodeName

func DecodeName(payload []byte) (NameReply, error)

DecodeName reads an SSH_FXP_NAME payload.

func (NameReply) EncodePayload

func (m NameReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (NameReply) Type

func (NameReply) Type() uint8

Type implements Message.

type OpenRequest

type OpenRequest struct {
	ID     uint32
	Path   string
	PFlags uint32
	Attrs  Attributes
}

OpenRequest is SSH_FXP_OPEN.

func DecodeOpen

func DecodeOpen(payload []byte) (OpenRequest, error)

DecodeOpen reads an SSH_FXP_OPEN payload.

func (OpenRequest) EncodePayload

func (m OpenRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (OpenRequest) Type

func (OpenRequest) Type() uint8

Type implements Message.

type PathRequest

type PathRequest struct {
	PacketType uint8
	ID         uint32
	Path       string
}

PathRequest is any request that is an id and a single path: SSH_FXP_LSTAT, SSH_FXP_STAT, SSH_FXP_OPENDIR, SSH_FXP_REMOVE, SSH_FXP_RMDIR, SSH_FXP_REALPATH and SSH_FXP_READLINK.

PacketType carries which one, because the payload cannot say.

func DecodePath

func DecodePath(typ uint8, payload []byte) (PathRequest, error)

DecodePath reads the payload of any PathRequest-shaped message.

func (PathRequest) EncodePayload

func (m PathRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (PathRequest) Type

func (m PathRequest) Type() uint8

Type implements Message.

type ReadRequest

type ReadRequest struct {
	ID     uint32
	Handle string
	Offset uint64
	Length uint32
}

ReadRequest is SSH_FXP_READ: read Length bytes at Offset.

This is the request that maps exactly onto github.com/go-filesystems/interface.File.ReadAt, which is why a driver implementing the optional Opener capability can serve it without reading anything it was not asked for.

func DecodeRead

func DecodeRead(payload []byte) (ReadRequest, error)

DecodeRead reads an SSH_FXP_READ payload.

func (ReadRequest) EncodePayload

func (m ReadRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (ReadRequest) Type

func (ReadRequest) Type() uint8

Type implements Message.

type RenameRequest

type RenameRequest struct {
	ID               uint32
	OldPath, NewPath string
}

RenameRequest is SSH_FXP_RENAME.

func DecodeRename

func DecodeRename(payload []byte) (RenameRequest, error)

DecodeRename reads an SSH_FXP_RENAME payload.

func (RenameRequest) EncodePayload

func (m RenameRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (RenameRequest) Type

func (RenameRequest) Type() uint8

Type implements Message.

type SetstatRequest

type SetstatRequest struct {
	PacketType uint8
	ID         uint32
	Path       string
	Attrs      Attributes
}

SetstatRequest is SSH_FXP_SETSTAT (by path) or SSH_FXP_MKDIR, both of which are an id, a path and an ATTRS.

func DecodeSetstat

func DecodeSetstat(typ uint8, payload []byte) (SetstatRequest, error)

DecodeSetstat reads an SSH_FXP_SETSTAT or SSH_FXP_MKDIR payload.

func (SetstatRequest) EncodePayload

func (m SetstatRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (SetstatRequest) Type

func (m SetstatRequest) Type() uint8

Type implements Message.

type Status

type Status uint32

Status is an SSH_FX_* code: the answer carried by an SSH_FXP_STATUS packet.

Version 3 defines exactly the nine below. Later drafts add finer codes (SSH_FX_NO_SUCH_PATH, SSH_FX_DIR_NOT_EMPTY, ...) and it is tempting to send them anyway, but a version-3 client is entitled to treat any code it does not know as a hard failure, and some do. The finer distinction goes in the human-readable message instead, where it costs nothing and reaches the user.

const (
	// StatusOK means the operation completed. It is also what a
	// write-shaped request returns on success, since there is nothing else
	// to send back.
	StatusOK Status = 0
	// StatusEOF ends a READ or a READDIR. It is not an error: it is the
	// only way either of those two loops terminates.
	StatusEOF Status = 1
	// StatusNoSuchFile is ENOENT.
	StatusNoSuchFile Status = 2
	// StatusPermissionDenied is EACCES/EPERM, and is what a read-only
	// export answers to every mutating request.
	StatusPermissionDenied Status = 3
	// StatusFailure is the catch-all. A client shows its message to the
	// user, so the message is the only place the real cause can go.
	StatusFailure Status = 4
	// StatusBadMessage reports a packet that did not decode. It is the
	// codec's verdict, not the filesystem's.
	StatusBadMessage Status = 5
	// StatusNoConnection and StatusConnectionLost are client-side codes in
	// practice; they are defined for completeness of the enum.
	StatusNoConnection   Status = 6
	StatusConnectionLost Status = 7
	// StatusOpUnsupported reports an operation the server will not do.
	// Answering it honestly is what lets a client fall back instead of
	// hanging or believing a lie.
	StatusOpUnsupported Status = 8
)

The version 3 status codes.

func (Status) String

func (s Status) String() string

String renders a status as its SSH_FX_ name, for logs and test failures.

type StatusReply

type StatusReply struct {
	ID      uint32
	Code    Status
	Message string
	Lang    string
}

StatusReply is SSH_FXP_STATUS.

Message is shown to the user by every client worth using, so it is the one place a cause that version 3's nine status codes cannot express still reaches a person. Lang is an RFC 1766 tag and is universally empty.

func DecodeStatus

func DecodeStatus(payload []byte) (StatusReply, error)

DecodeStatus reads an SSH_FXP_STATUS payload.

The message and language fields were added in version 3 and some version-2 era peers omit them, so a payload that ends after the code decodes as a status with an empty message rather than as a protocol error.

func (StatusReply) EncodePayload

func (m StatusReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (StatusReply) Type

func (StatusReply) Type() uint8

Type implements Message.

type SymlinkRequest

type SymlinkRequest struct {
	ID                   uint32
	TargetPath, LinkPath string
}

SymlinkRequest is SSH_FXP_SYMLINK.

The field order on the wire is target-then-link, which is the REVERSE of what draft-ietf-secsh-filexfer-02 §6.10 says. That is not a mistake here: OpenSSH's server shipped with the arguments swapped, the error was not noticed until it was deployed everywhere, and every client now sends the swapped order. OpenSSH documents the discrepancy in its own PROTOCOL file, §4.1. Following the draft instead of the deployed reality would create symlinks pointing the wrong way for every real client — the field names below are the truth on the wire, and the order they are written in is why.

func DecodeSymlink(payload []byte) (SymlinkRequest, error)

DecodeSymlink reads an SSH_FXP_SYMLINK payload.

func (SymlinkRequest) EncodePayload

func (m SymlinkRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (SymlinkRequest) Type

func (SymlinkRequest) Type() uint8

Type implements Message.

type VersionReply

type VersionReply struct {
	Version    uint32
	Extensions []ExtendedPair
}

VersionReply is SSH_FXP_VERSION, the server's answer to INIT. It too has no request id.

func DecodeVersion

func DecodeVersion(payload []byte) (VersionReply, error)

DecodeVersion reads an SSH_FXP_VERSION payload. It shares INIT's shape, so it shares its decoder.

func (VersionReply) EncodePayload

func (m VersionReply) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (VersionReply) Type

func (VersionReply) Type() uint8

Type implements Message.

type WriteRequest

type WriteRequest struct {
	ID     uint32
	Handle string
	Offset uint64
	Data   []byte
}

WriteRequest is SSH_FXP_WRITE: put Data at Offset.

The offset is the hard part of serving this over the current github.com/go-filesystems/interface contract; see the server package's documentation for the cost and its measurement.

func DecodeWrite

func DecodeWrite(payload []byte) (WriteRequest, error)

DecodeWrite reads an SSH_FXP_WRITE payload.

func (WriteRequest) EncodePayload

func (m WriteRequest) EncodePayload(e *Encoder)

EncodePayload implements Message.

func (WriteRequest) Type

func (WriteRequest) Type() uint8

Type implements Message.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL