mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
sftpd: add folder prefix middleware
This commit is contained in:
parent
90b324d707
commit
3ae8abda9e
9 changed files with 740 additions and 19 deletions
|
@ -153,6 +153,7 @@ func Init() {
|
||||||
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
|
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
|
||||||
KeyboardInteractiveHook: "",
|
KeyboardInteractiveHook: "",
|
||||||
PasswordAuthentication: true,
|
PasswordAuthentication: true,
|
||||||
|
FolderPrefix: "",
|
||||||
},
|
},
|
||||||
FTPD: ftpd.Configuration{
|
FTPD: ftpd.Configuration{
|
||||||
Bindings: []ftpd.Binding{defaultFTPDBinding},
|
Bindings: []ftpd.Binding{defaultFTPDBinding},
|
||||||
|
@ -977,6 +978,7 @@ func setViperDefaults() {
|
||||||
viper.SetDefault("sftpd.enabled_ssh_commands", globalConf.SFTPD.EnabledSSHCommands)
|
viper.SetDefault("sftpd.enabled_ssh_commands", globalConf.SFTPD.EnabledSSHCommands)
|
||||||
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
|
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
|
||||||
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
|
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
|
||||||
|
viper.SetDefault("sftpd.folder_prefix", globalConf.SFTPD.FolderPrefix)
|
||||||
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
|
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
|
||||||
viper.SetDefault("ftpd.banner_file", globalConf.FTPD.BannerFile)
|
viper.SetDefault("ftpd.banner_file", globalConf.FTPD.BannerFile)
|
||||||
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
|
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -18,6 +18,7 @@ require (
|
||||||
github.com/go-chi/render v1.0.1
|
github.com/go-chi/render v1.0.1
|
||||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -281,6 +281,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||||
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
|
229
sftpd/middleware.go
Normal file
229
sftpd/middleware.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
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
|
||||||
|
}
|
328
sftpd/middleware_test.go
Normal file
328
sftpd/middleware_test.go
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
package sftpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/sftpd/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrefixMiddlewareSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
MockCtl *gomock.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) BeforeTest(_, _ string) {
|
||||||
|
Suite.MockCtl = gomock.NewController(Suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) AfterTest(_, _ string) {
|
||||||
|
Suite.MockCtl.Finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileWriter() {
|
||||||
|
prefix := prefixMiddleware{prefix: `/files`}
|
||||||
|
|
||||||
|
// parent of prefix
|
||||||
|
WriterAt, err := prefix.Filewrite(&sftp.Request{Filepath: `/`})
|
||||||
|
Suite.Nil(WriterAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
// file path and prefix are unrelated
|
||||||
|
WriterAt, err = prefix.Filewrite(&sftp.Request{Filepath: `/random`})
|
||||||
|
Suite.Nil(WriterAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
// file path is sub path of configured prefix
|
||||||
|
// mocked returns are not import, just the call to the next file writer
|
||||||
|
mockedWriter := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
mockedWriter.EXPECT().
|
||||||
|
Filewrite(&sftp.Request{Filepath: `/data`}).
|
||||||
|
Return(nil, nil)
|
||||||
|
prefix.next = mockedWriter
|
||||||
|
WriterAt, err = prefix.Filewrite(&sftp.Request{Filepath: `/files/data`})
|
||||||
|
Suite.Nil(err)
|
||||||
|
Suite.Nil(WriterAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileReader() {
|
||||||
|
middleware := prefixMiddleware{prefix: `/files`}
|
||||||
|
|
||||||
|
// parent of prefix
|
||||||
|
ReaderAt, err := middleware.Fileread(&sftp.Request{Filepath: `/`})
|
||||||
|
Suite.Nil(ReaderAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
// file path and prefix are unrelated
|
||||||
|
ReaderAt, err = middleware.Fileread(&sftp.Request{Filepath: `/random`})
|
||||||
|
Suite.Nil(ReaderAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
// file path is sub path of configured prefix
|
||||||
|
// mocked returns are not import, just the call to the next file writer
|
||||||
|
mockedReader := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
mockedReader.EXPECT().
|
||||||
|
Fileread(&sftp.Request{Filepath: `/data`}).
|
||||||
|
Return(nil, nil)
|
||||||
|
middleware.next = mockedReader
|
||||||
|
ReaderAt, err = middleware.Fileread(&sftp.Request{Filepath: `/files/data`})
|
||||||
|
Suite.Nil(err)
|
||||||
|
Suite.Nil(ReaderAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestOpenFile() {
|
||||||
|
middleware := prefixMiddleware{prefix: `/files`}
|
||||||
|
|
||||||
|
ReadWriteAt, err := middleware.OpenFile(&sftp.Request{Filepath: `/`})
|
||||||
|
Suite.Nil(ReadWriteAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
// file path and prefix are unrelated
|
||||||
|
ReadWriteAt, err = middleware.OpenFile(&sftp.Request{Filepath: `/random`})
|
||||||
|
Suite.Nil(ReadWriteAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
RequestPath string
|
||||||
|
NextPath string
|
||||||
|
}{
|
||||||
|
// test normalization of various request paths
|
||||||
|
{RequestPath: `/files/data.csv`, NextPath: `/data.csv`},
|
||||||
|
{RequestPath: `files/data.csv`, NextPath: `/data.csv`},
|
||||||
|
{RequestPath: `//files/./data.csv`, NextPath: `/data.csv`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
OpenFileMock := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
OpenFileMock.EXPECT().
|
||||||
|
OpenFile(&sftp.Request{Filepath: test.NextPath}).
|
||||||
|
Return(nil, nil)
|
||||||
|
middleware.next = OpenFileMock
|
||||||
|
|
||||||
|
ReadWriteAt, err = middleware.OpenFile(&sftp.Request{Filepath: test.RequestPath})
|
||||||
|
Suite.Nil(ReadWriteAt)
|
||||||
|
Suite.Nil(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileListForwarding() {
|
||||||
|
var tests = []struct {
|
||||||
|
Method string
|
||||||
|
FilePath string
|
||||||
|
FwdPath string
|
||||||
|
}{
|
||||||
|
{Method: `List`, FilePath: `/files/data`, FwdPath: `/data`},
|
||||||
|
{Method: `List`, FilePath: `/./files/data`, FwdPath: `/data`},
|
||||||
|
{Method: `List`, FilePath: `files/data`, FwdPath: `/data`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
FileListMock := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
FileListMock.EXPECT().
|
||||||
|
Filelist(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.FwdPath,
|
||||||
|
}).Return(nil, nil)
|
||||||
|
|
||||||
|
handlers := newPrefixMiddleware(`/files`, FileListMock)
|
||||||
|
ListerAt, err := handlers.Filelist(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.FilePath,
|
||||||
|
})
|
||||||
|
Suite.Nil(ListerAt)
|
||||||
|
Suite.Nil(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileList() {
|
||||||
|
var tests = []struct {
|
||||||
|
Method string
|
||||||
|
FilePath string
|
||||||
|
ExpectedErr error
|
||||||
|
ExpectedPath string
|
||||||
|
}{
|
||||||
|
{Method: `List`, FilePath: `/random`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `List`, FilePath: `/`, ExpectedPath: `files`},
|
||||||
|
{Method: `Stat`, FilePath: `/`, ExpectedPath: `/`},
|
||||||
|
{Method: `NotAnOp`, ExpectedErr: sftp.ErrSSHFxOpUnsupported},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
middleware := prefixMiddleware{prefix: `/files`}
|
||||||
|
ListerAt, err := middleware.Filelist(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.FilePath,
|
||||||
|
})
|
||||||
|
if test.ExpectedErr != nil {
|
||||||
|
Suite.Equal(test.ExpectedErr, err)
|
||||||
|
Suite.Nil(ListerAt)
|
||||||
|
} else {
|
||||||
|
Suite.Nil(err)
|
||||||
|
Suite.IsType(listerAt{}, ListerAt)
|
||||||
|
if directList, ok := ListerAt.(listerAt); ok {
|
||||||
|
Suite.Len(directList, 1)
|
||||||
|
Suite.Equal(test.ExpectedPath, directList[0].Name())
|
||||||
|
Suite.InDelta(time.Now().Unix(), directList[0].ModTime().Unix(), 1)
|
||||||
|
Suite.True(directList[0].IsDir())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestLstat() {
|
||||||
|
middleware := prefixMiddleware{prefix: `/files`}
|
||||||
|
ListerAt, err := middleware.Lstat(&sftp.Request{Filepath: `/`})
|
||||||
|
Suite.Nil(err)
|
||||||
|
Suite.IsType(listerAt{}, ListerAt)
|
||||||
|
if directList, ok := ListerAt.(listerAt); ok {
|
||||||
|
Suite.Len(directList, 1)
|
||||||
|
Suite.Equal(`/`, directList[0].Name())
|
||||||
|
Suite.InDelta(time.Now().Unix(), directList[0].ModTime().Unix(), 1)
|
||||||
|
Suite.True(directList[0].IsDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware = prefixMiddleware{prefix: `/files`}
|
||||||
|
ListerAt, err = middleware.Lstat(&sftp.Request{Filepath: `/random`})
|
||||||
|
Suite.Nil(ListerAt)
|
||||||
|
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||||
|
|
||||||
|
MockLstat := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
MockLstat.EXPECT().
|
||||||
|
Lstat(&sftp.Request{Filepath: "/data"}).
|
||||||
|
Return(nil, nil)
|
||||||
|
middleware = prefixMiddleware{prefix: `/files`}
|
||||||
|
middleware.next = MockLstat
|
||||||
|
|
||||||
|
ListerAt, err = middleware.Lstat(&sftp.Request{Filepath: `/files/data`})
|
||||||
|
Suite.Nil(err)
|
||||||
|
Suite.Nil(ListerAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileCmdForwarding() {
|
||||||
|
var tests = []struct {
|
||||||
|
Method string
|
||||||
|
FilePath string
|
||||||
|
TargetPath string
|
||||||
|
FwdFilePath string
|
||||||
|
FwdTargetPath string
|
||||||
|
}{
|
||||||
|
{Method: `Rename`, FilePath: `/files/data.csv`, TargetPath: `/files/new-data.csv`, FwdFilePath: `/data.csv`, FwdTargetPath: `/new-data.csv`},
|
||||||
|
{Method: `Rename`, FilePath: `files/data.csv`, TargetPath: `files/new-data.csv`, FwdFilePath: `/data.csv`, FwdTargetPath: `/new-data.csv`},
|
||||||
|
{Method: `Symlink`, FilePath: `/./files/data.csv`, TargetPath: `files/new-data.csv`, FwdFilePath: `/data.csv`, FwdTargetPath: `/new-data.csv`},
|
||||||
|
|
||||||
|
{Method: `Setstat`, FilePath: `files/data.csv`, FwdFilePath: `/data.csv`},
|
||||||
|
{Method: `Remove`, FilePath: `/./files/data.csv`, FwdFilePath: `/data.csv`},
|
||||||
|
{Method: `Rmdir`, FilePath: `files/data`, FwdFilePath: `/data`},
|
||||||
|
{Method: `Mkdir`, FilePath: `/./files/data`, FwdFilePath: `/data`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
FileCmdMock := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||||
|
FileCmdMock.EXPECT().
|
||||||
|
Filecmd(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.FwdFilePath,
|
||||||
|
Target: test.FwdTargetPath,
|
||||||
|
}).Return(nil)
|
||||||
|
|
||||||
|
middleware := prefixMiddleware{
|
||||||
|
prefix: `/files`,
|
||||||
|
next: FileCmdMock,
|
||||||
|
}
|
||||||
|
|
||||||
|
Suite.Nil(middleware.Filecmd(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.FilePath,
|
||||||
|
Target: test.TargetPath,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestFileCmdErrors() {
|
||||||
|
middleware := prefixMiddleware{prefix: `/files`}
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
Method string
|
||||||
|
RequestPath string
|
||||||
|
TargetPath string
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
// two path methods
|
||||||
|
{Method: `Rename`, RequestPath: `/`, TargetPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Rename`, RequestPath: `/random`, TargetPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Rename`, RequestPath: `/random`, TargetPath: `/files`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Symlink`, RequestPath: `/`, TargetPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Symlink`, RequestPath: `/random`, TargetPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Symlink`, RequestPath: `/random`, TargetPath: `/files`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
|
||||||
|
// single path methods
|
||||||
|
{Method: `Setstat`, RequestPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Setstat`, RequestPath: `/unrelated`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Rmdir`, RequestPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Rmdir`, RequestPath: `/unrelated`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Mkdir`, RequestPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Mkdir`, RequestPath: `/unrelated`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Remove`, RequestPath: `/`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
{Method: `Remove`, RequestPath: `/unrelated`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
||||||
|
|
||||||
|
{Method: `NotACmd`, ExpectedErr: sftp.ErrSSHFxOpUnsupported},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := middleware.Filecmd(&sftp.Request{
|
||||||
|
Method: test.Method,
|
||||||
|
Filepath: test.RequestPath,
|
||||||
|
Target: test.TargetPath,
|
||||||
|
})
|
||||||
|
Suite.Equal(test.ExpectedErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestNextFolder() {
|
||||||
|
prefix := prefixMiddleware{prefix: `/files/data`}
|
||||||
|
Suite.Equal(`files`, prefix.nextListFolder(`/`))
|
||||||
|
Suite.Equal(`files`, prefix.nextListFolder(``))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`/files`))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`files`))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`files/`))
|
||||||
|
|
||||||
|
prefix = prefixMiddleware{prefix: `files/data`}
|
||||||
|
Suite.Equal(`files`, prefix.nextListFolder(`/`))
|
||||||
|
Suite.Equal(`files`, prefix.nextListFolder(``))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`/files`))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`files`))
|
||||||
|
Suite.Equal(`data`, prefix.nextListFolder(`files/`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestContainsPrefix() {
|
||||||
|
prefix := prefixMiddleware{prefix: `/`}
|
||||||
|
Suite.True(prefix.containsPrefix(`/data`))
|
||||||
|
Suite.True(prefix.containsPrefix(`/`))
|
||||||
|
|
||||||
|
prefix = prefixMiddleware{prefix: `/files`}
|
||||||
|
Suite.True(prefix.containsPrefix(`files`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Suite *PrefixMiddlewareSuite) TestRemoveFolderPrefix() {
|
||||||
|
prefix := prefixMiddleware{prefix: `/`}
|
||||||
|
path, ok := prefix.removeFolderPrefix(`/files`)
|
||||||
|
Suite.Equal(`/files`, path)
|
||||||
|
Suite.True(ok)
|
||||||
|
|
||||||
|
prefix = prefixMiddleware{prefix: `/files`}
|
||||||
|
path, ok = prefix.removeFolderPrefix(`files`)
|
||||||
|
Suite.Equal(`/`, path)
|
||||||
|
Suite.True(ok)
|
||||||
|
|
||||||
|
path, ok = prefix.removeFolderPrefix(`/random`)
|
||||||
|
Suite.Equal(`/random`, path)
|
||||||
|
Suite.False(ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFolderPrefixSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(PrefixMiddlewareSuite))
|
||||||
|
}
|
140
sftpd/mocks/middleware.go
Normal file
140
sftpd/mocks/middleware.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: ../middleware.go
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
io "io"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
sftp "github.com/pkg/sftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockMiddleware is a mock of Middleware interface.
|
||||||
|
type MockMiddleware struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockMiddlewareMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockMiddlewareMockRecorder is the mock recorder for MockMiddleware.
|
||||||
|
type MockMiddlewareMockRecorder struct {
|
||||||
|
mock *MockMiddleware
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockMiddleware creates a new mock instance.
|
||||||
|
func NewMockMiddleware(ctrl *gomock.Controller) *MockMiddleware {
|
||||||
|
mock := &MockMiddleware{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockMiddlewareMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockMiddleware) EXPECT() *MockMiddlewareMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filecmd mocks base method.
|
||||||
|
func (m *MockMiddleware) Filecmd(arg0 *sftp.Request) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Filecmd", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filecmd indicates an expected call of Filecmd.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) Filecmd(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filecmd", reflect.TypeOf((*MockMiddleware)(nil).Filecmd), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filelist mocks base method.
|
||||||
|
func (m *MockMiddleware) Filelist(arg0 *sftp.Request) (sftp.ListerAt, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Filelist", arg0)
|
||||||
|
ret0, _ := ret[0].(sftp.ListerAt)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filelist indicates an expected call of Filelist.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) Filelist(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filelist", reflect.TypeOf((*MockMiddleware)(nil).Filelist), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fileread mocks base method.
|
||||||
|
func (m *MockMiddleware) Fileread(arg0 *sftp.Request) (io.ReaderAt, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Fileread", arg0)
|
||||||
|
ret0, _ := ret[0].(io.ReaderAt)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fileread indicates an expected call of Fileread.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) Fileread(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fileread", reflect.TypeOf((*MockMiddleware)(nil).Fileread), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filewrite mocks base method.
|
||||||
|
func (m *MockMiddleware) Filewrite(arg0 *sftp.Request) (io.WriterAt, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Filewrite", arg0)
|
||||||
|
ret0, _ := ret[0].(io.WriterAt)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filewrite indicates an expected call of Filewrite.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) Filewrite(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filewrite", reflect.TypeOf((*MockMiddleware)(nil).Filewrite), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lstat mocks base method.
|
||||||
|
func (m *MockMiddleware) Lstat(arg0 *sftp.Request) (sftp.ListerAt, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Lstat", arg0)
|
||||||
|
ret0, _ := ret[0].(sftp.ListerAt)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lstat indicates an expected call of Lstat.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) Lstat(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lstat", reflect.TypeOf((*MockMiddleware)(nil).Lstat), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFile mocks base method.
|
||||||
|
func (m *MockMiddleware) OpenFile(arg0 *sftp.Request) (sftp.WriterAtReaderAt, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "OpenFile", arg0)
|
||||||
|
ret0, _ := ret[0].(sftp.WriterAtReaderAt)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFile indicates an expected call of OpenFile.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) OpenFile(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenFile", reflect.TypeOf((*MockMiddleware)(nil).OpenFile), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatVFS mocks base method.
|
||||||
|
func (m *MockMiddleware) StatVFS(arg0 *sftp.Request) (*sftp.StatVFS, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StatVFS", arg0)
|
||||||
|
ret0, _ := ret[0].(*sftp.StatVFS)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatVFS indicates an expected call of StatVFS.
|
||||||
|
func (mr *MockMiddlewareMockRecorder) StatVFS(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatVFS", reflect.TypeOf((*MockMiddleware)(nil).StatVFS), arg0)
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -118,8 +119,10 @@ type Configuration struct {
|
||||||
KeyboardInteractiveHook string `json:"keyboard_interactive_auth_hook" mapstructure:"keyboard_interactive_auth_hook"`
|
KeyboardInteractiveHook string `json:"keyboard_interactive_auth_hook" mapstructure:"keyboard_interactive_auth_hook"`
|
||||||
// PasswordAuthentication specifies whether password authentication is allowed.
|
// PasswordAuthentication specifies whether password authentication is allowed.
|
||||||
PasswordAuthentication bool `json:"password_authentication" mapstructure:"password_authentication"`
|
PasswordAuthentication bool `json:"password_authentication" mapstructure:"password_authentication"`
|
||||||
certChecker *ssh.CertChecker
|
// Virtual root folder prefix to include in all file operations (ex: /files)
|
||||||
parsedUserCAKeys []ssh.PublicKey
|
FolderPrefix string `json:"folder_prefix" mapstructure:"folder_prefix"`
|
||||||
|
certChecker *ssh.CertChecker
|
||||||
|
parsedUserCAKeys []ssh.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticationError struct {
|
type authenticationError struct {
|
||||||
|
@ -198,6 +201,7 @@ func (c *Configuration) Initialize(configDir string) error {
|
||||||
c.configureKeyboardInteractiveAuth(serverConfig)
|
c.configureKeyboardInteractiveAuth(serverConfig)
|
||||||
c.configureLoginBanner(serverConfig, configDir)
|
c.configureLoginBanner(serverConfig, configDir)
|
||||||
c.checkSSHCommands()
|
c.checkSSHCommands()
|
||||||
|
c.checkFolderPrefix()
|
||||||
|
|
||||||
exitChannel := make(chan error, 1)
|
exitChannel := make(chan error, 1)
|
||||||
serviceStatus.Bindings = nil
|
serviceStatus.Bindings = nil
|
||||||
|
@ -394,9 +398,9 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
|
||||||
|
|
||||||
defer user.CloseFs() //nolint:errcheck
|
defer user.CloseFs() //nolint:errcheck
|
||||||
|
|
||||||
logger.Log(logger.LevelInfo, common.ProtocolSSH, connectionID,
|
logger.Log(logger.LevelDebug, common.ProtocolSSH, connectionID,
|
||||||
"User id: %d, logged in with: %#v, username: %#v, home_dir: %#v remote addr: %#v",
|
"User %#v, logged in with: %#v, from ip: %#v, client version %#v",
|
||||||
user.ID, loginType, user.Username, user.HomeDir, ipAddr)
|
user.Username, loginType, ipAddr, string(sconn.ClientVersion()))
|
||||||
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
|
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
|
||||||
|
|
||||||
sshConnection := common.NewSSHConnection(connectionID, conn)
|
sshConnection := common.NewSSHConnection(connectionID, conn)
|
||||||
|
@ -475,11 +479,27 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
|
||||||
common.Connections.Add(connection)
|
common.Connections.Add(connection)
|
||||||
defer common.Connections.Remove(connection.GetID())
|
defer common.Connections.Remove(connection.GetID())
|
||||||
|
|
||||||
// Create a new handler for the currently logged in user's server.
|
var handlers sftp.Handlers
|
||||||
handler := c.createHandler(connection)
|
|
||||||
|
if c.FolderPrefix != "" {
|
||||||
|
prefixMiddleware := newPrefixMiddleware(c.FolderPrefix, connection)
|
||||||
|
handlers = sftp.Handlers{
|
||||||
|
FileGet: prefixMiddleware,
|
||||||
|
FilePut: prefixMiddleware,
|
||||||
|
FileCmd: prefixMiddleware,
|
||||||
|
FileList: prefixMiddleware,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handlers = sftp.Handlers{
|
||||||
|
FileGet: connection,
|
||||||
|
FilePut: connection,
|
||||||
|
FileCmd: connection,
|
||||||
|
FileList: connection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the server instance for the channel using the handler we created above.
|
// Create the server instance for the channel using the handler we created above.
|
||||||
server := sftp.NewRequestServer(channel, handler, sftp.WithRSAllocator())
|
server := sftp.NewRequestServer(channel, handlers, sftp.WithRSAllocator())
|
||||||
|
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
if err := server.Serve(); err == io.EOF {
|
if err := server.Serve(); err == io.EOF {
|
||||||
|
@ -492,15 +512,6 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) createHandler(connection *Connection) sftp.Handlers {
|
|
||||||
return sftp.Handlers{
|
|
||||||
FileGet: connection,
|
|
||||||
FilePut: connection,
|
|
||||||
FileCmd: connection,
|
|
||||||
FileList: connection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAuthError(ip string, err error) {
|
func checkAuthError(ip string, err error) {
|
||||||
if authErrors, ok := err.(*ssh.ServerAuthError); ok {
|
if authErrors, ok := err.(*ssh.ServerAuthError); ok {
|
||||||
// check public key auth errors here
|
// check public key auth errors here
|
||||||
|
@ -587,6 +598,14 @@ func (c *Configuration) checkSSHCommands() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.EnabledSSHCommands = sshCommands
|
c.EnabledSSHCommands = sshCommands
|
||||||
|
logger.Debug(logSender, "", "enabled SSH commands %v", c.EnabledSSHCommands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) checkFolderPrefix() {
|
||||||
|
if c.FolderPrefix != "" {
|
||||||
|
c.FolderPrefix = path.Join("/", c.FolderPrefix)
|
||||||
|
logger.Debug(logSender, "", "folder prefix %#v configured", c.FolderPrefix)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) generateDefaultHostKeys(configDir string) error {
|
func (c *Configuration) generateDefaultHostKeys(configDir string) error {
|
||||||
|
|
|
@ -85,7 +85,7 @@ func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := connection.CloseFS()
|
err := connection.CloseFS()
|
||||||
connection.Log(logger.LevelDebug, "unable to unmarsh ssh command, close fs, err: %v", err)
|
connection.Log(logger.LevelDebug, "unable to unmarshal ssh command, close fs, err: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,8 @@
|
||||||
"scp"
|
"scp"
|
||||||
],
|
],
|
||||||
"keyboard_interactive_auth_hook": "",
|
"keyboard_interactive_auth_hook": "",
|
||||||
"password_authentication": true
|
"password_authentication": true,
|
||||||
|
"folder_prefix": ""
|
||||||
},
|
},
|
||||||
"ftpd": {
|
"ftpd": {
|
||||||
"bindings": [
|
"bindings": [
|
||||||
|
|
Loading…
Reference in a new issue