| package sftp |
| |
| import ( |
| "cmp" |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "io/fs" |
| "math" |
| "slices" |
| "time" |
| |
| sshfx "github.com/pkg/sftp/v2/encoding/ssh/filexfer" |
| "github.com/pkg/sftp/v2/encoding/ssh/filexfer/openssh" |
| "github.com/pkg/sftp/v2/internal/pragma" |
| "github.com/pkg/sftp/v2/internal/sync" |
| ) |
| |
| var errInvalidHandle = errors.New("invalid handle") |
| |
| // ServerHandler defines an interface that an SFTP service must implement in order to be handled by [Server] code. |
| type ServerHandler interface { |
| Mkdir(ctx context.Context, req *sshfx.MkdirPacket) error |
| Remove(ctx context.Context, req *sshfx.RemovePacket) error |
| Rename(ctx context.Context, req *sshfx.RenamePacket) error |
| Rmdir(ctx context.Context, req *sshfx.RmdirPacket) error |
| SetStat(ctx context.Context, req *sshfx.SetStatPacket) error |
| Symlink(ctx context.Context, req *sshfx.SymlinkPacket) error |
| |
| LStat(ctx context.Context, req *sshfx.LStatPacket) (*sshfx.Attributes, error) |
| Stat(ctx context.Context, req *sshfx.StatPacket) (*sshfx.Attributes, error) |
| |
| ReadLink(ctx context.Context, req *sshfx.ReadLinkPacket) (string, error) |
| RealPath(ctx context.Context, req *sshfx.RealPathPacket) (string, error) |
| |
| Open(ctx context.Context, req *sshfx.OpenPacket) (FileHandler, error) |
| OpenDir(ctx context.Context, req *sshfx.OpenDirPacket) (DirHandler, error) |
| |
| mustEmbedUnimplementedServerHandler() |
| } |
| |
| // HardlinkServerHandler is an extension interface for supporting the "[email protected]" extension. |
| type HardlinkServerHandler interface { |
| ServerHandler |
| Hardlink(ctx context.Context, req *openssh.HardlinkExtendedPacket) error |
| } |
| |
| // POSIXRenameServerHandler is an extension interface for supporting the "[email protected]" extension. |
| type POSIXRenameServerHandler interface { |
| ServerHandler |
| POSIXRename(ctx context.Context, req *openssh.POSIXRenameExtendedPacket) error |
| } |
| |
| // StatVFSServerHandler is an extension interface for supporting the "[email protected]" extension. |
| type StatVFSServerHandler interface { |
| ServerHandler |
| StatVFS(ctx context.Context, req *openssh.StatVFSExtendedPacket) (*openssh.StatVFSExtendedReplyPacket, error) |
| } |
| |
| // FileHandler defines an interface that the Server code can use to support file-handle request packets. |
| type FileHandler interface { |
| io.Closer |
| io.ReaderAt |
| io.WriterAt |
| |
| Name() string |
| Handle() string |
| Stat() (*sshfx.Attributes, error) |
| Sync() error |
| } |
| |
| // SetStatFileHandler is an extension interface for handling the SSH_FXP_FSETSTAT request packet. |
| type SetStatFileHandler interface { |
| FileHandler |
| SetStat(attrs *sshfx.Attributes) error |
| } |
| |
| // TruncateFileHandler is an extension interface for handling the truncate subfunction of an SSH_FXP_FSETSTAT request. |
| type TruncateFileHandler interface { |
| FileHandler |
| Truncate(size int64) error |
| } |
| |
| func ftrunc(attr *sshfx.Attributes, f FileHandler) error { |
| if !attr.HasSize() { |
| return nil |
| } |
| |
| sz := attr.GetSize() |
| |
| if truncater, ok := f.(TruncateFileHandler); ok { |
| return truncater.Truncate(int64(sz)) |
| } |
| |
| return &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: "unsupported fsetstat: ftruncate", |
| } |
| } |
| |
| // ChownFileHandler is an extension interface for handling the chown subfunction of an SSH_FXP_FSETSTAT request. |
| type ChownFileHandler interface { |
| FileHandler |
| Chown(uid, gid int) error |
| } |
| |
| func fchown(attr *sshfx.Attributes, f FileHandler) error { |
| if !attr.HasUserGroup() { |
| return nil |
| } |
| |
| uid, gid := attr.GetUserGroup() |
| |
| if chowner, ok := f.(ChownFileHandler); ok { |
| return chowner.Chown(int(uid), int(gid)) |
| } |
| |
| return &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: "unsupported fsetstat: fchown", |
| } |
| } |
| |
| // ChmodFileHandler is an extension interface for handling the chmod subfunction of an SSH_FXP_FSETSTAT request. |
| type ChmodFileHandler interface { |
| FileHandler |
| Chmod(mode fs.FileMode) error |
| } |
| |
| func fchmod(attr *sshfx.Attributes, f FileHandler) error { |
| if !attr.HasPermissions() { |
| return nil |
| } |
| |
| mode := attr.GetPermissions() |
| |
| if chmoder, ok := f.(ChmodFileHandler); ok { |
| return chmoder.Chmod(sshfx.ToGoFileMode(mode)) |
| } |
| |
| return &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: "unsupported fsetstat: fchmod", |
| } |
| } |
| |
| // ChtimesFileHandler is an extension interface for handling the chmod subfunction of an SSH_FXP_FSETSTAT request. |
| type ChtimesFileHandler interface { |
| FileHandler |
| Chtimes(atime, mtime time.Time) error |
| } |
| |
| func fchtimes(attr *sshfx.Attributes, f FileHandler) error { |
| if !attr.HasACModTime() { |
| return nil |
| } |
| |
| atime, mtime := attr.GetACModTime() |
| |
| if chtimeser, ok := f.(ChtimesFileHandler); ok { |
| return chtimeser.Chtimes(time.Unix(int64(atime), 0), time.Unix(int64(mtime), 0)) |
| } |
| |
| return &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: "unsupported fsetstat: fchtimes", |
| } |
| } |
| |
| // StatVFSFileHandler is an extension interface for supporting the "[email protected]" extension. |
| type StatVFSFileHandler interface { |
| FileHandler |
| |
| StatVFS() (*openssh.StatVFSExtendedReplyPacket, error) |
| } |
| |
| // DirHandler defines an interface that the Server code can use to support directory-handle request packets. |
| type DirHandler interface { |
| io.Closer |
| |
| Name() string |
| Handle() string |
| ReadDir(maxDataLen uint32) ([]*sshfx.NameEntry, error) |
| } |
| |
| type wrapHandler func(ctx context.Context, req sshfx.Packet) (sshfx.Packet, error) |
| |
| // commonHandle is the intersection of FileHandler and DirHandler |
| type commonHandle interface { |
| io.Closer |
| |
| Name() string |
| Handle() string |
| } |
| |
| // A Server defines parameters for running an SFTP server. |
| // The zero value for Server is a valid configuration. |
| type Server struct { |
| _ pragma.DoNotCopy // be explicit about no copy. |
| |
| Handler ServerHandler |
| |
| MaxPacketLength int |
| MaxDataLength int |
| Extensions map[string]string |
| |
| Debug io.Writer |
| |
| wg sync.WaitGroup |
| handles sync.Map[string, commonHandle] |
| hijacks map[sshfx.PacketType]wrapHandler |
| |
| dataPktPool *sync.Pool[sshfx.DataPacket] |
| |
| mu sync.Mutex |
| shutdown chan struct{} |
| err error |
| } |
| |
| // GracefulStop stops the SFTP server gracefully. |
| func (srv *Server) GracefulStop() error { |
| srv.mu.Lock() |
| select { |
| case <-srv.shutdown: |
| srv.mu.Unlock() |
| return fmt.Errorf("sftp: already shutting down") |
| default: |
| close(srv.shutdown) |
| } |
| srv.mu.Unlock() |
| |
| srv.wg.Wait() |
| |
| for handle, f := range srv.handles.Range { |
| fmt.Fprintf(srv.Debug, "sftp server file with handle %q left open: %T", handle, f.Name()) |
| } |
| |
| return srv.err |
| } |
| |
| func (srv *Server) handshake(conn io.ReadWriter, maxPktLen uint32) error { |
| b := make([]byte, maxPktLen) |
| |
| var initPkt sshfx.InitPacket |
| if err := initPkt.ReadFrom(conn, b, maxPktLen); err != nil { |
| return fmt.Errorf("handshake: %w", err) |
| } |
| |
| verPkt := sshfx.VersionPacket{ |
| Version: sftpProtocolVersion, |
| } |
| |
| sshfx.RegisterExtendedPacketType[openssh.FSyncExtendedPacket]() |
| verPkt.Extensions = append(verPkt.Extensions, openssh.ExtensionFSync()) |
| |
| if _, ok := srv.Handler.(HardlinkServerHandler); ok { |
| sshfx.RegisterExtendedPacketType[openssh.HardlinkExtendedPacket]() |
| verPkt.Extensions = append(verPkt.Extensions, openssh.ExtensionHardlink()) |
| } |
| |
| if _, ok := srv.Handler.(POSIXRenameServerHandler); ok { |
| sshfx.RegisterExtendedPacketType[openssh.POSIXRenameExtendedPacket]() |
| verPkt.Extensions = append(verPkt.Extensions, openssh.ExtensionPOSIXRename()) |
| } |
| |
| if _, ok := srv.Handler.(StatVFSServerHandler); ok { |
| sshfx.RegisterExtendedPacketType[openssh.StatVFSExtendedPacket]() |
| verPkt.Extensions = append(verPkt.Extensions, openssh.ExtensionStatVFS()) |
| |
| sshfx.RegisterExtendedPacketType[openssh.FStatVFSExtendedPacket]() |
| verPkt.Extensions = append(verPkt.Extensions, openssh.ExtensionFStatVFS()) |
| } |
| |
| for name, data := range srv.Extensions { |
| verPkt.Extensions = append(verPkt.Extensions, &sshfx.ExtensionPair{ |
| Name: name, |
| Data: data, |
| }) |
| } |
| |
| data, err := verPkt.MarshalBinary() |
| if err != nil { |
| return fmt.Errorf("marshal version: %w", err) |
| } |
| |
| _, err = conn.Write(data) |
| if err != nil { |
| return fmt.Errorf("write version: %w", err) |
| } |
| |
| return nil |
| } |
| |
| // FileFromHandle returns the FileHandler associated with the given handle. |
| // It returns an error if there is no handler associated with the handle, |
| // or if the handler is not a FileHandler. |
| func (srv *Server) FileFromHandle(handle string) (FileHandler, error) { |
| f, _ := srv.handles.Load(handle) |
| file, _ := f.(FileHandler) |
| if file == nil { |
| return nil, errInvalidHandle |
| } |
| return file, nil |
| } |
| |
| // DirFromHandle returns the DirHandler associated with the given handle. |
| // It returns an error if there is no handler associated with the handle, |
| // or if the handler is not a FileHandler. |
| func (srv *Server) DirFromHandle(handle string) (DirHandler, error) { |
| f, _ := srv.handles.Load(handle) |
| file, _ := f.(DirHandler) |
| if file == nil { |
| return nil, errInvalidHandle |
| } |
| return file, nil |
| } |
| |
| /* func WithResponseExtension[REQ interface{ sshfx.Packet; sshfx.ExtendedData }, RESP sshfx.Packet](srv *Server, ext sshfx.ExtensionPair, fn func(context.Context, REQ) error) error { |
| wrap := wrapHandler(func(ctx context.Context, req sshfx.Packet) (sshfx.Packet, error) { |
| |
| }) |
| |
| return nil |
| } */ |
| |
| // Hijack registers a hijacking function that will be called to handle the given SFTP request packet, |
| // rather than the standard Server code calling into the ServerHandler. |
| // The error returned by the function will be turned into a SSH_FXP_STATUS package, |
| // and a nil error return will reply back with an SSH_FX_OK. |
| // This is really only useful for supporting newer versions of the SFTP standard. |
| func Hijack[REQ sshfx.Packet](srv *Server, fn func(context.Context, REQ) error) error { |
| wrap := wrapHandler(func(ctx context.Context, req sshfx.Packet) (sshfx.Packet, error) { |
| return nil, fn(ctx, req.(REQ)) |
| }) |
| |
| var pkt REQ |
| |
| return srv.register(pkt.Type(), wrap) |
| } |
| |
| // HijackWithResponse registers a hijacking function that will be called to handle the given SFTP request packet, |
| // rather than the standard Server code calling into the ServerHandler. |
| // If a non-nil error is returned by the function, it will be turned into a SSH_FXP_STATUS package, |
| // and any returned response packet will be ignored. |
| // Otherwise, the returned response packet will be sent to the client. |
| // This is really only useful for supporting newer versions of the SFTP standard. |
| func HijackWithResponse[REQ, RESP sshfx.Packet](srv *Server, fn func(context.Context, REQ) (RESP, error)) error { |
| wrap := wrapHandler(func(ctx context.Context, req sshfx.Packet) (sshfx.Packet, error) { |
| resp, err := fn(ctx, req.(REQ)) |
| if err != nil { |
| // We have to convert maybe typed-zero to untyped-nil. |
| return nil, err |
| } |
| return resp, nil |
| }) |
| |
| var pkt REQ |
| |
| return srv.register(pkt.Type(), wrap) |
| } |
| |
| func (srv *Server) register(typ sshfx.PacketType, wrap wrapHandler) error { |
| srv.mu.Lock() |
| defer srv.mu.Unlock() |
| |
| if srv.shutdown != nil { |
| return fmt.Errorf("sftp server: cannot register after starting server") |
| } |
| |
| if _, exists := srv.hijacks[typ]; exists { |
| return fmt.Errorf("sftp server: handler already registered for type: %s", typ) |
| } |
| |
| srv.hijacks[typ] = wrap |
| return nil |
| } |
| |
| // Serve accepts incoming connections on the socket conn. |
| // The server reads SFTP requests and then calls the registered handlers to reply to them. |
| // Serve returns when a read returns any error other than sshfx.ErrBadMessage, |
| // or a write returns any error. |
| // conn will be closed when this method returns. |
| // Serve will return a non-nil error unless GracefulStop is called, |
| // or an EOF is encountered at the end of a complete packet. |
| func (srv *Server) Serve(conn io.ReadWriteCloser) error { |
| srv.mu.Lock() |
| if srv.shutdown != nil { |
| return fmt.Errorf("sftp: already serving") |
| } |
| srv.shutdown = make(chan struct{}) |
| srv.mu.Unlock() |
| |
| if srv.MaxDataLength < 0 { |
| return fmt.Errorf("sftp: max data length cannot be negative: %d", srv.MaxDataLength) |
| } |
| |
| if uint(srv.MaxDataLength) > math.MaxUint32 { |
| return fmt.Errorf("sftp: max data length too large: %d", srv.MaxDataLength) |
| } |
| |
| maxDataLen := uint32(srv.MaxDataLength) |
| if maxDataLen == 0 { |
| maxDataLen = sshfx.DefaultMaxDataLength |
| } |
| |
| if srv.MaxPacketLength < 0 { |
| return fmt.Errorf("sftp: max packet length cannot be negative: %d", srv.MaxDataLength) |
| } |
| |
| if uint(srv.MaxPacketLength) > math.MaxUint32 { |
| return fmt.Errorf("sftp: max packet length too large: %d", srv.MaxPacketLength) |
| } |
| |
| maxPktLen := uint32(srv.MaxPacketLength) |
| if maxPktLen == 0 { |
| maxPktLen = maxDataLen + sshfx.MaxPacketLengthOverhead |
| } |
| |
| if maxPktLen < maxDataLen { |
| return fmt.Errorf("sftp: max packet length shorter than max data length: %d < %d", maxPktLen, maxDataLen) |
| } |
| |
| if err := srv.handshake(conn, maxPktLen); err != nil { |
| return fmt.Errorf("sftp: handshake failure: %w", err) |
| } |
| |
| srv.wg.Add(1) |
| defer func() { |
| conn.Close() |
| srv.wg.Done() |
| }() |
| |
| var reqPkt sshfx.RequestPacket |
| scratch := make([]byte, maxPktLen) |
| dataHint := make([]byte, maxDataLen) |
| outHint := make([]byte, maxPktLen) |
| |
| srv.dataPktPool = sync.NewPool[sshfx.DataPacket](64) |
| |
| for { |
| select { |
| case <-srv.shutdown: |
| return nil |
| default: |
| } |
| |
| reqPkt.Reset() |
| |
| var err error |
| var resp sshfx.Packet |
| |
| if err = reqPkt.ReadFrom(conn, scratch, maxPktLen); err != nil { |
| select { |
| case <-srv.shutdown: |
| return nil |
| default: |
| } |
| |
| switch { |
| case errors.Is(err, sshfx.StatusBadMessage): |
| // Respond back with the StatusBadMessage, and continue on. |
| |
| case errors.Is(err, io.EOF): |
| return nil |
| |
| default: |
| return fmt.Errorf("sftp: read request packet: %w", err) |
| } |
| |
| } else { |
| resp, err = srv.handle(reqPkt.Request, dataHint, maxDataLen) |
| } |
| |
| if resp == nil { |
| resp = errorToStatus(err) |
| } |
| |
| hdr, payload, err := resp.MarshalPacket(reqPkt.RequestID, outHint) |
| if err != nil { |
| return fmt.Errorf("sftp: marshal response packet: %w", err) |
| } |
| |
| if _, err := conn.Write(hdr); err != nil { |
| return fmt.Errorf("sftp: write packet: %w", err) |
| } |
| |
| if len(payload) > 0 { |
| if _, err := conn.Write(payload); err != nil { |
| return fmt.Errorf("sftp: write packet payload: %w", err) |
| } |
| } |
| |
| switch resp := resp.(type) { |
| case *sshfx.DataPacket: |
| srv.dataPktPool.Put(resp) |
| } |
| } |
| } |
| |
| func do[REQ any](srv *Server, req REQ, fn func(context.Context, REQ) error) (sshfx.Packet, error) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| srv.wg.Add(1) |
| defer srv.wg.Done() |
| |
| return nil, fn(ctx, req) |
| } |
| |
| func get[REQ, RES any](srv *Server, req REQ, fn func(context.Context, REQ) (RES, error)) (RES, error) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| srv.wg.Add(1) |
| defer srv.wg.Done() |
| |
| return fn(ctx, req) |
| } |
| |
| func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (sshfx.Packet, error) { |
| defer sshfx.PoolReturn(req) |
| |
| if len(srv.hijacks) > 0 { |
| if fn := srv.hijacks[req.Type()]; fn != nil { |
| // Hijack takes care of wrapping the getter into an untyped-nil on error. |
| return get(srv, req, fn) |
| } |
| } |
| |
| // Falling out of this switch returns err as an sshfx.StatusPacket. |
| // Any other response packet type should return that packet directly, and not fall out of this switch. |
| switch req := req.(type) { |
| case *sshfx.MkdirPacket: |
| return do(srv, req, srv.Handler.Mkdir) |
| |
| case *sshfx.RemovePacket: |
| return do(srv, req, srv.Handler.Remove) |
| |
| case *sshfx.RenamePacket: |
| return do(srv, req, srv.Handler.Rename) |
| |
| case *sshfx.RmdirPacket: |
| return do(srv, req, srv.Handler.Rmdir) |
| |
| case *sshfx.SetStatPacket: |
| return do(srv, req, srv.Handler.SetStat) |
| |
| case *sshfx.SymlinkPacket: |
| return do(srv, req, srv.Handler.Symlink) |
| |
| case *sshfx.LStatPacket: |
| attrs, err := get(srv, req, srv.Handler.LStat) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &sshfx.AttrsPacket{ |
| Attrs: *attrs, |
| }, nil |
| |
| case *sshfx.StatPacket: |
| attrs, err := get(srv, req, srv.Handler.Stat) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &sshfx.AttrsPacket{ |
| Attrs: *attrs, |
| }, nil |
| |
| case *sshfx.ReadLinkPacket: |
| name, err := get(srv, req, srv.Handler.ReadLink) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &sshfx.PathPseudoPacket{ |
| Path: name, |
| }, nil |
| |
| case *sshfx.RealPathPacket: |
| name, err := get(srv, req, srv.Handler.RealPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &sshfx.PathPseudoPacket{ |
| Path: name, |
| }, nil |
| |
| case *sshfx.ExtendedPacket: |
| // specially: falling out of this switch statement returns an SSH_FX_OP_UNSUPPORTED response. |
| switch req := req.Data.(type) { |
| case *openssh.POSIXRenameExtendedPacket: |
| if renamer, ok := srv.Handler.(POSIXRenameServerHandler); ok { |
| return do(srv, req, renamer.POSIXRename) |
| } |
| |
| case *openssh.HardlinkExtendedPacket: |
| if hardlinker, ok := srv.Handler.(HardlinkServerHandler); ok { |
| return do(srv, req, hardlinker.Hardlink) |
| } |
| |
| case *openssh.StatVFSExtendedPacket: |
| if statvfser, ok := srv.Handler.(StatVFSServerHandler); ok { |
| resp, err := get(srv, req, statvfser.StatVFS) |
| if err != nil { |
| // We have to convert typed-nil to untyped-nil. |
| return nil, err |
| } |
| |
| return resp, nil |
| } |
| |
| case interface{ GetHandle() string }: |
| file, err := srv.FileFromHandle(req.GetHandle()) |
| if err != nil { |
| return nil, err |
| } |
| |
| switch req.(type) { |
| case *openssh.FSyncExtendedPacket: |
| return nil, file.Sync() |
| |
| case *openssh.FStatVFSExtendedPacket: |
| if statvfser, ok := file.(StatVFSFileHandler); ok { |
| resp, err := statvfser.StatVFS() |
| if err != nil { |
| // We have to convert typed-nil to untyped-nil. |
| return nil, err |
| } |
| |
| return resp, nil |
| } |
| |
| if statvfser, ok := srv.Handler.(StatVFSServerHandler); ok { |
| req := &openssh.StatVFSExtendedPacket{ |
| Path: file.Name(), |
| } |
| |
| resp, err := get(srv, req, statvfser.StatVFS) |
| if err != nil { |
| // We have to convert typed-nil to untyped-nil. |
| return nil, err |
| } |
| |
| return resp, nil |
| } |
| } |
| } |
| |
| if _, ok := req.Data.(*sshfx.Buffer); ok { |
| // Return a different message when it is entirely unregisted into the system. |
| // This allows one to more easily identify the situation. |
| return nil, &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: fmt.Sprintf("unregistered extended packet: %s", req.ExtendedRequest), |
| } |
| } |
| |
| return nil, &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: fmt.Sprintf("unsupported extended packet: %s", req.ExtendedRequest), |
| } |
| |
| case *sshfx.OpenPacket: |
| file, err := get(srv, req, srv.Handler.Open) |
| if err != nil { |
| return nil, err |
| } |
| |
| handle := file.Handle() |
| |
| srv.handles.Store(handle, file) |
| |
| return &sshfx.HandlePacket{ |
| Handle: handle, |
| }, nil |
| |
| case *sshfx.OpenDirPacket: |
| dir, err := get(srv, req, srv.Handler.OpenDir) |
| if err != nil { |
| return nil, err |
| } |
| |
| handle := dir.Handle() |
| |
| srv.handles.Store(handle, dir) |
| |
| return &sshfx.HandlePacket{ |
| Handle: handle, |
| }, nil |
| |
| case *sshfx.ClosePacket: |
| file, _ := srv.handles.LoadAndDelete(req.Handle) |
| if file == nil { |
| return nil, errInvalidHandle |
| } |
| |
| return nil, file.Close() |
| |
| case *sshfx.ReadDirPacket: |
| dir, err := srv.DirFromHandle(req.Handle) |
| if err != nil { |
| return nil, err |
| } |
| |
| entries, err := dir.ReadDir(maxDataLen) |
| if err != nil { |
| if !errors.Is(err, io.EOF) || len(entries) == 0 { |
| return nil, err |
| } |
| } |
| |
| return &sshfx.NamePacket{Entries: entries}, nil |
| |
| case interface{ GetHandle() string }: |
| file, err := srv.FileFromHandle(req.GetHandle()) |
| if err != nil { |
| return nil, err |
| } |
| |
| switch req := req.(type) { |
| case *sshfx.ReadPacket: |
| if req.Length > maxDataLen { |
| return nil, fmt.Errorf("read length request too large: %d", req.Length) |
| } |
| |
| hint = slices.Grow(hint[:0], int(req.Length))[:req.Length] |
| |
| n, err := file.ReadAt(hint, int64(req.Offset)) |
| if err != nil { |
| // We cannot return results AND a status like SSH_FX_EOF, |
| // so we return io.EOF only if we didn't read anything at all. |
| if !errors.Is(err, io.EOF) || n == 0 { |
| return nil, err |
| } |
| } |
| |
| pkt := srv.dataPktPool.Get() |
| pkt.Data = hint[:n] |
| |
| return pkt, nil |
| |
| case *sshfx.WritePacket: |
| n, err := file.WriteAt(req.Data, int64(req.Offset)) |
| if err != nil { |
| return nil, err |
| } |
| |
| if n != len(req.Data) { |
| // We have no way to return the length of bytes written, |
| // so we have to instead return a short write error, |
| // otherwise the client might not ever know we didn't write the whole request. |
| return nil, io.ErrShortWrite |
| } |
| |
| // explicitly return statusOK here, rather than both nil. |
| return statusOK, nil |
| |
| case *sshfx.FStatPacket: |
| attrs, err := file.Stat() |
| if err != nil { |
| return nil, err |
| } |
| |
| return &sshfx.AttrsPacket{Attrs: *attrs}, nil |
| |
| case *sshfx.FSetStatPacket: |
| if file, ok := file.(SetStatFileHandler); ok { |
| return nil, file.SetStat(&req.Attrs) |
| } |
| |
| var err error |
| |
| if len(req.Attrs.Extended) > 0 { |
| err = &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: "unsupported fsetstat: extended attributes", |
| } |
| } |
| |
| // This is the order and behavior of operations for SSH_FXP_FSETSTAT in openssh's sftp-server. |
| err = cmp.Or(ftrunc(&req.Attrs, file), err) |
| err = cmp.Or(fchmod(&req.Attrs, file), err) |
| err = cmp.Or(fchtimes(&req.Attrs, file), err) |
| err = cmp.Or(fchown(&req.Attrs, file), err) |
| |
| return nil, err |
| } |
| } |
| |
| return nil, &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusOpUnsupported, |
| ErrorMessage: fmt.Sprintf("unsupported packet: %s", req.Type()), |
| } |
| } |
| |
| // These are so common, and they're all the same, just use a global singleton. |
| var ( |
| statusOK = &sshfx.StatusPacket{StatusCode: sshfx.StatusOK} |
| statusEOF = &sshfx.StatusPacket{StatusCode: sshfx.StatusEOF, ErrorMessage: io.EOF.Error()} |
| ) |
| |
| func errorToStatus(err error) sshfx.Packet { |
| if err == nil { |
| return statusOK |
| } |
| |
| if errors.Is(err, io.EOF) { |
| return statusEOF |
| } |
| |
| var status *sshfx.StatusPacket |
| if errors.As(err, &status) { |
| return status |
| } |
| |
| status = &sshfx.StatusPacket{ |
| StatusCode: sshfx.StatusFailure, |
| ErrorMessage: err.Error(), |
| } |
| |
| switch { |
| case errors.As(err, &status.StatusCode): |
| // Nothing to do, errors.As already assigned the value to statusPacket. |
| case errors.Is(err, fs.ErrNotExist): |
| status.StatusCode = sshfx.StatusNoSuchFile |
| case syscallErrorAsStatus(err, status): |
| } |
| |
| return status |
| } |