sftp/localfs/server_windows.go

217 lines
4.3 KiB
Go
Raw Permalink Normal View History

2024-10-01 01:38:18 +08:00
package localfs
import (
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
"golang.org/x/sys/windows"
"github.com/pkg/sftp/v2"
sshfx "github.com/pkg/sftp/v2/encoding/ssh/filexfer"
)
func toLocalPath(p string) (string, error) {
if !path.IsAbs(p) {
// Relative path: just convert from slashes.
return filepath.FromSlash(p), nil
}
if p == "/" {
// This returns a sorta technically valid filename.
// This is a DOS device path specifier.
// Paths that start with this _must_ then be followed by a volume specifier.
// So, arguably, listing this path should enumerate drives.
//
// Since we cleaned the path in ServerHandler.toLocalPath,
// we shouldnt be able to return anything like this from any other code path.
return `\\.\`, nil
}
// Convert the path and strip the leading '/'.
lp := filepath.FromSlash(p[1:])
if strings.HasPrefix(lp, `wsl.localhost\`) || strings.HasPrefix(lp, `wsl$\`) {
// Exceptionally permit local Windows Subservice for Linux .
return `\\` + lp, nil
}
if strings.HasPrefix(p, `\??\`) {
// This is a native NT path
return "", fs.ErrInvalid
}
if !filepath.IsAbs(lp) {
// In ServerHandler.toLocalPath, we already removed all repeat slashes with path.Clean.
// So, the only `filepath.IsAbs(lp) == true` paths are simple drive letter paths.
//
// This means, we avoid UNC paths, both device path specifiers, and \\wsl$\ paths.
// Hopefully, this leads to a lower chance of accidentally exposing every UNC path the host can access.
return "", fs.ErrInvalid
}
if len(lp) == 2 {
// This could only be a simple drive letter.
// We need to add a backslash at the end to be valid.
return lp + `\`, nil
}
return lp, nil
}
func bitsToDrives(bitmap uint32) []string {
var drive rune = 'a'
var drives []string
for bitmap != 0 && drive <= 'z' {
if bitmap&1 == 1 {
drives = append(drives, string(drive)+":")
}
drive++
bitmap >>= 1
}
return drives
}
func getDrives() ([]string, error) {
mask, err := windows.GetLogicalDrives()
if err != nil {
return nil, &os.SyscallError{Syscall: "GetLogicalDrives", Err: err}
}
return bitsToDrives(mask), nil
}
type winRoot struct {
filename string
handle string
mtime uint32
entries []*sshfx.NameEntry
}
func (h *ServerHandler) newWinRoot() (f *winRoot, myErr error) {
drives, err := getDrives()
if err != nil {
return nil, err
}
var entries []*sshfx.NameEntry
for _, drive := range drives {
fi, err := os.Stat(drive + `\`)
if err != nil {
return nil, err
}
attrs := fileInfoToAttrs(fi)
entry := &sshfx.NameEntry{
Filename: drive,
Attrs: *attrs,
}
entry.Longname = sftp.FormatLongname(entry, h)
entries = append(entries, entry)
}
return &winRoot{
filename: "/",
handle: fmt.Sprint(h.handles.Add(1)),
mtime: uint32(time.Now().Unix()),
entries: entries,
}, nil
}
func (f *winRoot) Close() error {
for i := range f.entries {
f.entries[i] = nil
}
f.entries = nil
return nil
}
func (f *winRoot) Name() string {
return "/"
}
func (f *winRoot) Handle() string {
return f.handle
}
func (f *winRoot) Stat() (*sshfx.Attributes, error) {
attrs := new(sshfx.Attributes)
attrs.SetSize(0)
attrs.SetACModTime(f.mtime, f.mtime)
attrs.SetPermissions(0555)
return attrs, nil
2024-10-01 01:38:18 +08:00
}
func (f *winRoot) ReadDir(maxDataLen uint32) ([]*sshfx.NameEntry, error) {
var ret []*sshfx.NameEntry
size := 4
for len(f.entries) > 0 {
entry := f.entries[0]
if size+entry.MarshalSize() > int(maxDataLen) {
2024-10-01 01:38:18 +08:00
return ret, nil
}
f.entries[0] = nil // clear the pointer before shifting it out.
f.entries = f.entries[1:]
ret = append(ret, entry)
}
return ret, io.EOF
}
func (f *winRoot) SetStat(_ *sshfx.Attributes) error {
return fs.ErrPermission
}
func (f *winRoot) ReadAt(_ []byte, _ int64) (int, error) {
return 0, fs.ErrPermission
}
func (f *winRoot) WriteAt(_ []byte, _ int64) (int, error) {
return 0, fs.ErrPermission
}
func (f *winRoot) Sync() error {
return fs.ErrPermission
}
type fileDir interface {
sftp.FileHandler
sftp.DirHandler
}
func (h *ServerHandler) openfile(path string, flag int, mode fs.FileMode) (fileDir, error) {
if path == `\\.\` {
return h.newWinRoot()
}
f, err := os.OpenFile(path, flag, mode)
if err != nil {
return nil, err
}
return &File{
filename: path,
handle: fmt.Sprint(h.handles.Add(1)),
File: f,
}, nil
}