mirror of https://github.com/pkg/sftp.git
326 lines
6.9 KiB
Go
326 lines
6.9 KiB
Go
package localfs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pkg/sftp/v2"
|
|
sshfx "github.com/pkg/sftp/v2/encoding/ssh/filexfer"
|
|
"github.com/pkg/sftp/v2/encoding/ssh/filexfer/openssh"
|
|
)
|
|
|
|
// ServerHandler implements the sftp.ServerHandler interface using the local filesystem as the filesystem.
|
|
// NOTE: This is not normally a safe thing to expose.
|
|
type ServerHandler struct {
|
|
sftp.UnimplementedServerHandler
|
|
|
|
ReadOnly bool
|
|
WorkDir string
|
|
|
|
handles atomic.Uint64
|
|
}
|
|
|
|
func (h *ServerHandler) toLocalPath(p string) (string, error) {
|
|
if h.WorkDir != "" && !path.IsAbs(p) {
|
|
p = path.Join(h.WorkDir, p)
|
|
} else {
|
|
// Ensure both paths are cleaning the path.
|
|
// This has important reasons for Windows, but is a good idea in general.
|
|
p = path.Clean(p)
|
|
}
|
|
|
|
if p == "" {
|
|
return "", sshfx.StatusNoSuchFile
|
|
}
|
|
|
|
return toLocalPath(p)
|
|
}
|
|
|
|
// Mkdir implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Mkdir(_ context.Context, req *sshfx.MkdirPacket) error {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var perms sshfx.FileMode = 0755
|
|
if req.Attrs.HasPermissions() {
|
|
perms = req.Attrs.GetPermissions().Perm()
|
|
}
|
|
|
|
return os.Mkdir(lpath, fs.FileMode(perms))
|
|
}
|
|
|
|
// Remove implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Remove(_ context.Context, req *sshfx.RemovePacket) error {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fi, err := os.Stat(lpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
return &fs.PathError{
|
|
Op: "remove",
|
|
Path: lpath,
|
|
Err: fmt.Errorf("is a directory"),
|
|
}
|
|
}
|
|
|
|
return os.Remove(lpath)
|
|
}
|
|
|
|
// Rename implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Rename(_ context.Context, req *sshfx.RenamePacket) error {
|
|
from, err := h.toLocalPath(req.OldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
to, err := h.toLocalPath(req.NewPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := os.Stat(to); !errors.Is(err, fs.ErrNotExist) {
|
|
if err == nil {
|
|
return fs.ErrExist
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return os.Rename(from, to)
|
|
}
|
|
|
|
// POSIXRename implements [sftp.POSIXRenameServerHandler].
|
|
func (h *ServerHandler) POSIXRename(_ context.Context, req *openssh.POSIXRenameExtendedPacket) error {
|
|
from, err := h.toLocalPath(req.OldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
to, err := h.toLocalPath(req.NewPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return posixRename(from, to)
|
|
}
|
|
|
|
// Rmdir implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Rmdir(_ context.Context, req *sshfx.RmdirPacket) error {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fi, err := os.Stat(lpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
return &fs.PathError{
|
|
Op: "rmdir",
|
|
Path: lpath,
|
|
Err: fmt.Errorf("not a directory"),
|
|
}
|
|
}
|
|
|
|
return os.Remove(lpath)
|
|
}
|
|
|
|
// SetStat implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) SetStat(_ context.Context, req *sshfx.SetStatPacket) error {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if req.Attrs.HasSize() {
|
|
sz := req.Attrs.GetSize()
|
|
if err := os.Truncate(lpath, int64(sz)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.Attrs.HasUserGroup() {
|
|
uid, gid := req.Attrs.GetUserGroup()
|
|
if err := os.Chown(lpath, int(uid), int(gid)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.Attrs.HasPermissions() {
|
|
perms := req.Attrs.GetPermissions()
|
|
if err := os.Chmod(lpath, fs.FileMode(perms.Perm())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.Attrs.HasACModTime() {
|
|
atime, mtime := req.Attrs.GetACModTime()
|
|
if err := os.Chtimes(lpath, time.Unix(int64(atime), 0), time.Unix(int64(mtime), 0)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Symlink implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Symlink(_ context.Context, req *sshfx.SymlinkPacket) error {
|
|
target, err := h.toLocalPath(req.TargetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
link, err := h.toLocalPath(req.LinkPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Symlink(target, link)
|
|
}
|
|
|
|
func fileInfoToAttrs(fi fs.FileInfo) *sshfx.Attributes {
|
|
attrs := new(sshfx.Attributes)
|
|
attrs.SetSize(uint64(fi.Size()))
|
|
attrs.SetPermissions(sshfx.FromGoFileMode(fi.Mode()))
|
|
|
|
mtime := uint32(fi.ModTime().Unix())
|
|
attrs.SetACModTime(mtime, mtime)
|
|
|
|
fileStatFromInfoOs(fi, attrs)
|
|
|
|
return attrs
|
|
}
|
|
|
|
// LStat implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) LStat(_ context.Context, req *sshfx.LStatPacket) (*sshfx.Attributes, error) {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fi, err := os.Lstat(lpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fileInfoToAttrs(fi), nil
|
|
}
|
|
|
|
// Stat implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Stat(_ context.Context, req *sshfx.StatPacket) (*sshfx.Attributes, error) {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fi, err := os.Stat(lpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fileInfoToAttrs(fi), nil
|
|
}
|
|
|
|
// ReadLink implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) ReadLink(_ context.Context, req *sshfx.ReadLinkPacket) (string, error) {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return os.Readlink(lpath)
|
|
}
|
|
|
|
// RealPath implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) RealPath(_ context.Context, req *sshfx.RealPathPacket) (string, error) {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
abs, err := filepath.Abs(lpath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return path.Join("/", filepath.ToSlash(abs)), nil
|
|
}
|
|
|
|
// Open implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) Open(_ context.Context, req *sshfx.OpenPacket) (sftp.FileHandler, error) {
|
|
lpath, err := h.toLocalPath(req.Filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var osFlags int
|
|
|
|
switch {
|
|
case req.PFlags&sshfx.FlagRead != 0:
|
|
if req.PFlags&sshfx.FlagWrite != 0 && !h.ReadOnly {
|
|
osFlags |= os.O_RDWR
|
|
} else {
|
|
osFlags |= os.O_RDONLY
|
|
}
|
|
|
|
case req.PFlags&sshfx.FlagWrite != 0:
|
|
if h.ReadOnly {
|
|
return nil, sshfx.StatusPermissionDenied
|
|
}
|
|
osFlags |= os.O_WRONLY
|
|
|
|
default:
|
|
return nil, fs.ErrInvalid
|
|
}
|
|
|
|
// Don't use O_APPEND flag as it conflicts with WriteAt.
|
|
// The sshfx.FlagAppend is a no-op here as the client sends the offsets anyways.
|
|
|
|
if req.PFlags&sshfx.FlagCreate != 0 {
|
|
osFlags |= os.O_CREATE
|
|
}
|
|
if req.PFlags&sshfx.FlagTruncate != 0 {
|
|
osFlags |= os.O_TRUNC
|
|
}
|
|
if req.PFlags&sshfx.FlagExclusive != 0 {
|
|
osFlags |= os.O_EXCL
|
|
}
|
|
|
|
// Like OpenSSH, we only handle permissions here, and only when the file is being created.
|
|
// Otherwise, the permissions are ignored.
|
|
|
|
var perms sshfx.FileMode = 0666
|
|
if req.Attrs.HasPermissions() {
|
|
perms = req.Attrs.GetPermissions().Perm()
|
|
}
|
|
|
|
return h.openfile(lpath, osFlags, fs.FileMode(perms))
|
|
}
|
|
|
|
// OpenDir implements [sftp.ServerHandler].
|
|
func (h *ServerHandler) OpenDir(_ context.Context, req *sshfx.OpenDirPacket) (sftp.DirHandler, error) {
|
|
lpath, err := h.toLocalPath(req.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return h.openfile(lpath, os.O_RDONLY, 0)
|
|
}
|