sftpd: add support for chmod/chown
added matching permissions too and a new setting "setstat_mode". Setting setstat_mode to 1 you can keep the previous behaviour that silently ignore setstat requests
This commit is contained in:
parent
206799ff1c
commit
bb37a1c1ce
13 changed files with 291 additions and 101 deletions
13
README.md
13
README.md
|
@ -12,7 +12,7 @@ Full featured and highly configurable SFTP server
|
||||||
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
|
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
|
||||||
- Bandwidth throttling is supported, with distinct settings for upload and download.
|
- Bandwidth throttling is supported, with distinct settings for upload and download.
|
||||||
- Per user maximum concurrent sessions.
|
- Per user maximum concurrent sessions.
|
||||||
- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks can be enabled or disabled.
|
- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode 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 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).
|
||||||
- Configurable custom commands and/or HTTP notifications on files upload, download, delete, rename and on users add, update and delete.
|
- Configurable custom commands and/or HTTP notifications on files upload, download, delete, rename and on users add, update and delete.
|
||||||
- Automatically terminating idle connections.
|
- Automatically terminating idle connections.
|
||||||
|
@ -150,6 +150,7 @@ The `sftpgo` configuration file contains the following sections:
|
||||||
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
|
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
|
||||||
- `macs`, list of strings. available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
|
- `macs`, list of strings. available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
|
||||||
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to send no login banner
|
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to send no login banner
|
||||||
|
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions and owner/group are executed. 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
|
||||||
- **"data_provider"**, the configuration for the data provider
|
- **"data_provider"**, the configuration for the data provider
|
||||||
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory`
|
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory`
|
||||||
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
|
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
|
||||||
|
@ -207,7 +208,8 @@ Here is a full example showing the default config in JSON format:
|
||||||
"kex_algorithms": [],
|
"kex_algorithms": [],
|
||||||
"ciphers": [],
|
"ciphers": [],
|
||||||
"macs": [],
|
"macs": [],
|
||||||
"login_banner_file": ""
|
"login_banner_file": "",
|
||||||
|
"setstat_mode": 0
|
||||||
},
|
},
|
||||||
"data_provider": {
|
"data_provider": {
|
||||||
"driver": "sqlite",
|
"driver": "sqlite",
|
||||||
|
@ -363,6 +365,8 @@ For each account the following properties can be configured:
|
||||||
- `rename` rename files or directories is allowed
|
- `rename` rename files or directories is allowed
|
||||||
- `create_dirs` create directories is allowed
|
- `create_dirs` create directories is allowed
|
||||||
- `create_symlinks` create symbolic links is allowed
|
- `create_symlinks` create symbolic links is allowed
|
||||||
|
- `chmod` changing file or directory permissions is allowed
|
||||||
|
- `chown` changing file or directory owner and group is allowed
|
||||||
- `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited.
|
- `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited.
|
||||||
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited.
|
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited.
|
||||||
|
|
||||||
|
@ -452,11 +456,14 @@ The logs can be divided into the following categories:
|
||||||
- `connection_id` string. Unique connection identifier
|
- `connection_id` string. Unique connection identifier
|
||||||
- `protocol` string. `SFTP` or `SCP`
|
- `protocol` string. `SFTP` or `SCP`
|
||||||
- **"command logs"**, SFTP/SCP command logs:
|
- **"command logs"**, SFTP/SCP command logs:
|
||||||
- `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`
|
- `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`
|
||||||
- `level` string
|
- `level` string
|
||||||
- `username`, string
|
- `username`, string
|
||||||
- `file_path` string
|
- `file_path` string
|
||||||
- `target_path` string
|
- `target_path` string
|
||||||
|
- `filemode` string. Valid for sender `Chmod` otherwise empty
|
||||||
|
- `uid` integer. Valid for sender `Chown` otherwise -1
|
||||||
|
- `gid` integer. Valid for sender `Chown` otherwise -1
|
||||||
- `connection_id` string. Unique connection identifier
|
- `connection_id` string. Unique connection identifier
|
||||||
- `protocol` string. `SFTP` or `SCP`
|
- `protocol` string. `SFTP` or `SCP`
|
||||||
- **"http logs"**, REST API logs:
|
- **"http logs"**, REST API logs:
|
||||||
|
|
|
@ -65,7 +65,7 @@ var (
|
||||||
BoltDataProviderName, MemoryDataProviderName}
|
BoltDataProviderName, MemoryDataProviderName}
|
||||||
// ValidPerms list that contains all the valid permissions for an user
|
// ValidPerms list that contains all the valid permissions for an user
|
||||||
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
|
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
|
||||||
PermCreateDirs, PermCreateSymlinks}
|
PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown}
|
||||||
config Config
|
config Config
|
||||||
provider Provider
|
provider Provider
|
||||||
sqlPlaceholders []string
|
sqlPlaceholders []string
|
||||||
|
|
|
@ -30,6 +30,10 @@ const (
|
||||||
PermCreateDirs = "create_dirs"
|
PermCreateDirs = "create_dirs"
|
||||||
// create symbolic links is allowed
|
// create symbolic links is allowed
|
||||||
PermCreateSymlinks = "create_symlinks"
|
PermCreateSymlinks = "create_symlinks"
|
||||||
|
// changing file or directory permissions is allowed
|
||||||
|
PermChmod = "chmod"
|
||||||
|
// changing file or directory owner is allowed
|
||||||
|
PermChown = "chown"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User defines an SFTP user
|
// User defines an SFTP user
|
||||||
|
|
|
@ -543,6 +543,8 @@ components:
|
||||||
- rename
|
- rename
|
||||||
- create_dirs
|
- create_dirs
|
||||||
- create_symlinks
|
- create_symlinks
|
||||||
|
- chmod
|
||||||
|
- chown
|
||||||
description: >
|
description: >
|
||||||
Permissions:
|
Permissions:
|
||||||
* `*` - all permissions are granted
|
* `*` - all permissions are granted
|
||||||
|
@ -554,6 +556,8 @@ components:
|
||||||
* `rename` - rename files or directories is allowed
|
* `rename` - rename files or directories is allowed
|
||||||
* `create_dirs` - create directories is allowed
|
* `create_dirs` - create directories is allowed
|
||||||
* `create_symlinks` - create links is allowed
|
* `create_symlinks` - create links is allowed
|
||||||
|
* `chmod` changing file or directory permissions is allowed
|
||||||
|
* `chown` changing file or directory owner and group is allowed
|
||||||
User:
|
User:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -150,13 +150,16 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandLog logs an SFTP/SCP command
|
// CommandLog logs an SFTP/SCP command
|
||||||
func CommandLog(command string, path string, target string, user string, connectionID string, protocol string) {
|
func CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int) {
|
||||||
logger.Info().
|
logger.Info().
|
||||||
Timestamp().
|
Timestamp().
|
||||||
Str("sender", command).
|
Str("sender", command).
|
||||||
Str("username", user).
|
Str("username", user).
|
||||||
Str("file_path", path).
|
Str("file_path", path).
|
||||||
Str("target_path", target).
|
Str("target_path", target).
|
||||||
|
Str("filemode", fileMode).
|
||||||
|
Int("uid", uid).
|
||||||
|
Int("gid", gid).
|
||||||
Str("connection_id", connectionID).
|
Str("connection_id", connectionID).
|
||||||
Str("protocol", protocol).
|
Str("protocol", protocol).
|
||||||
Msg("")
|
Msg("")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import argparse
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -160,7 +160,7 @@ def addCommonUserArguments(parser):
|
||||||
parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s")
|
parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s")
|
||||||
parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[],
|
parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[],
|
||||||
choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs',
|
choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs',
|
||||||
'create_symlinks'], help='Default: %(default)s')
|
'create_symlinks', 'chmod', 'chown'], help='Default: %(default)s')
|
||||||
parser.add_argument('-U', '--upload-bandwidth', type=int, default=0,
|
parser.add_argument('-U', '--upload-bandwidth', type=int, default=0,
|
||||||
help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s')
|
help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s')
|
||||||
parser.add_argument('-D', '--download-bandwidth', type=int, default=0,
|
parser.add_argument('-D', '--download-bandwidth', type=int, default=0,
|
||||||
|
|
144
sftpd/handler.go
144
sftpd/handler.go
|
@ -56,20 +56,20 @@ func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
||||||
|
|
||||||
p, err := c.buildPath(request.Filepath)
|
p, err := c.buildPath(request.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
if _, err := os.Stat(p); err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(p)
|
file, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "could not open file %#v for reading: %v", p, err)
|
c.Log(logger.LevelWarn, logSender, "could not open file %#v for reading: %v", p, err)
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Log(logger.LevelDebug, logSender, "fileread requested for path: %#v", p)
|
c.Log(logger.LevelDebug, logSender, "fileread requested for path: %#v", p)
|
||||||
|
@ -103,7 +103,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||||
|
|
||||||
p, err := c.buildPath(request.Filepath)
|
p, err := c.buildPath(request.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := p
|
filePath := p
|
||||||
|
@ -123,7 +123,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
c.Log(logger.LevelError, logSender, "error performing file stat %#v: %v", p, statErr)
|
c.Log(logger.LevelError, logSender, "error performing file stat %#v: %v", p, statErr)
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This happen if we upload a file that has the same name of an existing directory
|
// This happen if we upload a file that has the same name of an existing directory
|
||||||
|
@ -146,12 +146,12 @@ func (c Connection) Filecmd(request *sftp.Request) error {
|
||||||
|
|
||||||
p, err := c.buildPath(request.Filepath)
|
p, err := c.buildPath(request.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sftp.ErrSSHFxNoSuchFile
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := c.getSFTPCmdTargetPath(request.Target)
|
target, err := c.getSFTPCmdTargetPath(request.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sftp.ErrSSHFxOpUnsupported
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Log(logger.LevelDebug, logSender, "new cmd, method: %v, sourcePath: %#v, targetPath: %#v", request.Method,
|
c.Log(logger.LevelDebug, logSender, "new cmd, method: %v, sourcePath: %#v, targetPath: %#v", request.Method,
|
||||||
|
@ -159,10 +159,9 @@ func (c Connection) Filecmd(request *sftp.Request) error {
|
||||||
|
|
||||||
switch request.Method {
|
switch request.Method {
|
||||||
case "Setstat":
|
case "Setstat":
|
||||||
return nil
|
return c.handleSFTPSetstat(p, request)
|
||||||
case "Rename":
|
case "Rename":
|
||||||
err = c.handleSFTPRename(p, target)
|
if err = c.handleSFTPRename(p, target); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,8 +177,7 @@ func (c Connection) Filecmd(request *sftp.Request) error {
|
||||||
|
|
||||||
break
|
break
|
||||||
case "Symlink":
|
case "Symlink":
|
||||||
err = c.handleSFTPSymlink(p, target)
|
if err = c.handleSFTPSymlink(p, target); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +206,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||||
updateConnectionActivity(c.ID)
|
updateConnectionActivity(c.ID)
|
||||||
p, err := c.buildPath(request.Filepath)
|
p, err := c.buildPath(request.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch request.Method {
|
switch request.Method {
|
||||||
|
@ -220,11 +218,9 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||||
c.Log(logger.LevelDebug, logSender, "requested list file for dir: %#v", p)
|
c.Log(logger.LevelDebug, logSender, "requested list file for dir: %#v", p)
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(p)
|
files, err := ioutil.ReadDir(p)
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
c.Log(logger.LevelWarn, logSender, "error listing directory: %#v", err)
|
||||||
} else if err != nil {
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
c.Log(logger.LevelError, logSender, "error listing directory: %#v", err)
|
|
||||||
return nil, sftp.ErrSSHFxFailure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return listerAt(files), nil
|
return listerAt(files), nil
|
||||||
|
@ -233,13 +229,11 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||||
return nil, sftp.ErrSSHFxPermissionDenied
|
return nil, sftp.ErrSSHFxPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Log(logger.LevelDebug, logSender, "requested stat for file: %#v", p)
|
c.Log(logger.LevelDebug, logSender, "requested Stat for file: %#v", p)
|
||||||
s, err := os.Stat(p)
|
s, err := os.Stat(p)
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
c.Log(logger.LevelWarn, logSender, "error running Stat on file: %#v", err)
|
||||||
} else if err != nil {
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
c.Log(logger.LevelError, logSender, "error running STAT on file: %#v", err)
|
|
||||||
return nil, sftp.ErrSSHFxFailure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return listerAt([]os.FileInfo{s}), nil
|
return listerAt([]os.FileInfo{s}), nil
|
||||||
|
@ -251,27 +245,58 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||||
func (c Connection) getSFTPCmdTargetPath(requestTarget string) (string, error) {
|
func (c Connection) getSFTPCmdTargetPath(requestTarget string) (string, error) {
|
||||||
var target string
|
var target string
|
||||||
// If a target is provided in this request validate that it is going to the correct
|
// If a target is provided in this request validate that it is going to the correct
|
||||||
// location for the server. If it is not, return an operation unsupported error. This
|
// location for the server. If it is not, return an error
|
||||||
// is maybe not the best error response, but its not wrong either.
|
if len(requestTarget) > 0 {
|
||||||
if requestTarget != "" {
|
|
||||||
var err error
|
var err error
|
||||||
target, err = c.buildPath(requestTarget)
|
target, err = c.buildPath(requestTarget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return target, sftp.ErrSSHFxOpUnsupported
|
return target, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error {
|
||||||
|
if setstatMode == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
attrFlags := request.AttrFlags()
|
||||||
|
if attrFlags.Permissions {
|
||||||
|
if !c.User.HasPerm(dataprovider.PermChmod) {
|
||||||
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
|
}
|
||||||
|
fileMode := request.Attributes().FileMode()
|
||||||
|
if err := os.Chmod(path, fileMode); err != nil {
|
||||||
|
c.Log(logger.LevelWarn, logSender, "failed to chmod path %#v, mode: %v, err: %v", path, fileMode.String(), err)
|
||||||
|
return getSFTPErrorFromOSError(err)
|
||||||
|
}
|
||||||
|
logger.CommandLog(chmodLogSender, path, "", c.User.Username, fileMode.String(), c.ID, c.protocol, -1, -1)
|
||||||
|
return nil
|
||||||
|
} else if attrFlags.UidGid {
|
||||||
|
if !c.User.HasPerm(dataprovider.PermChown) {
|
||||||
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
|
}
|
||||||
|
uid := int(request.Attributes().UID)
|
||||||
|
gid := int(request.Attributes().GID)
|
||||||
|
if err := os.Chown(path, uid, gid); err != nil {
|
||||||
|
c.Log(logger.LevelWarn, logSender, "failed to chown path %#v, uid: %v, gid: %v, err: %v", path, uid, gid, err)
|
||||||
|
return getSFTPErrorFromOSError(err)
|
||||||
|
}
|
||||||
|
logger.CommandLog(chownLogSender, path, "", c.User.Username, "", c.ID, c.protocol, uid, gid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error {
|
func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error {
|
||||||
if !c.User.HasPerm(dataprovider.PermRename) {
|
if !c.User.HasPerm(dataprovider.PermRename) {
|
||||||
return sftp.ErrSSHFxPermissionDenied
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
}
|
}
|
||||||
if err := os.Rename(sourcePath, targetPath); err != nil {
|
if err := os.Rename(sourcePath, targetPath); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "failed to rename file, source: %#v target: %#v: %v", sourcePath, targetPath, err)
|
c.Log(logger.LevelWarn, logSender, "failed to rename file, source: %#v target: %#v: %v", sourcePath, targetPath, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, c.ID, c.protocol)
|
logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1)
|
||||||
go executeAction(operationRename, c.User.Username, sourcePath, targetPath)
|
go executeAction(operationRename, c.User.Username, sourcePath, targetPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -284,8 +309,8 @@ func (c Connection) handleSFTPRmdir(path string) error {
|
||||||
var fi os.FileInfo
|
var fi os.FileInfo
|
||||||
var err error
|
var err error
|
||||||
if fi, err = os.Lstat(path); err != nil {
|
if fi, err = os.Lstat(path); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "failed to remove a dir %#v: stat error: %v", path, err)
|
c.Log(logger.LevelWarn, logSender, "failed to remove a dir %#v: stat error: %v", path, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
if !fi.IsDir() || fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
if !fi.IsDir() || fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
c.Log(logger.LevelDebug, logSender, "cannot remove %#v is not a directory", path)
|
c.Log(logger.LevelDebug, logSender, "cannot remove %#v is not a directory", path)
|
||||||
|
@ -293,11 +318,11 @@ func (c Connection) handleSFTPRmdir(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.Remove(path); err != nil {
|
if err = os.Remove(path); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "failed to remove directory %#v: %v", path, err)
|
c.Log(logger.LevelWarn, logSender, "failed to remove directory %#v: %v", path, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.CommandLog(rmdirLogSender, path, "", c.User.Username, c.ID, c.protocol)
|
logger.CommandLog(rmdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1)
|
||||||
return sftp.ErrSSHFxOk
|
return sftp.ErrSSHFxOk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,10 +332,10 @@ func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) erro
|
||||||
}
|
}
|
||||||
if err := os.Symlink(sourcePath, targetPath); err != nil {
|
if err := os.Symlink(sourcePath, targetPath); err != nil {
|
||||||
c.Log(logger.LevelWarn, logSender, "failed to create symlink %#v -> %#v: %v", sourcePath, targetPath, err)
|
c.Log(logger.LevelWarn, logSender, "failed to create symlink %#v -> %#v: %v", sourcePath, targetPath, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.CommandLog(symlinkLogSender, sourcePath, targetPath, c.User.Username, c.ID, c.protocol)
|
logger.CommandLog(symlinkLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,12 +344,12 @@ func (c Connection) handleSFTPMkdir(path string) error {
|
||||||
return sftp.ErrSSHFxPermissionDenied
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
}
|
}
|
||||||
if err := os.Mkdir(path, 0777); err != nil {
|
if err := os.Mkdir(path, 0777); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "error creating missing dir: %#v error: %v", path, err)
|
c.Log(logger.LevelWarn, logSender, "error creating missing dir: %#v error: %v", path, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
utils.SetPathPermissions(path, c.User.GetUID(), c.User.GetGID())
|
utils.SetPathPermissions(path, c.User.GetUID(), c.User.GetGID())
|
||||||
|
|
||||||
logger.CommandLog(mkdirLogSender, path, "", c.User.Username, c.ID, c.protocol)
|
logger.CommandLog(mkdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,8 +362,8 @@ func (c Connection) handleSFTPRemove(path string) error {
|
||||||
var fi os.FileInfo
|
var fi os.FileInfo
|
||||||
var err error
|
var err error
|
||||||
if fi, err = os.Lstat(path); err != nil {
|
if fi, err = os.Lstat(path); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "failed to remove a file %#v: stat error: %v", path, err)
|
c.Log(logger.LevelWarn, logSender, "failed to remove a file %#v: stat error: %v", path, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
if fi.IsDir() && fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
if fi.IsDir() && fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||||
c.Log(logger.LevelDebug, logSender, "cannot remove %#v is not a file/symlink", path)
|
c.Log(logger.LevelDebug, logSender, "cannot remove %#v is not a file/symlink", path)
|
||||||
|
@ -346,11 +371,11 @@ func (c Connection) handleSFTPRemove(path string) error {
|
||||||
}
|
}
|
||||||
size = fi.Size()
|
size = fi.Size()
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "failed to remove a file/symlink %#v: %v", path, err)
|
c.Log(logger.LevelWarn, logSender, "failed to remove a file/symlink %#v: %v", path, err)
|
||||||
return sftp.ErrSSHFxFailure
|
return getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.CommandLog(removeLogSender, path, "", c.User.Username, c.ID, c.protocol)
|
logger.CommandLog(removeLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1)
|
||||||
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||||
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
|
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
|
||||||
}
|
}
|
||||||
|
@ -367,8 +392,8 @@ func (c Connection) handleSFTPUploadToNewFile(requestPath, filePath string) (io.
|
||||||
|
|
||||||
file, err := os.Create(filePath)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "error creating file %#v: %v", requestPath, err)
|
c.Log(logger.LevelWarn, logSender, "error creating file %#v: %v", requestPath, err)
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SetPathPermissions(filePath, c.User.GetUID(), c.User.GetGID())
|
utils.SetPathPermissions(filePath, c.User.GetUID(), c.User.GetGID())
|
||||||
|
@ -407,16 +432,16 @@ func (c Connection) handleSFTPUploadToExistingFile(pflags sftp.FileOpenFlags, re
|
||||||
if isAtomicUploadEnabled() {
|
if isAtomicUploadEnabled() {
|
||||||
err = os.Rename(requestPath, filePath)
|
err = os.Rename(requestPath, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "error renaming existing file for atomic upload, source: %#v, dest: %#v, err: %v",
|
c.Log(logger.LevelWarn, logSender, "error renaming existing file for atomic upload, source: %#v, dest: %#v, err: %v",
|
||||||
requestPath, filePath, err)
|
requestPath, filePath, err)
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we use 0666 so the umask is applied
|
// we use 0666 so the umask is applied
|
||||||
file, err := os.OpenFile(filePath, osFlags, 0666)
|
file, err := os.OpenFile(filePath, osFlags, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelError, logSender, "error opening existing file, flags: %v, source: %#v, err: %v", pflags, filePath, err)
|
c.Log(logger.LevelWarn, logSender, "error opening existing file, flags: %v, source: %#v, err: %v", pflags, filePath, err)
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, getSFTPErrorFromOSError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pflags.Append && osFlags&os.O_TRUNC == 0 {
|
if pflags.Append && osFlags&os.O_TRUNC == 0 {
|
||||||
|
@ -602,3 +627,14 @@ func getUploadTempFilePath(path string) string {
|
||||||
guid := xid.New().String()
|
guid := xid.New().String()
|
||||||
return filepath.Join(dir, ".sftpgo-upload."+guid+"."+filepath.Base(path))
|
return filepath.Join(dir, ".sftpgo-upload."+guid+"."+filepath.Base(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSFTPErrorFromOSError(err error) error {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return sftp.ErrSSHFxNoSuchFile
|
||||||
|
} else if os.IsPermission(err) {
|
||||||
|
return sftp.ErrSSHFxPermissionDenied
|
||||||
|
} else if err != nil {
|
||||||
|
return sftp.ErrSSHFxFailure
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -191,11 +191,39 @@ func TestSFTPCmdTargetPath(t *testing.T) {
|
||||||
User: u,
|
User: u,
|
||||||
}
|
}
|
||||||
_, err := connection.getSFTPCmdTargetPath("invalid_path")
|
_, err := connection.getSFTPCmdTargetPath("invalid_path")
|
||||||
if err != sftp.ErrSSHFxOpUnsupported {
|
if err != sftp.ErrSSHFxNoSuchFile {
|
||||||
t.Errorf("getSFTPCmdTargetPath must fal with the expected error: %v", err)
|
t.Errorf("getSFTPCmdTargetPath must fal with the expected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetSFTPErrorFromOSError(t *testing.T) {
|
||||||
|
err := os.ErrNotExist
|
||||||
|
err = getSFTPErrorFromOSError(err)
|
||||||
|
if err != sftp.ErrSSHFxNoSuchFile {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
err = os.ErrPermission
|
||||||
|
err = getSFTPErrorFromOSError(err)
|
||||||
|
if err != sftp.ErrSSHFxPermissionDenied {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
err = getSFTPErrorFromOSError(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetstatModeIgnore(t *testing.T) {
|
||||||
|
originalMode := setstatMode
|
||||||
|
setstatMode = 1
|
||||||
|
connection := Connection{}
|
||||||
|
err := connection.handleSFTPSetstat("invalid", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v setstat should be silently ignore in mode 1", err)
|
||||||
|
}
|
||||||
|
setstatMode = originalMode
|
||||||
|
}
|
||||||
|
|
||||||
func TestSFTPGetUsedQuota(t *testing.T) {
|
func TestSFTPGetUsedQuota(t *testing.T) {
|
||||||
u := dataprovider.User{}
|
u := dataprovider.User{}
|
||||||
u.HomeDir = "home_rel_path"
|
u.HomeDir = "home_rel_path"
|
||||||
|
|
|
@ -601,14 +601,14 @@ func (c *scpCommand) sendProtocolMessage(message string) error {
|
||||||
func (c *scpCommand) sendExitStatus(err error) {
|
func (c *scpCommand) sendExitStatus(err error) {
|
||||||
status := uint32(0)
|
status := uint32(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status = 1
|
status = uint32(1)
|
||||||
}
|
}
|
||||||
ex := exitStatusMsg{
|
exitStatus := sshSubsystemExitStatus{
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
c.connection.Log(logger.LevelDebug, logSenderSCP, "send exit status for command with args: %v user: %v err: %v",
|
c.connection.Log(logger.LevelDebug, logSenderSCP, "send exit status for command with args: %v user: %v err: %v",
|
||||||
c.args, c.connection.User.Username, err)
|
c.args, c.connection.User.Username, err)
|
||||||
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&ex))
|
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||||
c.connection.channel.Close()
|
c.connection.channel.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,6 @@ const defaultPrivateKeyName = "id_rsa"
|
||||||
|
|
||||||
var sftpExtensions = []string{"posix-rename@openssh.com"}
|
var sftpExtensions = []string{"posix-rename@openssh.com"}
|
||||||
|
|
||||||
type exitStatusMsg struct {
|
|
||||||
Status uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration for the SFTP server
|
// Configuration for the SFTP server
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
// Identification string used by the server
|
// Identification string used by the server
|
||||||
|
@ -84,6 +80,9 @@ type Configuration struct {
|
||||||
// LoginBannerFile the contents of the specified file, if any, are sent to
|
// LoginBannerFile the contents of the specified file, if any, are sent to
|
||||||
// the remote user before authentication is allowed.
|
// the remote user before authentication is allowed.
|
||||||
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
|
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
|
||||||
|
// SetstatMode 0 means "normal mode": requests for changing permissions and owner/group are executed.
|
||||||
|
// 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
|
||||||
|
SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key contains information about host keys
|
// Key contains information about host keys
|
||||||
|
@ -169,6 +168,7 @@ func (c Configuration) Initialize(configDir string) error {
|
||||||
|
|
||||||
actions = c.Actions
|
actions = c.Actions
|
||||||
uploadMode = c.UploadMode
|
uploadMode = c.UploadMode
|
||||||
|
setstatMode = c.SetstatMode
|
||||||
logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String())
|
logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String())
|
||||||
if c.IdleTimeout > 0 {
|
if c.IdleTimeout > 0 {
|
||||||
startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute)
|
startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute)
|
||||||
|
@ -333,12 +333,10 @@ func (c Configuration) handleSftpConnection(channel ssh.Channel, connection Conn
|
||||||
server := sftp.NewRequestServer(channel, handler)
|
server := sftp.NewRequestServer(channel, handler)
|
||||||
|
|
||||||
if err := server.Serve(); err == io.EOF {
|
if err := server.Serve(); err == io.EOF {
|
||||||
connection.Log(logger.LevelDebug, logSender, "connection closed, sending exit-status")
|
connection.Log(logger.LevelDebug, logSender, "connection closed, sending exit status")
|
||||||
ex := exitStatusMsg{
|
exitStatus := sshSubsystemExitStatus{Status: uint32(0)}
|
||||||
Status: 0,
|
_, err = channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||||
}
|
connection.Log(logger.LevelDebug, logSender, "sent exit status %+v error: %v", exitStatus, err)
|
||||||
_, err = channel.SendRequest("exit-status", false, ssh.Marshal(&ex))
|
|
||||||
connection.Log(logger.LevelDebug, logSender, "send exit status error: %v", err)
|
|
||||||
server.Close()
|
server.Close()
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
connection.Log(logger.LevelWarn, logSender, "connection closed with error: %v", err)
|
connection.Log(logger.LevelWarn, logSender, "connection closed with error: %v", err)
|
||||||
|
@ -357,13 +355,13 @@ func (c Configuration) createHandler(connection Connection) sftp.Handlers {
|
||||||
|
|
||||||
func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, error) {
|
func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, error) {
|
||||||
if !filepath.IsAbs(user.HomeDir) {
|
if !filepath.IsAbs(user.HomeDir) {
|
||||||
logger.Warn(logSender, "", "user %v has invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
|
logger.Warn(logSender, "", "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
|
||||||
user.Username, user.HomeDir)
|
user.Username, user.HomeDir)
|
||||||
return nil, fmt.Errorf("cannot login user with invalid home dir: %v", user.HomeDir)
|
return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(user.HomeDir); os.IsNotExist(err) {
|
if _, err := os.Stat(user.HomeDir); os.IsNotExist(err) {
|
||||||
err := os.MkdirAll(user.HomeDir, 0777)
|
err := os.MkdirAll(user.HomeDir, 0777)
|
||||||
logger.Debug(logSender, "", "home directory %#v for user %v does not exist, try to create, mkdir error: %v",
|
logger.Debug(logSender, "", "home directory %#v for user %#v does not exist, try to create, mkdir error: %v",
|
||||||
user.HomeDir, user.Username, err)
|
user.HomeDir, user.Username, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
utils.SetPathPermissions(user.HomeDir, user.GetUID(), user.GetGID())
|
utils.SetPathPermissions(user.HomeDir, user.GetUID(), user.GetGID())
|
||||||
|
@ -373,7 +371,7 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro
|
||||||
if user.MaxSessions > 0 {
|
if user.MaxSessions > 0 {
|
||||||
activeSessions := getActiveSessions(user.Username)
|
activeSessions := getActiveSessions(user.Username)
|
||||||
if activeSessions >= user.MaxSessions {
|
if activeSessions >= user.MaxSessions {
|
||||||
logger.Debug(logSender, "", "authentication refused for user: %v, too many open sessions: %v/%v", user.Username,
|
logger.Debug(logSender, "", "authentication refused for user: %#v, too many open sessions: %v/%v", user.Username,
|
||||||
activeSessions, user.MaxSessions)
|
activeSessions, user.MaxSessions)
|
||||||
return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
|
return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ const (
|
||||||
mkdirLogSender = "Mkdir"
|
mkdirLogSender = "Mkdir"
|
||||||
symlinkLogSender = "Symlink"
|
symlinkLogSender = "Symlink"
|
||||||
removeLogSender = "Remove"
|
removeLogSender = "Remove"
|
||||||
|
chownLogSender = "Chown"
|
||||||
|
chmodLogSender = "Chmod"
|
||||||
operationDownload = "download"
|
operationDownload = "download"
|
||||||
operationUpload = "upload"
|
operationUpload = "upload"
|
||||||
operationDelete = "delete"
|
operationDelete = "delete"
|
||||||
|
@ -54,6 +56,7 @@ var (
|
||||||
dataProvider dataprovider.Provider
|
dataProvider dataprovider.Provider
|
||||||
actions Actions
|
actions Actions
|
||||||
uploadMode int
|
uploadMode int
|
||||||
|
setstatMode int
|
||||||
)
|
)
|
||||||
|
|
||||||
type connectionTransfer struct {
|
type connectionTransfer struct {
|
||||||
|
@ -103,6 +106,10 @@ type ConnectionStatus struct {
|
||||||
Transfers []connectionTransfer `json:"active_transfers"`
|
Transfers []connectionTransfer `json:"active_transfers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sshSubsystemExitStatus struct {
|
||||||
|
Status uint32
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
openConnections = make(map[string]Connection)
|
openConnections = make(map[string]Connection)
|
||||||
idleConnectionTicker = time.NewTicker(5 * time.Minute)
|
idleConnectionTicker = time.NewTicker(5 * time.Minute)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -415,7 +416,7 @@ func TestRemove(t *testing.T) {
|
||||||
}
|
}
|
||||||
err = client.RemoveDirectory(path.Join("/test", testFileName))
|
err = client.RemoveDirectory(path.Join("/test", testFileName))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("remove directory as file must fail")
|
t.Errorf("remove a file with rmdir must fail")
|
||||||
}
|
}
|
||||||
err = client.Remove(path.Join("/test", testFileName))
|
err = client.Remove(path.Join("/test", testFileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -498,9 +499,6 @@ func TestLink(t *testing.T) {
|
||||||
func TestStat(t *testing.T) {
|
func TestStat(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to add user: %v", err)
|
|
||||||
}
|
|
||||||
client, err := getSftpClient(user, usePubKey)
|
client, err := getSftpClient(user, usePubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to create sftp client: %v", err)
|
t.Errorf("unable to create sftp client: %v", err)
|
||||||
|
@ -517,15 +515,16 @@ func TestStat(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
fi, err := client.Lstat(testFileName)
|
_, err := client.Lstat(testFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("stat error: %v", err)
|
t.Errorf("stat error: %v", err)
|
||||||
}
|
}
|
||||||
err = client.Chown(testFileName, 1000, 1000)
|
err = client.Chown(testFileName, os.Getuid(), os.Getgid())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("chown error: %v", err)
|
t.Errorf("chown error: %v", err)
|
||||||
}
|
}
|
||||||
err = client.Chmod(testFileName, 0600)
|
newPerm := os.FileMode(0600)
|
||||||
|
err = client.Chmod(testFileName, newPerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("chmod error: %v", err)
|
t.Errorf("chmod error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -533,8 +532,8 @@ func TestStat(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("stat error: %v", err)
|
t.Errorf("stat error: %v", err)
|
||||||
}
|
}
|
||||||
if fi.Mode().Perm() != newFi.Mode().Perm() {
|
if newPerm != newFi.Mode().Perm() {
|
||||||
t.Errorf("stat must remain unchanged")
|
t.Errorf("chown failed expected: %v, actual: %v", newPerm, newFi.Mode().Perm())
|
||||||
}
|
}
|
||||||
_, err = client.ReadLink(testFileName)
|
_, err = client.ReadLink(testFileName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -544,11 +543,21 @@ func TestStat(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
|
// l'errore viene riconvertito da sftp.ErrSSHFxNoSuchFile in os.ErrNotExist
|
||||||
|
err = client.Chmod(testFileName, newPerm)
|
||||||
|
if err != os.ErrNotExist {
|
||||||
|
t.Errorf("unexpected chmod error: %v expected: %v", err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
err = client.Chown(testFileName, os.Getuid(), os.Getgid())
|
||||||
|
if err != os.ErrNotExist {
|
||||||
|
t.Errorf("unexpected chown error: %v expected: %v", err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
err = client.Chtimes(testFileName, time.Now(), time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("chtime must be silently ignored: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to remove user: %v", err)
|
|
||||||
}
|
|
||||||
os.RemoveAll(user.GetHomeDir())
|
os.RemoveAll(user.GetHomeDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1274,12 +1283,22 @@ func TestOpenError(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("upload must fail if we have no filesystem write permissions")
|
t.Errorf("upload must fail if we have no filesystem write permissions")
|
||||||
}
|
}
|
||||||
|
err = client.Mkdir("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error making dir: %v", err)
|
||||||
|
}
|
||||||
os.Chmod(user.GetHomeDir(), 0000)
|
os.Chmod(user.GetHomeDir(), 0000)
|
||||||
_, err = client.Lstat(testFileName)
|
_, err = client.Lstat(testFileName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("file stat must fail if we have no filesystem read permissions")
|
t.Errorf("file stat must fail if we have no filesystem read permissions")
|
||||||
}
|
}
|
||||||
os.Chmod(user.GetHomeDir(), 0755)
|
os.Chmod(user.GetHomeDir(), 0755)
|
||||||
|
os.Chmod(filepath.Join(user.GetHomeDir(), "test"), 0000)
|
||||||
|
err = client.Rename(testFileName, path.Join("test", testFileName))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), sftp.ErrSSHFxPermissionDenied.Error()) {
|
||||||
|
t.Errorf("unexpected error: %v expected: %v", err, sftp.ErrSSHFxPermissionDenied)
|
||||||
|
}
|
||||||
|
os.Chmod(filepath.Join(user.GetHomeDir(), "test"), 0755)
|
||||||
}
|
}
|
||||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1509,7 +1528,7 @@ func TestPermList(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1539,7 +1558,7 @@ func TestPermDownload(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1581,7 +1600,7 @@ func TestPermUpload(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1614,7 +1633,7 @@ func TestPermOverwrite(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1651,7 +1670,7 @@ func TestPermDelete(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1688,7 +1707,7 @@ func TestPermRename(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1729,7 +1748,7 @@ func TestPermCreateDirs(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1755,7 +1774,7 @@ func TestPermSymlink(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite}
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
@ -1792,6 +1811,89 @@ func TestPermSymlink(t *testing.T) {
|
||||||
os.RemoveAll(user.GetHomeDir())
|
os.RemoveAll(user.GetHomeDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPermChmod(t *testing.T) {
|
||||||
|
usePubKey := false
|
||||||
|
u := getTestUser(usePubKey)
|
||||||
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,
|
||||||
|
dataprovider.PermChown}
|
||||||
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
}
|
||||||
|
client, err := getSftpClient(user, usePubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create sftp client: %v", err)
|
||||||
|
} else {
|
||||||
|
defer client.Close()
|
||||||
|
testFileName := "test_file.dat"
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create test file: %v", err)
|
||||||
|
}
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file upload error: %v", err)
|
||||||
|
}
|
||||||
|
err = client.Chmod(testFileName, 0666)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("chmod without permission should not succeed")
|
||||||
|
}
|
||||||
|
err = client.Remove(testFileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to remove user: %v", err)
|
||||||
|
}
|
||||||
|
os.RemoveAll(user.GetHomeDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPermChown(t *testing.T) {
|
||||||
|
usePubKey := false
|
||||||
|
u := getTestUser(usePubKey)
|
||||||
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,
|
||||||
|
dataprovider.PermChmod}
|
||||||
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
}
|
||||||
|
client, err := getSftpClient(user, usePubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create sftp client: %v", err)
|
||||||
|
} else {
|
||||||
|
defer client.Close()
|
||||||
|
testFileName := "test_file.dat"
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create test file: %v", err)
|
||||||
|
}
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file upload error: %v", err)
|
||||||
|
}
|
||||||
|
err = client.Chown(testFileName, 1000, 1000)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("chown without permission should not succeed")
|
||||||
|
}
|
||||||
|
err = client.Remove(testFileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to remove user: %v", err)
|
||||||
|
}
|
||||||
|
os.RemoveAll(user.GetHomeDir())
|
||||||
|
}
|
||||||
func TestSSHConnection(t *testing.T) {
|
func TestSSHConnection(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
"kex_algorithms": [],
|
"kex_algorithms": [],
|
||||||
"ciphers": [],
|
"ciphers": [],
|
||||||
"macs": [],
|
"macs": [],
|
||||||
"login_banner_file": ""
|
"login_banner_file": "",
|
||||||
|
"setstat_mode": 0
|
||||||
},
|
},
|
||||||
"data_provider": {
|
"data_provider": {
|
||||||
"driver": "sqlite",
|
"driver": "sqlite",
|
||||||
|
|
Loading…
Reference in a new issue