sftpgo-mirror/sftpd/middleware.go
2021-07-29 00:32:55 +02:00

229 lines
6.1 KiB
Go

package sftpd
import (
"io"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/pkg/sftp"
"github.com/drakkan/sftpgo/v2/vfs"
)
// Middleware defines the interface for sftp middlewares
type Middleware interface {
sftp.FileReader
sftp.FileWriter
sftp.OpenFileWriter
sftp.FileCmder
sftp.StatVFSFileCmder
sftp.FileLister
sftp.LstatFileLister
}
type prefixMatch uint8
const (
pathContainsPrefix prefixMatch = iota
pathIsPrefixParent
pathDiverged
methodList = "List"
methodStat = "Stat"
)
type prefixMiddleware struct {
prefix string
next Middleware
}
func newPrefixMiddleware(prefix string, next Middleware) Middleware {
return &prefixMiddleware{
prefix: prefix,
next: next,
}
}
func (p *prefixMiddleware) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.Lstat(request)
case pathIsPrefixParent:
return listerAt([]os.FileInfo{
vfs.NewFileInfo(request.Filepath, true, 0, time.Now(), false),
}), nil
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) OpenFile(request *sftp.Request) (sftp.WriterAtReaderAt, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.OpenFile(request)
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.Filelist(request)
case pathIsPrefixParent:
Now := time.Now()
switch request.Method {
case methodList:
FileName := p.nextListFolder(request.Filepath)
return listerAt([]os.FileInfo{
// vfs.NewFileInfo(`.`, true, 0, Now, false),
vfs.NewFileInfo(FileName, true, 0, Now, false),
}), nil
case methodStat:
return listerAt([]os.FileInfo{
vfs.NewFileInfo(request.Filepath, true, 0, Now, false),
}), nil
default:
return nil, sftp.ErrSSHFxOpUnsupported
}
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) Filewrite(request *sftp.Request) (io.WriterAt, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
// forward to next handler
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.Filewrite(request)
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) Fileread(request *sftp.Request) (io.ReaderAt, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.Fileread(request)
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) Filecmd(request *sftp.Request) error {
switch request.Method {
case "Rename", "Symlink":
if getPrefixHierarchy(p.prefix, request.Filepath) == pathContainsPrefix &&
getPrefixHierarchy(p.prefix, request.Target) == pathContainsPrefix {
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
request.Target, _ = p.removeFolderPrefix(request.Target)
return p.next.Filecmd(request)
}
return sftp.ErrSSHFxPermissionDenied
// commands have a source and destination (file path and target path)
case "Setstat", "Rmdir", "Mkdir", "Remove":
// commands just the file path
if getPrefixHierarchy(p.prefix, request.Filepath) == pathContainsPrefix {
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.Filecmd(request)
}
return sftp.ErrSSHFxPermissionDenied
default:
return sftp.ErrSSHFxOpUnsupported
}
}
func (p *prefixMiddleware) StatVFS(request *sftp.Request) (*sftp.StatVFS, error) {
switch getPrefixHierarchy(p.prefix, request.Filepath) {
case pathContainsPrefix:
// forward to next handler
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
return p.next.StatVFS(request)
default:
return nil, sftp.ErrSSHFxPermissionDenied
}
}
func (p *prefixMiddleware) nextListFolder(requestPath string) string {
cleanPath := filepath.Clean(`/` + requestPath)
cleanPrefix := filepath.Clean(`/` + p.prefix)
FileName := cleanPrefix[len(cleanPath):]
FileName = strings.TrimLeft(FileName, `/`)
SlashIndex := strings.Index(FileName, `/`)
if SlashIndex > 0 {
return FileName[0:SlashIndex]
}
return FileName
}
func (p *prefixMiddleware) containsPrefix(virtualPath string) bool {
if !path.IsAbs(virtualPath) {
virtualPath = path.Clean(`/` + virtualPath)
}
if p.prefix == `/` || p.prefix == `` {
return true
} else if p.prefix == virtualPath {
return true
}
return strings.HasPrefix(virtualPath, p.prefix+`/`)
}
func (p *prefixMiddleware) removeFolderPrefix(virtualPath string) (string, bool) {
if p.prefix == `/` || p.prefix == `` {
return virtualPath, true
}
virtualPath = filepath.Clean(`/` + virtualPath)
if p.containsPrefix(virtualPath) {
effectivePath := virtualPath[len(p.prefix):]
if effectivePath == `` {
effectivePath = `/`
}
return effectivePath, true
}
return virtualPath, false
}
func getPrefixHierarchy(prefix, path string) prefixMatch {
prefixSplit := strings.Split(filepath.Clean(`/`+prefix), `/`)
pathSplit := strings.Split(filepath.Clean(`/`+path), `/`)
for {
// stop if either slice is empty of the current head elements do not match
if len(prefixSplit) == 0 || len(pathSplit) == 0 ||
prefixSplit[0] != pathSplit[0] {
break
}
prefixSplit = prefixSplit[1:]
pathSplit = pathSplit[1:]
}
// The entire Prefix is included in Test Path
// Example: Prefix (/files) with Test Path (/files/test.csv)
if len(prefixSplit) == 0 ||
(len(prefixSplit) == 1 && prefixSplit[0] == ``) {
return pathContainsPrefix
}
// Test Path is part of the Prefix Hierarchy
// Example: Prefix (/files) with Test Path (/)
if len(pathSplit) == 0 ||
(len(pathSplit) == 1 && pathSplit[0] == ``) {
return pathIsPrefixParent
}
// Test Path is not with the Prefix Hierarchy
// Example: Prefix (/files) with Test Path (/files2)
return pathDiverged
}