Skip to content

Commit

Permalink
add support for a start directory
Browse files Browse the repository at this point in the history
Fixes drakkan#705

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
  • Loading branch information
drakkan committed Mar 3, 2022
1 parent 4519bff commit 5c2fd8d
Show file tree
Hide file tree
Showing 28 changed files with 478 additions and 94 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:
upload-coverage: false

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}

Expand Down Expand Up @@ -218,10 +218,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17

Expand Down Expand Up @@ -274,10 +274,10 @@ jobs:
- 3307:3306

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17

Expand Down Expand Up @@ -345,12 +345,12 @@ jobs:
go: latest
go-arch: arm7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
if: ${{ matrix.arch == 'amd64' }}
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}

Expand Down Expand Up @@ -449,10 +449,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
optional_deps: false
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Gather image information
id: info
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ on:
tags: 'v*'

env:
GO_VERSION: 1.17.5
GO_VERSION: 1.17.7

jobs:
prepare-sources-with-deps:
name: Prepare sources with deps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}

Expand Down Expand Up @@ -45,9 +45,9 @@ jobs:
os: [macos-10.15, windows-2019]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}

Expand Down Expand Up @@ -283,10 +283,10 @@ jobs:
tar-arch: armv7

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
if: ${{ matrix.arch == 'amd64' }}
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}

Expand Down Expand Up @@ -467,7 +467,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Get versions
id: get_version
run: |
Expand Down
7 changes: 6 additions & 1 deletion cmd/portable.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
portableAdvertiseCredentials bool
portableUsername string
portablePassword string
portableStartDir string
portableLogFile string
portableLogVerbose bool
portableLogUTCTime bool
Expand Down Expand Up @@ -163,7 +164,8 @@ Please take a look at the usage below to customize the serving parameters`,
},
Filters: dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
FilePatterns: parsePatternsFilesFilters(),
FilePatterns: parsePatternsFilesFilters(),
StartDirectory: portableStartDir,
},
},
FsConfig: vfs.Filesystem{
Expand Down Expand Up @@ -246,6 +248,9 @@ func init() {
This can be an absolute path or a path
relative to the current directory
`)
portableCmd.Flags().StringVar(&portableStartDir, "start-directory", "/", `Alternate start directory.
This is a virtual path not a filesystem
path`)
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
< 0 disabled`)
portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
Expand Down
1 change: 1 addition & 0 deletions common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,7 @@ func TestGetTLSVersion(t *testing.T) {
func TestCleanPath(t *testing.T) {
assert.Equal(t, "/", util.CleanPath("/"))
assert.Equal(t, "/", util.CleanPath("."))
assert.Equal(t, "/", util.CleanPath(""))
assert.Equal(t, "/", util.CleanPath("/."))
assert.Equal(t, "/", util.CleanPath("/a/.."))
assert.Equal(t, "/a", util.CleanPath("/a/"))
Expand Down
16 changes: 13 additions & 3 deletions dataprovider/dataprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,18 @@ func validateTransferLimitsFilter(user *User) error {
return nil
}

func updateFiltersValues(user *User) {
if !user.HasExternalAuth() {
user.Filters.ExternalAuthCacheTime = 0
}
if user.Filters.StartDirectory != "" {
user.Filters.StartDirectory = util.CleanPath(user.Filters.StartDirectory)
if user.Filters.StartDirectory == "/" {
user.Filters.StartDirectory = ""
}
}
}

func validateFilters(user *User) error {
checkEmptyFiltersStruct(user)
if err := validateIPFilters(user); err != nil {
Expand Down Expand Up @@ -2061,9 +2073,7 @@ func validateFilters(user *User) error {
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
}
}
if !user.HasExternalAuth() {
user.Filters.ExternalAuthCacheTime = 0
}
updateFiltersValues(user)

return validateFiltersPatternExtensions(user)
}
Expand Down
47 changes: 46 additions & 1 deletion dataprovider/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ func (u *User) CheckFsRoot(connectionID string) error {
return err
}
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
if u.Filters.StartDirectory != "" {
err = u.checkDirWithParents(u.Filters.StartDirectory, connectionID)
if err != nil {
logger.Warn(logSender, connectionID, "could not create start directory %#v, err: %v",
u.Filters.StartDirectory, err)
}
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
fs, err = u.GetFilesystemForPath(v.VirtualPath, connectionID)
Expand All @@ -234,6 +241,23 @@ func (u *User) CheckFsRoot(connectionID string) error {
return nil
}

// GetCleanedPath returns a clean POSIX absolute path using the user start directory as base
// if the provided rawVirtualPath is relative
func (u *User) GetCleanedPath(rawVirtualPath string) string {
if u.Filters.StartDirectory != "" {
if !path.IsAbs(rawVirtualPath) {
var b strings.Builder

b.Grow(len(u.Filters.StartDirectory) + 1 + len(rawVirtualPath))
b.WriteString(u.Filters.StartDirectory)
b.WriteString("/")
b.WriteString(rawVirtualPath)
return util.CleanPath(b.String())
}
}
return util.CleanPath(rawVirtualPath)
}

// isFsEqual returns true if the fs has the same configuration
func (u *User) isFsEqual(other *User) bool {
if u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {
Expand All @@ -242,6 +266,9 @@ func (u *User) isFsEqual(other *User) bool {
if !u.FsConfig.IsEqual(&other.FsConfig) {
return false
}
if u.Filters.StartDirectory != other.Filters.StartDirectory {
return false
}
if len(u.VirtualFolders) != len(other.VirtualFolders) {
return false
}
Expand Down Expand Up @@ -586,13 +613,30 @@ func (u *User) GetVirtualFoldersInPath(virtualPath string) map[string]bool {
}
}

if u.Filters.StartDirectory != "" {
dirsForPath := util.GetDirsForVirtualPath(u.Filters.StartDirectory)
for index := range dirsForPath {
d := dirsForPath[index]
if d == "/" {
continue
}
if path.Dir(d) == virtualPath {
result[d] = true
}
}
}

return result
}

func (u *User) hasVirtualDirs() bool {
return len(u.VirtualFolders) > 0 || u.Filters.StartDirectory != ""
}

// FilterListDir adds virtual folders and remove hidden items from the given files list
func (u *User) FilterListDir(dirContents []os.FileInfo, virtualPath string) []os.FileInfo {
filter := u.getPatternsFilterForPath(virtualPath)
if len(u.VirtualFolders) == 0 && filter.DenyPolicy != sdk.DenyPolicyHide {
if !u.hasVirtualDirs() && filter.DenyPolicy != sdk.DenyPolicyHide {
return dirContents
}

Expand Down Expand Up @@ -1395,6 +1439,7 @@ func (u *User) getACopy() User {
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
filters.DisableFsChecks = u.Filters.DisableFsChecks
filters.StartDirectory = u.Filters.StartDirectory
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
filters.ExternalAuthCacheTime = u.Filters.ExternalAuthCacheTime
filters.WebClient = make([]string, len(u.Filters.WebClient))
Expand Down
2 changes: 1 addition & 1 deletion docs/defender.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The built-in `defender` allows you to configure an auto-blocking policy for SFTPGo and thus helps to prevent DoS (Denial of Service) and brute force password guessing.

If enabled it will protect SFTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
If enabled it will protect SFTP, HTTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.

You can configure a score for the following events:

Expand Down
3 changes: 3 additions & 0 deletions docs/portable-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ Flags:
"*" means any supported SSH command
including scp
(default [md5sum,sha1sum,cd,pwd,scp])
--start-directory string Alternate start directory.
This is a virtual path not a filesystem
path (default "/")
-u, --username string Leave empty to use an auto generated
value
--webdav-cert string Path to the certificate file for WebDAV
Expand Down
62 changes: 62 additions & 0 deletions ftpd/ftpd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,68 @@ func TestBasicFTPHandling(t *testing.T) {
50*time.Millisecond)
}

func TestStartDirectory(t *testing.T) {
startDir := "/start/dir"
u := getTestUser()
u.Filters.StartDirectory = startDir
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.Filters.StartDirectory = startDir
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClient(user, true, nil)
if assert.NoError(t, err) {
currentDir, err := client.CurrentDir()
assert.NoError(t, err)
assert.Equal(t, startDir, currentDir)

testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
entries, err := client.List(".")
assert.NoError(t, err)
assert.Len(t, entries, 3)

entries, err = client.List("/")
assert.NoError(t, err)
assert.Len(t, entries, 2)

err = client.ChangeDirToParent()
assert.NoError(t, err)
currentDir, err = client.CurrentDir()
assert.NoError(t, err)
assert.Equal(t, path.Dir(startDir), currentDir)
err = client.ChangeDirToParent()
assert.NoError(t, err)
currentDir, err = client.CurrentDir()
assert.NoError(t, err)
assert.Equal(t, "/", currentDir)

err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
err = client.Quit()
assert.NoError(t, err)
}
}

_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
}

func TestMultiFactorAuth(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
Expand Down
2 changes: 2 additions & 0 deletions ftpd/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ func (cc mockFTPClientContext) Path() string {
return ""
}

func (cc mockFTPClientContext) SetPath(name string) {}

func (cc mockFTPClientContext) SetDebug(debug bool) {}

func (cc mockFTPClientContext) Debug() bool {
Expand Down
Loading

0 comments on commit 5c2fd8d

Please sign in to comment.