mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 15:10:23 +00:00
parent
8e22dd1b13
commit
3d48fa7382
11 changed files with 766 additions and 42 deletions
23
README.md
23
README.md
|
@ -173,26 +173,9 @@ More in-depth analysis of performance can be found [here](./docs/performance.md)
|
|||
|
||||
## Acknowledgements
|
||||
|
||||
- [pkg/sftp](https://github.com/pkg/sftp)
|
||||
- [go-chi](https://github.com/go-chi/chi)
|
||||
- [zerolog](https://github.com/rs/zerolog)
|
||||
- [lumberjack](https://gopkg.in/natefinch/lumberjack.v2)
|
||||
- [argon2id](https://github.com/alexedwards/argon2id)
|
||||
- [go-sqlite3](https://github.com/mattn/go-sqlite3)
|
||||
- [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
|
||||
- [bbolt](https://github.com/etcd-io/bbolt)
|
||||
- [lib/pq](https://github.com/lib/pq)
|
||||
- [viper](https://github.com/spf13/viper)
|
||||
- [cobra](https://github.com/spf13/cobra)
|
||||
- [xid](https://github.com/rs/xid)
|
||||
- [nathanaelle/password](https://github.com/nathanaelle/password)
|
||||
- [PipeAt](https://github.com/eikenb/pipeat)
|
||||
- [ZeroConf](https://github.com/grandcat/zeroconf)
|
||||
- [SB Admin 2](https://github.com/BlackrockDigital/startbootstrap-sb-admin-2)
|
||||
- [shlex](https://github.com/google/shlex)
|
||||
- [go-proxyproto](https://github.com/pires/go-proxyproto)
|
||||
|
||||
Some code was initially taken from [Pterodactyl sftp server](https://github.com/pterodactyl/sftp-server)
|
||||
SFTPGo makes use of the third party libraries listed inside [go.mod](./go.mod).
|
||||
Some code was initially taken from [Pterodactyl SFTP Server](https://github.com/pterodactyl/sftp-server).
|
||||
We are very grateful to all the people who contributed with ideas and/or pull requests.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ FROM debian:latest
|
|||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
|
||||
# git and rsync are optional, uncomment the next line to add support for them if needed.
|
||||
#RUN apt-get update && apt-get install -y git rsync ca-certificates
|
||||
#RUN apt-get update && apt-get install -y git rsync
|
||||
|
||||
ARG BASE_DIR=/app
|
||||
ARG DATA_REL_DIR=data
|
||||
|
|
|
@ -12,7 +12,7 @@ If the `hook` defines a path to an external program, then this program is invoke
|
|||
- `action`, string, possible values are: `download`, `upload`, `pre-delete`,`delete`, `rename`, `ssh_cmd`
|
||||
- `username`
|
||||
- `path` is the full filesystem path, can be empty for some ssh commands
|
||||
- `target_path`, non-empty for `rename` action
|
||||
- `target_path`, non-empty for `rename` action and for `sftpgo-copy` SSH command
|
||||
- `ssh_cmd`, non-empty for `ssh_cmd` action
|
||||
|
||||
The external program can also read the following environment variables:
|
||||
|
|
|
@ -60,12 +60,7 @@ The configuration file contains the following sections:
|
|||
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
|
||||
- `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 disable login banner.
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored.
|
||||
- `enabled_ssh_commands`, list of enabled SSH commands. These SSH commands are enabled by default: `md5sum`, `sha1sum`, `cd`, `pwd`, `scp`. `*` enables all supported commands. Some commands are implemented directly inside SFTPGo, while for other commands we use system commands that need to be installed and in your system's `PATH`. For system commands we have no direct control on file creation/deletion and so we cannot support virtual folders, cloud storage filesystem, such as S3, and quota check is suboptimal: if quota is enabled, the number of files is checked at the command start and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one, and it is checked against the bytes transferred via SSH. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Anyway, we see the bytes that the remote command sends to the local command via SSH. These bytes contain both protocol commands and files, and so the size of the files is different from the size trasferred via SSH: for example, a command can send compressed files, or a protocol command (few bytes) could delete a big file. To mitigate this issue, quotas are recalculated at the command end with a full home directory scan. This could be heavy for big directories. If you need system commands and quotas you could consider disabling quota restrictions and periodically update quota usage yourself using the REST API. We support the following SSH commands:
|
||||
- `scp`, we have our own SCP implementation since we can't rely on `scp` system command to proper handle quotas, user's home dir restrictions, cloud storage providers and virtual folders. SCP between two remote hosts is supported using the `-3` scp option.
|
||||
- `md5sum`, `sha1sum`, `sha256sum`, `sha384sum`, `sha512sum`. Useful to check message digests for uploaded files. These commands are implemented inside SFTPGo so they work even if the matching system commands are not available, for example, on Windows.
|
||||
- `cd`, `pwd`. Some SFTP clients do not support the SFTP SSH_FXP_REALPATH packet type, so they use `cd` and `pwd` SSH commands to get the initial directory. Currently `cd` does nothing and `pwd` always returns the `/` path.
|
||||
- `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable support for Git repositories over SSH. They need to be installed and in your system's `PATH`. Git commands are not allowed inside virtual folders or inside directories with file extensions filters.
|
||||
- `rsync`. The `rsync` command needs to be installed and in your system's `PATH`. We cannot avoid that rsync creates symlinks, so if the user has the permission to create symlinks, we add the option `--safe-links` to the received rsync command if it is not already set. This should prevent creating symlinks that point outside the home dir. If the user cannot create symlinks, we add the option `--munge-links` if it is not already set. This should make symlinks unusable (but manually recoverable). The `rsync` command interacts with the filesystem directly and it is not aware of virtual folders and file extensions filters, so it will be automatically disabled for users with these features enabled.
|
||||
- `enabled_ssh_commands`, list of enabled SSH commands. `*` enables all supported commands. More information can be found [here](./ssh-commands.md).
|
||||
- `keyboard_interactive_auth_program`, string. Deprecated, please use `keyboard_interactive_auth_hook`.
|
||||
- `keyboard_interactive_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for keyboard interactive authentication. See the "Keyboard Interactive Authentication" paragraph for more details.
|
||||
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
|
||||
|
|
21
docs/ssh-commands.md
Normal file
21
docs/ssh-commands.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# SSH commands
|
||||
|
||||
Some SSH commands are implemented directly inside SFTPGo, while for other commands we use system commands that need to be installed and in your system's `PATH`. For system commands we have no direct control on file creation/deletion and so we cannot support virtual folders, cloud storage filesystem, such as S3, and quota check is suboptimal. If quota is enabled, the number of files is checked at the command start and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one, and it is checked against the bytes transferred via SSH. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Anyway, we see the bytes that the remote command sends to the local command via SSH. These bytes contain both protocol commands and files, and so the size of the files is different from the size trasferred via SSH: for example, a command can send compressed files, or a protocol command (few bytes) could delete a big file. To mitigate this issue, quotas are recalculated at the command end with a full home directory scan. This could be heavy for big directories. If you need system commands and quotas you could consider disabling quota restrictions and periodically update quota usage yourself using the REST API.
|
||||
|
||||
We support the following SSH commands:
|
||||
|
||||
- `scp`, we have our own SCP implementation since we can't rely on `scp` system command to proper handle quotas, user's home dir restrictions, cloud storage providers and virtual folders. SCP between two remote hosts is supported using the `-3` scp option.
|
||||
- `md5sum`, `sha1sum`, `sha256sum`, `sha384sum`, `sha512sum`. Useful to check message digests for uploaded files. These commands are implemented inside SFTPGo so they work even if the matching system commands are not available, for example, on Windows.
|
||||
- `cd`, `pwd`. Some SFTP clients do not support the SFTP SSH_FXP_REALPATH packet type, so they use `cd` and `pwd` SSH commands to get the initial directory. Currently `cd` does nothing and `pwd` always returns the `/` path.
|
||||
- `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable support for Git repositories over SSH. They need to be installed and in your system's `PATH`. Git commands are not allowed inside virtual folders or inside directories with file extensions filters.
|
||||
- `rsync`. The `rsync` command needs to be installed and in your system's `PATH`. We cannot avoid that rsync creates symlinks, so if the user has the permission to create symlinks, we add the option `--safe-links` to the received rsync command if it is not already set. This should prevent creating symlinks that point outside the home dir. If the user cannot create symlinks, we add the option `--munge-links` if it is not already set. This should make symlinks unusable (but manually recoverable). The `rsync` command interacts with the filesystem directly and it is not aware of virtual folders and file extensions filters, so it will be automatically disabled for users with these features enabled.
|
||||
- `sftpgo-copy`. This is a builtin copy implementation. It allows server side copy for files and directories. The first argument is the source file/directory and the second one is the destination file/directory, for example `sftpgo-copy <src> <dst>`. The command will fail if the destination directory exists. Copy for directories spanning virtual folders is not supported.
|
||||
- `sftpgo-remove`. This is a builtin remove implementation. It allows to remove files and directory recursively. The first argument is the file/directory to remove, for example `sftpgo-remove <dst>`.
|
||||
|
||||
The following SSH commands are enabled by default:
|
||||
|
||||
- `md5sum`
|
||||
- `sha1sum`
|
||||
- `cd`
|
||||
- `pwd`
|
||||
- `scp`
|
1
go.mod
1
go.mod
|
@ -19,6 +19,7 @@ require (
|
|||
github.com/miekg/dns v1.1.29 // indirect
|
||||
github.com/mitchellh/mapstructure v1.2.2 // indirect
|
||||
github.com/nathanaelle/password/v2 v2.0.1
|
||||
github.com/otiai10/copy v1.2.0
|
||||
github.com/pelletier/go-toml v1.7.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.1.3
|
||||
github.com/pkg/sftp v1.11.1-0.20200310224833-18dc4db7a456
|
||||
|
|
6
go.sum
6
go.sum
|
@ -186,6 +186,12 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
|||
github.com/nathanaelle/password/v2 v2.0.1 h1:ItoCTdsuIWzilYmllQPa3DR3YoCXcpfxScWLqr8Ii2s=
|
||||
github.com/nathanaelle/password/v2 v2.0.1/go.mod h1:eaoT+ICQEPNtikBRIAatN8ThWwMhVG+r1jTw60BvPJk=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
|
|
|
@ -712,7 +712,7 @@ func TestSSHCommandErrors(t *testing.T) {
|
|||
err = cmd.handle()
|
||||
assert.Error(t, err, "ssh command must fail, we are requesting an invalid path")
|
||||
|
||||
cmd.connection.User.HomeDir = os.TempDir()
|
||||
cmd.connection.User.HomeDir = filepath.Clean(os.TempDir())
|
||||
cmd.connection.User.QuotaFiles = 1
|
||||
cmd.connection.User.UsedQuotaFiles = 2
|
||||
fs, err = cmd.connection.User.GetFilesystem("123")
|
||||
|
@ -755,6 +755,54 @@ func TestSSHCommandErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = cmd.executeSystemCommand(command)
|
||||
assert.Error(t, err, "command must fail, pipe was already assigned")
|
||||
|
||||
cmd = sshCommand{
|
||||
command: "sftpgo-remove",
|
||||
connection: connection,
|
||||
args: []string{"/../../src"},
|
||||
}
|
||||
err = cmd.handle()
|
||||
assert.Error(t, err, "ssh command must fail, we are requesting an invalid path")
|
||||
|
||||
cmd = sshCommand{
|
||||
command: "sftpgo-copy",
|
||||
connection: connection,
|
||||
args: []string{"/../../test_src", "."},
|
||||
}
|
||||
err = cmd.handle()
|
||||
assert.Error(t, err, "ssh command must fail, we are requesting an invalid path")
|
||||
cmd.connection.fs = fs
|
||||
_, _, err = cmd.resolveCopyPaths(".", "../adir")
|
||||
assert.Error(t, err)
|
||||
cmd = sshCommand{
|
||||
command: "sftpgo-copy",
|
||||
connection: connection,
|
||||
args: []string{"src", "dst"},
|
||||
}
|
||||
cmd.connection.User.Permissions = make(map[string][]string)
|
||||
cmd.connection.User.Permissions["/"] = []string{dataprovider.PermDownload}
|
||||
_, _, err = cmd.getCopyPaths()
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, errPermissionDenied.Error())
|
||||
}
|
||||
cmd.connection.User.Permissions = make(map[string][]string)
|
||||
cmd.connection.User.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
if runtime.GOOS != osWindows {
|
||||
aDir := filepath.Join(os.TempDir(), "adir")
|
||||
err = os.MkdirAll(aDir, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
tmpFile := filepath.Join(aDir, "testcopy")
|
||||
err = ioutil.WriteFile(tmpFile, []byte("aaa"), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(aDir, 0001)
|
||||
assert.NoError(t, err)
|
||||
err = cmd.checkCopyDestination(tmpFile)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(aDir, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(tmpFile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandsWithExtensionsFilter(t *testing.T) {
|
||||
|
@ -875,6 +923,20 @@ func TestSSHCommandsRemoteFs(t *testing.T) {
|
|||
|
||||
err = cmd.executeSystemCommand(command)
|
||||
assert.Error(t, err, "command must fail for a non local filesystem")
|
||||
cmd = sshCommand{
|
||||
command: "sftpgo-copy",
|
||||
connection: connection,
|
||||
args: []string{},
|
||||
}
|
||||
err = cmd.handeSFTPGoCopy()
|
||||
assert.Error(t, err)
|
||||
cmd = sshCommand{
|
||||
command: "sftpgo-remove",
|
||||
connection: connection,
|
||||
args: []string{},
|
||||
}
|
||||
err = cmd.handeSFTPGoRemove()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSSHCommandQuotaScan(t *testing.T) {
|
||||
|
|
|
@ -70,7 +70,7 @@ var (
|
|||
uploadMode int
|
||||
setstatMode int
|
||||
supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd",
|
||||
"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
|
||||
"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync", "sftpgo-copy", "sftpgo-remove"}
|
||||
defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd", "scp"}
|
||||
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
|
||||
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
|
||||
|
|
|
@ -3625,6 +3625,8 @@ func TestOverlappedMappedFolders(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", path.Join(vdirPath1, subDir)), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
dataProvider = dataprovider.GetProvider()
|
||||
|
@ -3657,10 +3659,87 @@ func TestOverlappedMappedFolders(t *testing.T) {
|
|||
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
err = os.RemoveAll(mappedPath1)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestResolveOverlappedMappedPaths(t *testing.T) {
|
||||
u := getTestUser(false)
|
||||
mappedPath1 := filepath.Join(os.TempDir(), "mapped1", "subdir")
|
||||
vdirPath1 := "/vdir1/subdir"
|
||||
mappedPath2 := filepath.Join(os.TempDir(), "mapped2")
|
||||
vdirPath2 := "/vdir2/subdir"
|
||||
mappedPath3 := filepath.Join(os.TempDir(), "mapped1")
|
||||
vdirPath3 := "/vdir3"
|
||||
mappedPath4 := filepath.Join(os.TempDir(), "mapped1", "subdir", "vdir4")
|
||||
vdirPath4 := "/vdir4"
|
||||
u.VirtualFolders = []vfs.VirtualFolder{
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath1,
|
||||
},
|
||||
VirtualPath: vdirPath1,
|
||||
},
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath2,
|
||||
},
|
||||
VirtualPath: vdirPath2,
|
||||
},
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath3,
|
||||
},
|
||||
VirtualPath: vdirPath3,
|
||||
},
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath4,
|
||||
},
|
||||
VirtualPath: vdirPath4,
|
||||
},
|
||||
}
|
||||
err := os.MkdirAll(u.GetHomeDir(), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath1, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath2, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath3, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath4, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fs := vfs.NewOsFs("", u.GetHomeDir(), u.VirtualFolders)
|
||||
p, err := fs.ResolvePath("/vdir1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(u.GetHomeDir(), "vdir1"), p)
|
||||
p, err = fs.ResolvePath("/vdir1/subdir")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, mappedPath1, p)
|
||||
p, err = fs.ResolvePath("/vdir3/subdir/vdir4/file.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(mappedPath4, "file.txt"), p)
|
||||
p, err = fs.ResolvePath("/vdir4/file.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(mappedPath4, "file.txt"), p)
|
||||
assert.Equal(t, filepath.Join(mappedPath3, "subdir", "vdir4", "file.txt"), p)
|
||||
assert.Equal(t, filepath.Join(mappedPath1, "vdir4", "file.txt"), p)
|
||||
p, err = fs.ResolvePath("/vdir3/subdir/vdir4/../file.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(mappedPath3, "subdir", "file.txt"), p)
|
||||
assert.Equal(t, filepath.Join(mappedPath1, "file.txt"), p)
|
||||
|
||||
err = os.RemoveAll(u.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath4)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath1)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath3)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -5148,6 +5227,367 @@ func TestSSHFileHash(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSSHCopy(t *testing.T) {
|
||||
usePubKey := true
|
||||
u := getTestUser(usePubKey)
|
||||
u.QuotaFiles = 100
|
||||
mappedPath1 := filepath.Join(os.TempDir(), "vdir1")
|
||||
vdirPath1 := "/vdir1/subdir"
|
||||
mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
|
||||
vdirPath2 := "/vdir2/subdir"
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath1,
|
||||
},
|
||||
VirtualPath: vdirPath1,
|
||||
QuotaFiles: -1,
|
||||
QuotaSize: -1,
|
||||
})
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath2,
|
||||
},
|
||||
VirtualPath: vdirPath2,
|
||||
QuotaFiles: 100,
|
||||
QuotaSize: 0,
|
||||
})
|
||||
u.Filters.FileExtensions = []dataprovider.ExtensionsFilter{
|
||||
{
|
||||
Path: "/",
|
||||
DeniedExtensions: []string{".denied"},
|
||||
},
|
||||
}
|
||||
err := os.MkdirAll(mappedPath1, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath2, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
testDir := "adir"
|
||||
testDir1 := "adir1"
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
testFileName := "test_file.dat"
|
||||
testFileSize := int64(131072)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileName1 := "test_file1.dat"
|
||||
testFileSize1 := int64(65537)
|
||||
testFilePath1 := filepath.Join(homeBasePath, testFileName1)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = createTestFile(testFilePath1, testFileSize1)
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir(path.Join(vdirPath1, testDir1))
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir(path.Join(vdirPath2, testDir1))
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(testDir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, path.Join(testDir, testFileName1), testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath1, testDir1, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testDir1, testFileName1), testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath2, testDir1, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testDir1, testFileName1), testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Symlink(path.Join(testDir, testFileName), testFileName)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 2*testFileSize+2*testFileSize1, user.UsedQuotaSize)
|
||||
folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
}
|
||||
folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 2, f.UsedQuotaFiles)
|
||||
}
|
||||
|
||||
_, err = client.Stat(testDir1)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v", path.Join(vdirPath1, testDir1)), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-copy", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", testFileName, testFileName+".linkcopy"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
out, err := runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1), "."), user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
fi, err := client.Stat(testDir1)
|
||||
if assert.NoError(t, err) {
|
||||
assert.True(t, fi.IsDir())
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 3*testFileSize+3*testFileSize1, user.UsedQuotaSize)
|
||||
}
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", "missing\\ dir", "."), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1), "."), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath2, testDir1, testFileName), testFileName+".copy"),
|
||||
user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
fi, err := client.Stat(testFileName + ".copy")
|
||||
if assert.NoError(t, err) {
|
||||
assert.True(t, fi.Mode().IsRegular())
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 7, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize)
|
||||
}
|
||||
out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1), path.Join(vdirPath2, testDir1+"copy")),
|
||||
user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
fi, err := client.Stat(path.Join(vdirPath2, testDir1+"copy"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.True(t, fi.IsDir())
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 7, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize)
|
||||
folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
assert.Equal(t, testFileSize*2+testFileSize1*2, f.UsedQuotaSize)
|
||||
assert.Equal(t, 4, f.UsedQuotaFiles)
|
||||
}
|
||||
}
|
||||
out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1), path.Join(vdirPath1, testDir1+"copy")),
|
||||
user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
_, err := client.Stat(path.Join(vdirPath2, testDir1+"copy"))
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 5*testFileSize+4*testFileSize1, user.UsedQuotaSize)
|
||||
folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
assert.Equal(t, 2*testFileSize+2*testFileSize1, f.UsedQuotaSize)
|
||||
assert.Equal(t, 4, f.UsedQuotaFiles)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath2, ".."), "newdir"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(testDir, testFileName), testFileName+".denied"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
if runtime.GOOS != osWindows {
|
||||
err = os.Chmod(filepath.Join(mappedPath1, testDir1), 0001)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1), "newdir"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(filepath.Join(mappedPath1, testDir1), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir1), 0555)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1, testFileName),
|
||||
path.Join(testDir1, "anewdir")), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir1), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.RemoveAll(filepath.Join(user.GetHomeDir(), "vdir1"))
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(user.GetHomeDir(), 0555)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath2), "/vdir1"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(user.GetHomeDir(), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath1)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// test copy dir with no create dirs perm
|
||||
user.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client, err = getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %v %v", path.Join(vdirPath1, testDir1), testDir1+"copy1"),
|
||||
user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath1)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSSHRemove(t *testing.T) {
|
||||
usePubKey := false
|
||||
u := getTestUser(usePubKey)
|
||||
u.QuotaFiles = 100
|
||||
mappedPath1 := filepath.Join(os.TempDir(), "vdir1")
|
||||
vdirPath1 := "/vdir1/sub"
|
||||
mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
|
||||
vdirPath2 := "/vdir2/sub"
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath1,
|
||||
},
|
||||
VirtualPath: vdirPath1,
|
||||
QuotaFiles: -1,
|
||||
QuotaSize: -1,
|
||||
})
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath2,
|
||||
},
|
||||
VirtualPath: vdirPath2,
|
||||
QuotaFiles: 100,
|
||||
QuotaSize: 0,
|
||||
})
|
||||
err := os.MkdirAll(mappedPath1, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath2, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
testFileName := "test_file.dat"
|
||||
testFileSize := int64(131072)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileName1 := "test_file1.dat"
|
||||
testFileSize1 := int64(65537)
|
||||
testFilePath1 := filepath.Join(homeBasePath, testFileName1)
|
||||
testDir := "testdir"
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = createTestFile(testFilePath1, testFileSize1)
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir(path.Join(vdirPath1, testDir))
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir(path.Join(vdirPath2, testDir))
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath1, testDir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testDir, testFileName1), testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath2, testDir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testDir, testFileName1), testFileSize1, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Symlink(testFileName, testFileName+".link")
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", testFileName+".link"), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-remove /vdir1", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-remove /", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-remove", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
out, err := runSSHCommand(fmt.Sprintf("sftpgo-remove %v", testFileName), user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
_, err := client.Stat(testFileName)
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, user.UsedQuotaFiles)
|
||||
assert.Equal(t, testFileSize+2*testFileSize1, user.UsedQuotaSize)
|
||||
}
|
||||
out, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", path.Join(vdirPath1, testDir)), user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "OK\n", string(out))
|
||||
_, err := client.Stat(path.Join(vdirPath1, testFileName))
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, testFileSize1, user.UsedQuotaSize)
|
||||
}
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", vdirPath1), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-remove /", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
_, err = runSSHCommand("sftpgo-remove missing_file", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
if runtime.GOOS != osWindows {
|
||||
err = os.Chmod(filepath.Join(mappedPath2, testDir), 0555)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", path.Join(vdirPath2, testDir)), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(filepath.Join(mappedPath2, testDir), 0001)
|
||||
assert.NoError(t, err)
|
||||
_, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", path.Join(vdirPath2, testDir)), user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
err = os.Chmod(filepath.Join(mappedPath2, testDir), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// test remove dir with no delete perm
|
||||
user.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client, err = getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
_, err = runSSHCommand("sftpgo-remove adir", user, usePubKey)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath1)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBasicGitCommands(t *testing.T) {
|
||||
if len(gitPath) == 0 || len(sshPath) == 0 || runtime.GOOS == osWindows {
|
||||
t.Skip("git and/or ssh command not found or OS is windows, unable to execute this test")
|
||||
|
|
238
sftpd/ssh_cmd.go
238
sftpd/ssh_cmd.go
|
@ -11,11 +11,13 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/shlex"
|
||||
fscopy "github.com/otiai10/copy"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
|
@ -100,10 +102,149 @@ func (c *sshCommand) handle() error {
|
|||
// hard coded response to "/"
|
||||
c.connection.channel.Write([]byte("/\n")) //nolint:errcheck
|
||||
c.sendExitStatus(nil)
|
||||
} else if c.command == "sftpgo-copy" {
|
||||
return c.handeSFTPGoCopy()
|
||||
} else if c.command == "sftpgo-remove" {
|
||||
return c.handeSFTPGoRemove()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) handeSFTPGoCopy() error {
|
||||
if !vfs.IsLocalOsFs(c.connection.fs) {
|
||||
return c.sendErrorResponse(errUnsupportedConfig)
|
||||
}
|
||||
sshSourcePath, sshDestPath, err := c.getCopyPaths()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
fsSourcePath, fsDestPath, err := c.resolveCopyPaths(sshSourcePath, sshDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if err := c.checkCopyDestination(fsDestPath); err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "requested copy %#v -> %#v sftp paths %#v -> %#v",
|
||||
fsSourcePath, fsDestPath, sshSourcePath, sshDestPath)
|
||||
|
||||
fi, err := c.connection.fs.Lstat(fsSourcePath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
filesNum := 0
|
||||
filesSize := int64(0)
|
||||
if fi.IsDir() {
|
||||
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(sshDestPath)) {
|
||||
return c.sendErrorResponse(errPermissionDenied)
|
||||
}
|
||||
filesNum, filesSize, err = c.connection.fs.GetDirSize(fsSourcePath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if c.connection.User.HasVirtualFoldersInside(sshSourcePath) {
|
||||
err := errors.New("unsupported copy source: the source directory contains virtual folders")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if c.connection.User.HasVirtualFoldersInside(sshDestPath) {
|
||||
err := errors.New("unsupported copy source: the destination directory contains virtual folders")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
} else if fi.Mode().IsRegular() {
|
||||
if !c.connection.User.IsFileAllowed(sshDestPath) {
|
||||
err := errors.New("unsupported copy destination: this file is not allowed")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
filesNum = 1
|
||||
filesSize = fi.Size()
|
||||
} else {
|
||||
err := errors.New("unsupported copy source: only files and directories are supported")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "start copy %#v -> %#v", fsSourcePath, fsDestPath)
|
||||
err = fscopy.Copy(fsSourcePath, fsDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
c.updateQuota(sshDestPath, filesNum, filesSize)
|
||||
c.connection.channel.Write([]byte("OK\n")) //nolint:errcheck
|
||||
c.sendExitStatus(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) handeSFTPGoRemove() error {
|
||||
if !vfs.IsLocalOsFs(c.connection.fs) {
|
||||
return c.sendErrorResponse(errUnsupportedConfig)
|
||||
}
|
||||
sshDestPath, err := c.getRemovePath()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if !c.connection.User.HasPerm(dataprovider.PermDelete, path.Dir(sshDestPath)) {
|
||||
return c.sendErrorResponse(errPermissionDenied)
|
||||
}
|
||||
fsDestPath, err := c.connection.fs.ResolvePath(sshDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
fi, err := c.connection.fs.Lstat(fsDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
filesNum := 0
|
||||
filesSize := int64(0)
|
||||
if fi.IsDir() {
|
||||
filesNum, filesSize, err = c.connection.fs.GetDirSize(fsDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if sshDestPath == "/" {
|
||||
err := errors.New("removing root dir is not allowed")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if c.connection.User.HasVirtualFoldersInside(sshDestPath) {
|
||||
err := errors.New("unsupported remove source: this directory contains virtual folders")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if c.connection.User.IsVirtualFolder(sshDestPath) {
|
||||
err := errors.New("unsupported remove source: this directory is a virtual folder")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
if c.connection.User.IsMappedPath(fsDestPath) {
|
||||
err := errors.New("removing a directory mapped as virtual folder is not allowed")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
} else if fi.Mode().IsRegular() {
|
||||
filesNum = 1
|
||||
filesSize = fi.Size()
|
||||
} else {
|
||||
err := errors.New("unsupported remove source: only files and directories are supported")
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(fsDestPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
c.updateQuota(sshDestPath, -filesNum, -filesSize)
|
||||
c.connection.channel.Write([]byte("OK\n")) //nolint:errcheck
|
||||
c.sendExitStatus(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) updateQuota(sshDestPath string, filesNum int, filesSize int64) {
|
||||
vfolder, err := c.connection.User.GetVirtualFolderForPath(sshDestPath)
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(dataProvider, vfolder.BaseVirtualFolder, filesNum, filesSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(dataProvider, c.connection.User, filesNum, filesSize, false) //nolint:errcheck
|
||||
}
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(dataProvider, c.connection.User, filesNum, filesSize, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sshCommand) handleHashCommands() error {
|
||||
if !vfs.IsLocalOsFs(c.connection.fs) {
|
||||
return c.sendErrorResponse(errUnsupportedConfig)
|
||||
|
@ -412,15 +553,27 @@ func (c *sshCommand) rescanHomeDir() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// for the supported command, the path, if any, is the last argument
|
||||
// for the supported commands, the destination path, if any, is the last argument
|
||||
func (c *sshCommand) getDestPath() string {
|
||||
if len(c.args) == 0 {
|
||||
return ""
|
||||
}
|
||||
destPath := strings.Trim(c.args[len(c.args)-1], "'")
|
||||
destPath = strings.Trim(destPath, "\"")
|
||||
result := utils.CleanSFTPPath(destPath)
|
||||
if strings.HasSuffix(destPath, "/") && !strings.HasSuffix(result, "/") {
|
||||
return cleanCommandPath(c.args[len(c.args)-1])
|
||||
}
|
||||
|
||||
// for the supported commands, the destination path, if any, is the second-last argument
|
||||
func (c *sshCommand) getSourcePath() string {
|
||||
if len(c.args) < 2 {
|
||||
return ""
|
||||
}
|
||||
return cleanCommandPath(c.args[len(c.args)-2])
|
||||
}
|
||||
|
||||
func cleanCommandPath(name string) string {
|
||||
name = strings.Trim(name, "'")
|
||||
name = strings.Trim(name, "\"")
|
||||
result := utils.CleanSFTPPath(name)
|
||||
if strings.HasSuffix(name, "/") && !strings.HasSuffix(result, "/") {
|
||||
result += "/"
|
||||
}
|
||||
return result
|
||||
|
@ -437,6 +590,58 @@ func (c *sshCommand) getMappedError(err error) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (c *sshCommand) getCopyPaths() (string, string, error) {
|
||||
sshSourcePath := strings.TrimSuffix(c.getSourcePath(), "/")
|
||||
sshDestPath := c.getDestPath()
|
||||
if strings.HasSuffix(sshDestPath, "/") {
|
||||
sshDestPath = path.Join(sshDestPath, path.Base(sshSourcePath))
|
||||
}
|
||||
if len(sshSourcePath) == 0 || len(sshDestPath) == 0 || len(c.args) != 2 {
|
||||
err := errors.New("usage sftpgo-copy <source dir path> <destination dir path>")
|
||||
return "", "", err
|
||||
}
|
||||
if !c.connection.User.HasPerm(dataprovider.PermListItems, path.Dir(sshSourcePath)) ||
|
||||
!c.connection.User.HasPerm(dataprovider.PermUpload, path.Dir(sshDestPath)) {
|
||||
return "", "", errPermissionDenied
|
||||
}
|
||||
return sshSourcePath, sshDestPath, nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) getRemovePath() (string, error) {
|
||||
sshDestPath := c.getDestPath()
|
||||
if len(sshDestPath) == 0 || len(c.args) != 1 {
|
||||
err := errors.New("usage sftpgo-remove <destination path>")
|
||||
return "", err
|
||||
}
|
||||
if len(sshDestPath) > 1 {
|
||||
sshDestPath = strings.TrimSuffix(sshDestPath, "/")
|
||||
}
|
||||
return sshDestPath, nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) resolveCopyPaths(sshSourcePath, sshDestPath string) (string, string, error) {
|
||||
fsSourcePath, err := c.connection.fs.ResolvePath(sshSourcePath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
fsDestPath, err := c.connection.fs.ResolvePath(sshDestPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return fsSourcePath, fsDestPath, nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) checkCopyDestination(fsDestPath string) error {
|
||||
_, err := c.connection.fs.Lstat(fsDestPath)
|
||||
if err == nil {
|
||||
err := errors.New("invalid copy destination: cannot overwrite an existing file or directory")
|
||||
return err
|
||||
} else if !c.connection.fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) sendErrorResponse(err error) error {
|
||||
errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), c.getMappedError(err))
|
||||
c.connection.channel.Write([]byte(errorString)) //nolint:errcheck
|
||||
|
@ -446,12 +651,18 @@ func (c *sshCommand) sendErrorResponse(err error) error {
|
|||
|
||||
func (c *sshCommand) sendExitStatus(err error) {
|
||||
status := uint32(0)
|
||||
cmdPath := c.getDestPath()
|
||||
targetPath := ""
|
||||
if c.command == "sftpgo-copy" {
|
||||
targetPath = cmdPath
|
||||
cmdPath = c.getSourcePath()
|
||||
}
|
||||
if err != nil {
|
||||
status = uint32(1)
|
||||
c.connection.Log(logger.LevelWarn, logSenderSSH, "command failed: %#v args: %v user: %v err: %v",
|
||||
c.command, c.args, c.connection.User.Username, err)
|
||||
} else {
|
||||
logger.CommandLog(sshCommandLogSender, c.getDestPath(), "", c.connection.User.Username, "", c.connection.ID,
|
||||
logger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, "", c.connection.ID,
|
||||
protocolSSH, -1, -1, "", "", c.connection.command)
|
||||
}
|
||||
exitStatus := sshSubsystemExitStatus{
|
||||
|
@ -462,14 +673,19 @@ func (c *sshCommand) sendExitStatus(err error) {
|
|||
// for scp we notify single uploads/downloads
|
||||
if c.command != scpCmdName {
|
||||
metrics.SSHCommandCompleted(err)
|
||||
realPath := c.getDestPath()
|
||||
if len(realPath) > 0 {
|
||||
p, e := c.connection.fs.ResolvePath(realPath)
|
||||
if len(cmdPath) > 0 {
|
||||
p, e := c.connection.fs.ResolvePath(cmdPath)
|
||||
if e == nil {
|
||||
realPath = p
|
||||
cmdPath = p
|
||||
}
|
||||
}
|
||||
go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0, err)) //nolint:errcheck
|
||||
if len(targetPath) > 0 {
|
||||
p, e := c.connection.fs.ResolvePath(targetPath)
|
||||
if e == nil {
|
||||
targetPath = p
|
||||
}
|
||||
}
|
||||
go executeAction(newActionNotification(c.connection.User, operationSSHCmd, cmdPath, targetPath, c.command, 0, err)) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue