Skip to content

Commit

Permalink
add support for virtual folders
Browse files Browse the repository at this point in the history
directories outside the user home directory can be exposed as virtual folders
  • Loading branch information
drakkan committed Feb 23, 2020
1 parent 382c6fd commit 45b9366
Show file tree
Hide file tree
Showing 27 changed files with 972 additions and 135 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Full featured and highly configurable SFTP server
- Per user and per directory permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode, changing access and modification times can be enabled or disabled.
- Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only).
- Per user IP filters are supported: login can be restricted to specific ranges of IP addresses or to a specific IP address.
- Virtual folders are supported: directories outside the user home directory can be exposed as virtual folders.
- Configurable custom commands and/or HTTP notifications on file upload, download, delete, rename, on SSH commands and on user add, update and delete.
- Automatically terminating idle connections.
- Atomic uploads are configurable.
Expand Down Expand Up @@ -666,7 +667,8 @@ For each account the following properties can be configured:
- `public_keys` array of public keys. At least one public key or the password is mandatory.
- `status` 1 means "active", 0 "inactive". An inactive account cannot login.
- `expiration_date` expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration.
- `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path.
- `home_dir` the user cannot upload or download files outside this directory. Must be an absolute path.
- `virtual_folders` list of mappings between virtual SFTP/SCP paths and local filesystem paths outside the user home directory. The specified paths must be absolute and the virtual path cannot be "/", it must be a sub directory. The parent directory for the specified virtual path must exist. SFTPGo will try to automatically create any missing parent directory for the configured virtual folders at user login
- `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo.
- `max_sessions` maximum concurrent sessions. 0 means unlimited.
- `quota_size` maximum size allowed as bytes. 0 means unlimited.
Expand Down Expand Up @@ -833,10 +835,19 @@ The logs can be divided into the following categories:
- `login_type` string. Can be `publickey`, `password`, `keyboard-interactive` or `no_auth_tryed`
- `error` string. Optional error description

### Brute force protection
## Brute force protection

The **connection failed logs** can be used for integration in tools such as [Fail2ban](http://www.fail2ban.org/). Example of [jails](./fail2ban/jails) and [filters](./fail2ban/filters) working with `systemd`/`journald` are available in fail2ban directory.

## Performance

SFTPGo can easily saturate a Gigabit connection, on low end hardware, with no special configurations and this is generally enough for most use cases.

The main bootlenecks are the encryption and the messages authentication, so if you can use a fast cipher with implicit message authentication, for example `aes128-gcm@openssh.com`, you will get a big performace boost.

There is an open [issue](https://github.com/drakkan/sftpgo/issues/69) with some other suggestions to improve performance and some comparisons against OpenSSH.


## Acknowledgements

- [pkg/sftp](https://github.com/pkg/sftp)
Expand Down
77 changes: 77 additions & 0 deletions dataprovider/dataprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,80 @@ func buildUserHomeDir(user *User) {
}
}

func isVirtualDirOverlapped(dir1, dir2 string) bool {
if dir1 == dir2 {
return true
}
if len(dir1) > len(dir2) {
if strings.HasPrefix(dir1, dir2+"/") {
return true
}
}
if len(dir2) > len(dir1) {
if strings.HasPrefix(dir2, dir1+"/") {
return true
}
}
return false
}

func isMappedDirOverlapped(dir1, dir2 string) bool {
if dir1 == dir2 {
return true
}
if len(dir1) > len(dir2) {
if strings.HasPrefix(dir1, dir2+string(os.PathSeparator)) {
return true
}
}
if len(dir2) > len(dir1) {
if strings.HasPrefix(dir2, dir1+string(os.PathSeparator)) {
return true
}
}
return false
}

func validateVirtualFolders(user *User) error {
if len(user.VirtualFolders) == 0 || user.FsConfig.Provider != 0 {
user.VirtualFolders = []vfs.VirtualFolder{}
return nil
}
var virtualFolders []vfs.VirtualFolder
mappedPaths := make(map[string]string)
for _, v := range user.VirtualFolders {
cleanedVPath := filepath.ToSlash(path.Clean(v.VirtualPath))
if !path.IsAbs(cleanedVPath) || cleanedVPath == "/" {
return &ValidationError{err: fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath)}
}
cleanedMPath := filepath.Clean(v.MappedPath)
if !filepath.IsAbs(cleanedMPath) {
return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v", v.MappedPath)}
}
if isMappedDirOverlapped(cleanedMPath, user.GetHomeDir()) {
return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v",
v.MappedPath, user.GetHomeDir())}
}
virtualFolders = append(virtualFolders, vfs.VirtualFolder{
VirtualPath: cleanedVPath,
MappedPath: cleanedMPath,
})
for k, virtual := range mappedPaths {
if isMappedDirOverlapped(k, cleanedMPath) {
return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v",
v.MappedPath, k)}
}
if isVirtualDirOverlapped(virtual, cleanedVPath) {
return &ValidationError{err: fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v",
v.VirtualPath, virtual)}
}
}
mappedPaths[cleanedMPath] = cleanedVPath
}
user.VirtualFolders = virtualFolders
return nil
}

func validatePermissions(user *User) error {
if len(user.Permissions) == 0 {
return &ValidationError{err: "please grant some permissions to this user"}
Expand Down Expand Up @@ -659,6 +733,9 @@ func validateUser(user *User) error {
if err := validateFilesystemConfig(user); err != nil {
return err
}
if err := validateVirtualFolders(user); err != nil {
return err
}
if user.Status < 0 || user.Status > 1 {
return &ValidationError{err: fmt.Sprintf("invalid user status: %v", user.Status)}
}
Expand Down
34 changes: 33 additions & 1 deletion dataprovider/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
"`last_login` bigint(20) NOT NULL, `status` int(11) NOT NULL, `filters` longtext DEFAULT NULL, " +
"`filesystem` longtext DEFAULT NULL);"
mysqlSchemaTableSQL = "CREATE TABLE `schema_version` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);"
mysqlUsersV2SQL = "ALTER TABLE `{{users}}` ADD COLUMN `virtual_folders` longtext NULL;"
)

// MySQLProvider auth provider for MySQL/MariaDB database
Expand Down Expand Up @@ -143,5 +144,36 @@ func (p MySQLProvider) initializeDatabase() error {
}

func (p MySQLProvider) migrateDatabase() error {
return sqlCommonMigrateDatabase(p.dbHandle)
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle)
if err != nil {
return err
}
if dbVersion.Version == sqlDatabaseVersion {
providerLog(logger.LevelDebug, "sql database is updated, current version: %v", dbVersion.Version)
return nil
}
if dbVersion.Version == 1 {
return updateMySQLDatabaseFrom1To2(p.dbHandle)
}
return nil
}

func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
providerLog(logger.LevelInfo, "updating database version: 1 -> 2")
sql := strings.Replace(mysqlUsersV2SQL, "{{users}}", config.UsersTable, 1)
tx, err := dbHandle.Begin()
if err != nil {
return err
}
_, err = tx.Exec(sql)
if err != nil {
tx.Rollback()
return err
}
err = sqlCommonUpdateDatabaseVersionWithTX(tx, 2)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
34 changes: 33 additions & 1 deletion dataprovider/pgsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
"expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL, "filters" text NULL,
"filesystem" text NULL);`
pgsqlSchemaTableSQL = `CREATE TABLE "schema_version" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);`
pgsqlUsersV2SQL = `ALTER TABLE "{{users}}" ADD COLUMN "virtual_folders" text NULL;`
)

// PGSQLProvider auth provider for PostgreSQL database
Expand Down Expand Up @@ -141,5 +142,36 @@ func (p PGSQLProvider) initializeDatabase() error {
}

func (p PGSQLProvider) migrateDatabase() error {
return sqlCommonMigrateDatabase(p.dbHandle)
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle)
if err != nil {
return err
}
if dbVersion.Version == sqlDatabaseVersion {
providerLog(logger.LevelDebug, "sql database is updated, current version: %v", dbVersion.Version)
return nil
}
if dbVersion.Version == 1 {
return updatePGSQLDatabaseFrom1To2(p.dbHandle)
}
return nil
}

func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
providerLog(logger.LevelInfo, "updating database version: 1 -> 2")
sql := strings.Replace(pgsqlUsersV2SQL, "{{users}}", config.UsersTable, 1)
tx, err := dbHandle.Begin()
if err != nil {
return err
}
_, err = tx.Exec(sql)
if err != nil {
tx.Rollback()
return err
}
err = sqlCommonUpdateDatabaseVersionWithTX(tx, 2)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
Loading

0 comments on commit 45b9366

Please sign in to comment.