add support for Git over SSH
We use the system commands "git-receive-pack", "git-upload-pack" and "git-upload-archive". they need to be installed and in your system's PATH. Since we execute system commands we have no direct control on file creation/deletion and so quota check is suboptimal: if quota is enabled, the number of files is checked at the command begin and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Quotas are recalculated at the command end with a full home directory scan, this could be heavy for big directories.
This commit is contained in:
parent
7a8b1645ef
commit
0a025aabfd
16 changed files with 846 additions and 88 deletions
20
README.md
20
README.md
|
@ -14,10 +14,11 @@ Full featured and highly configurable SFTP server
|
|||
- Per user maximum concurrent sessions.
|
||||
- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode, changing access and modification times can be enabled or disabled.
|
||||
- Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only).
|
||||
- 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 file upload, download, delete, rename, on SSH commands and on user add, update and delete.
|
||||
- Automatically terminating idle connections.
|
||||
- Atomic uploads are configurable.
|
||||
- Optional SCP support.
|
||||
- Support for Git repository over SSH.
|
||||
- SCP is supported.
|
||||
- Prometheus metrics are exposed.
|
||||
- REST API for users and quota management and real time reports for the active connections with possibility of forcibly closing a connection.
|
||||
- Web based interface to easily manage users and connections.
|
||||
|
@ -132,17 +133,19 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default "SFTPGo_version"
|
||||
- `upload_mode` integer. 0 means standard, the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode if there is an upload error the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: as atomic but if there is an upload error the temporary file is renamed to the requested path and not deleted, this way a client can reconnect and resume the upload.
|
||||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions
|
||||
- `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`. Actions will be not executed if an error is detected and so a partial file is uploaded or downloaded. The `upload` condition includes both uploads to new files and overwrite of existing files. Leave empty to disable actions.
|
||||
- `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`, `ssh_cmd`. Actions will not be executed if an error is detected and so a partial file is uploaded or downloaded or an SSH command is not successfully completed. The `upload` condition includes both uploads to new files and overwrite of existing files. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`. Leave empty to disable actions.
|
||||
- `command`, string. Absolute path to the command to execute. Leave empty to disable. The command is invoked with the following arguments:
|
||||
- `action`, any valid `execute_on` string
|
||||
- `username`, user who did the action
|
||||
- `path` to the affected file. For `rename` action this is the old file name
|
||||
- `target_path`, non empty for `rename` action, this is the new file name
|
||||
- `ssh_cmd`, non empty for `ssh_cmd` action
|
||||
- `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable. The query string will contain the following parameters that have the same meaning of the command's arguments:
|
||||
- `action`
|
||||
- `username`
|
||||
- `path`
|
||||
- `target_path`, added for `rename` action only
|
||||
- `ssh_cmd`, added for `ssh_cmd` action only
|
||||
- `keys`, struct array. It contains the daemon's private keys. If empty or missing the daemon will search or try to generate `id_rsa` in the configuration directory.
|
||||
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
||||
- `enable_scp`, boolean. Default disabled. Set to `true` to enable the experimental SCP support. This setting is deprecated and will be removed in future versions, please add `scp` to the `enabled_ssh_commands` list to enable it
|
||||
|
@ -154,7 +157,8 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `enabled_ssh_commands`, list of enabled SSH commands. These SSH commands are enabled by default: `md5sum`, `sha1sum`, `cd`, `pwd`. `*` enables all supported commands. We support the following SSH commands:
|
||||
- `scp`, SCP is an experimental feature, we have our own SCP implementation since we can't rely on scp system command to proper handle permissions, quota and user's home dir restrictions. The SCP protocol is quite simple but there is no official docs about it, so we need more testing and feedbacks before enabling it by default. We may not handle some borderline cases or have sneaky bugs. Please do accurate tests yourself before enabling SCP and let us known if something does not work as expected for your use cases. 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 does not support the SFTP SSH_FXP_REALPATH and so they use `cd` and `pwd` SSH commands to get the initial directory. Currently `cd` do nothing and `pwd` always returns the `/` path.
|
||||
- `cd`, `pwd`. Some SFTP clients does not support the SFTP SSH_FXP_REALPATH packet type and so they use `cd` and `pwd` SSH commands to get the initial directory. Currently `cd` do nothing and `pwd` always returns the `/` path.
|
||||
- `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable `git` support, they need to be installed and in your system's `PATH`. Since we execute system commands we have no direct control on file creation/deletion and so quota check is suboptimal: if quota is enabled, the number of files is checked at the command begin and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Quotas are recalculated at the command end with a full home directory scan, this could be heavy for big directories.
|
||||
- **"data_provider"**, the configuration for the data provider
|
||||
- `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.
|
||||
|
@ -166,7 +170,7 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `connectionstring`, string. Provide a custom database connection string. If not empty this connection string will be used instead of build one using the previous parameters. Leave empty for drivers `bolt` and `memory`
|
||||
- `users_table`, string. Database table for SFTP users
|
||||
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable
|
||||
- `track_quota`, integer. Set the preferred way to track users quota between the following choices:
|
||||
- `track_quota`, integer. Set the preferred mode to track users quota between the following choices:
|
||||
- 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
|
||||
- 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
||||
- 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions
|
||||
|
@ -420,8 +424,10 @@ SFTPGo exposes [Prometheus](https://prometheus.io/) metrics at the `/metrics` HT
|
|||
Several counters and gauges are available, for example:
|
||||
|
||||
- Total uploads and downloads
|
||||
- Total uploads and downloads size
|
||||
- Total uploads and downloads errors
|
||||
- Total upload and download size
|
||||
- Total upload and download errors
|
||||
- Total executed SSH commands
|
||||
- Total SSH command errors
|
||||
- Number of active connections
|
||||
- Data provider availability
|
||||
- Total successful and failed logins using a password or a public key
|
||||
|
|
|
@ -174,6 +174,11 @@ func GetProvider() Provider {
|
|||
return provider
|
||||
}
|
||||
|
||||
// GetQuotaTracking returns the configured mode for user's quota tracking
|
||||
func GetQuotaTracking() int {
|
||||
return config.TrackQuota
|
||||
}
|
||||
|
||||
// Provider interface that data providers must implement.
|
||||
type Provider interface {
|
||||
validateUserAndPass(username string, password string) (User, error)
|
||||
|
|
|
@ -91,6 +91,19 @@ func (u *User) HasPerm(permission string) bool {
|
|||
return utils.IsStringInSlice(permission, u.Permissions)
|
||||
}
|
||||
|
||||
// HasPerms return true if the user has all the given permissions
|
||||
func (u *User) HasPerms(permissions []string) bool {
|
||||
if utils.IsStringInSlice(PermAny, u.Permissions) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if !utils.IsStringInSlice(permission, u.Permissions) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPermissionsAsJSON returns the permissions as json byte array
|
||||
func (u *User) GetPermissionsAsJSON() ([]byte, error) {
|
||||
return json.Marshal(u.Permissions)
|
||||
|
|
|
@ -31,10 +31,10 @@ func startQuotaScan(w http.ResponseWriter, r *http.Request) {
|
|||
go func() {
|
||||
numFiles, size, _, err := utils.ScanDirContents(user.HomeDir)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error scanning user home dir %v: %v", user.HomeDir, err)
|
||||
logger.Warn(logSender, "", "error scanning user home dir %#v: %v", user.HomeDir, err)
|
||||
} else {
|
||||
err := dataprovider.UpdateUserQuota(dataProvider, user, numFiles, size, true)
|
||||
logger.Debug(logSender, "", "user dir scanned, user: %v, dir: %v, error: %v", user.Username, user.HomeDir, err)
|
||||
logger.Debug(logSender, "", "user home dir scanned, user: %#v, dir: %#v, error: %v", user.Username, user.HomeDir, err)
|
||||
}
|
||||
sftpd.RemoveQuotaScan(user.Username)
|
||||
}()
|
||||
|
|
|
@ -787,6 +787,7 @@ func TestStartQuotaScanMock(t *testing.T) {
|
|||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
func TestStartQuotaScanBadUserMock(t *testing.T) {
|
||||
|
|
|
@ -55,6 +55,18 @@ var (
|
|||
Help: "The total download size as bytes",
|
||||
})
|
||||
|
||||
// totalSSHCommands is the metric that reports the total number of executed SSH commands
|
||||
totalSSHCommands = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_ssh_commands_total",
|
||||
Help: "The total number of executed SSH commands",
|
||||
})
|
||||
|
||||
// totalSSHCommandErrors is the metric that reports the total number of SSH command errors
|
||||
totalSSHCommandErrors = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_ssh_command_errors_total",
|
||||
Help: "The total number of SSH command errors",
|
||||
})
|
||||
|
||||
// totalLoginAttempts is the metric that reports the total number of login attempts
|
||||
totalLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_login_attempts_total",
|
||||
|
@ -157,6 +169,15 @@ func TransferCompleted(bytesSent, bytesReceived int64, transferKind int, err err
|
|||
}
|
||||
}
|
||||
|
||||
// SSHCommandCompleted update metrics after an SSH command terminates
|
||||
func SSHCommandCompleted(err error) {
|
||||
if err == nil {
|
||||
totalSSHCommands.Inc()
|
||||
} else {
|
||||
totalSSHCommandErrors.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDataProviderAvailability updates the metric for the data provider availability
|
||||
func UpdateDataProviderAvailability(err error) {
|
||||
if err == nil {
|
||||
|
|
16
sftpd/cmd_unix.go
Normal file
16
sftpd/cmd_unix.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// +build !windows
|
||||
|
||||
package sftpd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func wrapCmd(cmd *exec.Cmd, uid, gid int) *exec.Cmd {
|
||||
if uid > 0 || gid > 0 {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
|
||||
}
|
||||
return cmd
|
||||
}
|
9
sftpd/cmd_windows.go
Normal file
9
sftpd/cmd_windows.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package sftpd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func wrapCmd(cmd *exec.Cmd, uid, gid int) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
|
@ -315,7 +315,7 @@ func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error
|
|||
return getSFTPErrorFromOSError(err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -397,7 +397,7 @@ func (c Connection) handleSFTPRemove(path string) error {
|
|||
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
|
||||
}
|
||||
go executeAction(operationDelete, c.User.Username, path, "")
|
||||
go executeAction(operationDelete, c.User.Username, path, "", "")
|
||||
|
||||
return sftp.ErrSSHFxOk
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -18,10 +19,11 @@ import (
|
|||
)
|
||||
|
||||
type MockChannel struct {
|
||||
Buffer *bytes.Buffer
|
||||
StdErrBuffer *bytes.Buffer
|
||||
ReadError error
|
||||
WriteError error
|
||||
Buffer *bytes.Buffer
|
||||
StdErrBuffer *bytes.Buffer
|
||||
ReadError error
|
||||
WriteError error
|
||||
ShortWriteErr bool
|
||||
}
|
||||
|
||||
func (c *MockChannel) Read(data []byte) (int, error) {
|
||||
|
@ -35,6 +37,9 @@ func (c *MockChannel) Write(data []byte) (int, error) {
|
|||
if c.WriteError != nil {
|
||||
return 0, c.WriteError
|
||||
}
|
||||
if c.ShortWriteErr {
|
||||
return 0, nil
|
||||
}
|
||||
return c.Buffer.Write(data)
|
||||
}
|
||||
|
||||
|
@ -65,17 +70,17 @@ func TestWrongActions(t *testing.T) {
|
|||
Command: badCommand,
|
||||
HTTPNotificationURL: "",
|
||||
}
|
||||
err := executeAction(operationDownload, "username", "path", "")
|
||||
err := executeAction(operationDownload, "username", "path", "", "")
|
||||
if err == nil {
|
||||
t.Errorf("action with bad command must fail")
|
||||
}
|
||||
err = executeAction(operationDelete, "username", "path", "")
|
||||
err = executeAction(operationDelete, "username", "path", "", "")
|
||||
if err != nil {
|
||||
t.Errorf("action not configured must silently fail")
|
||||
}
|
||||
actions.Command = ""
|
||||
actions.HTTPNotificationURL = "http://foo\x7f.com/"
|
||||
err = executeAction(operationDownload, "username", "path", "")
|
||||
err = executeAction(operationDownload, "username", "path", "", "")
|
||||
if err == nil {
|
||||
t.Errorf("action with bad url must fail")
|
||||
}
|
||||
|
@ -288,6 +293,16 @@ func TestSSHCommandPath(t *testing.T) {
|
|||
if path != "/tmp/" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
sshCommand.args = []string{"-t", "/tmp/../../../path"}
|
||||
path = sshCommand.getDestPath()
|
||||
if path != "/path" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
sshCommand.args = []string{"-t", ".."}
|
||||
path = sshCommand.getDestPath()
|
||||
if path != "/" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandErrors(t *testing.T) {
|
||||
|
@ -305,6 +320,9 @@ func TestSSHCommandErrors(t *testing.T) {
|
|||
connection := Connection{
|
||||
channel: &mockSSHChannel,
|
||||
netConn: client,
|
||||
User: dataprovider.User{
|
||||
Permissions: []string{dataprovider.PermAny},
|
||||
},
|
||||
}
|
||||
cmd := sshCommand{
|
||||
command: "md5sum",
|
||||
|
@ -324,6 +342,170 @@ func TestSSHCommandErrors(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("ssh command must fail, we are requesting an invalid path")
|
||||
}
|
||||
cmd = sshCommand{
|
||||
command: "git-receive-pack",
|
||||
connection: connection,
|
||||
args: []string{"/../../testrepo"},
|
||||
}
|
||||
err = cmd.handle()
|
||||
if err == nil {
|
||||
t.Errorf("ssh command must fail, we are requesting an invalid path")
|
||||
}
|
||||
cmd.connection.User.HomeDir = os.TempDir()
|
||||
cmd.connection.User.QuotaFiles = 1
|
||||
cmd.connection.User.UsedQuotaFiles = 2
|
||||
err = cmd.handle()
|
||||
if err != errQuotaExceeded {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
cmd.connection.User.QuotaFiles = 0
|
||||
cmd.connection.User.UsedQuotaFiles = 0
|
||||
cmd.connection.User.Permissions = []string{dataprovider.PermListItems}
|
||||
err = cmd.handle()
|
||||
if err != errPermissionDenied {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
cmd.connection.User.Permissions = []string{dataprovider.PermAny}
|
||||
cmd.command = "invalid_command"
|
||||
command, err := cmd.getSystemCommand()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
err = cmd.executeSystemCommand(command)
|
||||
if err == nil {
|
||||
t.Errorf("invalid command must fail")
|
||||
}
|
||||
command, err = cmd.getSystemCommand()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
command.cmd.StderrPipe()
|
||||
err = cmd.executeSystemCommand(command)
|
||||
if err == nil {
|
||||
t.Errorf("command must fail, pipe was already assigned")
|
||||
}
|
||||
err = cmd.executeSystemCommand(command)
|
||||
if err == nil {
|
||||
t.Errorf("command must fail, pipe was already assigned")
|
||||
}
|
||||
command, err = cmd.getSystemCommand()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
command.cmd.StdoutPipe()
|
||||
err = cmd.executeSystemCommand(command)
|
||||
if err == nil {
|
||||
t.Errorf("command must fail, pipe was already assigned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandQuotaScan(t *testing.T) {
|
||||
buf := make([]byte, 65535)
|
||||
stdErrBuf := make([]byte, 65535)
|
||||
readErr := fmt.Errorf("test read error")
|
||||
mockSSHChannel := MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: readErr,
|
||||
}
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
connection := Connection{
|
||||
channel: &mockSSHChannel,
|
||||
netConn: client,
|
||||
User: dataprovider.User{
|
||||
Permissions: []string{dataprovider.PermAny},
|
||||
QuotaFiles: 1,
|
||||
HomeDir: "invalid_path",
|
||||
},
|
||||
}
|
||||
cmd := sshCommand{
|
||||
command: "git-receive-pack",
|
||||
connection: connection,
|
||||
args: []string{"/testrepo"},
|
||||
}
|
||||
err := cmd.rescanHomeDir()
|
||||
if err == nil {
|
||||
t.Errorf("scanning an invalid home dir must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemCommandErrors(t *testing.T) {
|
||||
buf := make([]byte, 65535)
|
||||
stdErrBuf := make([]byte, 65535)
|
||||
readErr := fmt.Errorf("test read error")
|
||||
writeErr := fmt.Errorf("test write error")
|
||||
mockSSHChannel := MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: nil,
|
||||
WriteError: writeErr,
|
||||
}
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
connection := Connection{
|
||||
channel: &mockSSHChannel,
|
||||
netConn: client,
|
||||
User: dataprovider.User{
|
||||
Permissions: []string{dataprovider.PermAny},
|
||||
HomeDir: os.TempDir(),
|
||||
},
|
||||
}
|
||||
sshCmd := sshCommand{
|
||||
command: "ls",
|
||||
connection: connection,
|
||||
args: []string{},
|
||||
}
|
||||
systemCmd, err := sshCmd.getSystemCommand()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
systemCmd.cmd.Dir = os.TempDir()
|
||||
// FIXME: the command completes but the fake client was unable to read the response
|
||||
// no error is reported in this case
|
||||
sshCmd.executeSystemCommand(systemCmd)
|
||||
|
||||
mockSSHChannel = MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: readErr,
|
||||
WriteError: nil,
|
||||
}
|
||||
sshCmd.connection.channel = &mockSSHChannel
|
||||
transfer := Transfer{transferType: transferDownload}
|
||||
destBuff := make([]byte, 65535)
|
||||
dst := bytes.NewBuffer(destBuff)
|
||||
_, err = transfer.copyFromReaderToWriter(dst, sshCmd.connection.channel, 0)
|
||||
if err != readErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
mockSSHChannel = MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: nil,
|
||||
WriteError: nil,
|
||||
}
|
||||
sshCmd.connection.channel = &mockSSHChannel
|
||||
_, err = transfer.copyFromReaderToWriter(dst, sshCmd.connection.channel, 1)
|
||||
if err != errQuotaExceeded {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
mockSSHChannel = MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: nil,
|
||||
WriteError: nil,
|
||||
ShortWriteErr: true,
|
||||
}
|
||||
sshCmd.connection.channel = &mockSSHChannel
|
||||
_, err = transfer.copyFromReaderToWriter(sshCmd.connection.channel, dst, 0)
|
||||
if err != io.ErrShortWrite {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConnectionInfo(t *testing.T) {
|
||||
|
@ -339,7 +521,6 @@ func TestGetConnectionInfo(t *testing.T) {
|
|||
if !strings.Contains(info, "sha1sum /test_file.dat") {
|
||||
t.Errorf("ssh command not found in connection info")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSCPFileMode(t *testing.T) {
|
||||
|
@ -974,3 +1155,17 @@ func TestSFTPExtensions(t *testing.T) {
|
|||
}
|
||||
sftpExtensions = initialSFTPExtensions
|
||||
}
|
||||
|
||||
func TestWrapCmd(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("executing a command as another uid/gid is not supported on Windows")
|
||||
}
|
||||
cmd := exec.Command("ls")
|
||||
cmd = wrapCmd(cmd, 1000, 1001)
|
||||
if cmd.SysProcAttr.Credential.Uid != 1000 {
|
||||
t.Errorf("unexpected uid")
|
||||
}
|
||||
if cmd.SysProcAttr.Credential.Gid != 1001 {
|
||||
t.Errorf("unexpected gid")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ const (
|
|||
operationUpload = "upload"
|
||||
operationDelete = "delete"
|
||||
operationRename = "rename"
|
||||
operationSSHCmd = "ssh_cmd"
|
||||
protocolSFTP = "SFTP"
|
||||
protocolSCP = "SCP"
|
||||
protocolSSH = "SSH"
|
||||
|
@ -61,9 +62,11 @@ var (
|
|||
actions Actions
|
||||
uploadMode int
|
||||
setstatMode int
|
||||
supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd"}
|
||||
defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd"}
|
||||
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
|
||||
supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd",
|
||||
"git-receive-pack", "git-upload-pack", "git-upload-archive"}
|
||||
defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd"}
|
||||
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
|
||||
gitCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive"}
|
||||
)
|
||||
|
||||
type connectionTransfer struct {
|
||||
|
@ -85,7 +88,7 @@ type ActiveQuotaScan struct {
|
|||
// Actions to execute on SFTP create, download, delete and rename.
|
||||
// An external command can be executed and/or an HTTP notification can be fired
|
||||
type Actions struct {
|
||||
// Valid values are download, upload, delete, rename. Empty slice to disable
|
||||
// Valid values are download, upload, delete, rename, ssh_cmd. Empty slice to disable
|
||||
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
|
||||
// Absolute path to the command to execute, empty to disable
|
||||
Command string `json:"command" mapstructure:"command"`
|
||||
|
@ -412,17 +415,17 @@ func isAtomicUploadEnabled() bool {
|
|||
}
|
||||
|
||||
// executed in a goroutine
|
||||
func executeAction(operation string, username string, path string, target string) error {
|
||||
func executeAction(operation, username, path, target, sshCmd string) error {
|
||||
if !utils.IsStringInSlice(operation, actions.ExecuteOn) {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if len(actions.Command) > 0 && filepath.IsAbs(actions.Command) {
|
||||
if _, err = os.Stat(actions.Command); err == nil {
|
||||
command := exec.Command(actions.Command, operation, username, path, target)
|
||||
command := exec.Command(actions.Command, operation, username, path, target, sshCmd)
|
||||
err = command.Start()
|
||||
logger.Debug(logSender, "", "start command %#v with arguments: %v, %v, %v, %v, error: %v",
|
||||
actions.Command, operation, username, path, target, err)
|
||||
logger.Debug(logSender, "", "start command %#v with arguments: %v, %v, %v, %v %v, error: %v",
|
||||
actions.Command, operation, username, path, target, sshCmd, err)
|
||||
if err == nil {
|
||||
// we are in a goroutine but we don't want to block here, this way we can send the
|
||||
// HTTP notification, if configured, without waiting the end of the command
|
||||
|
@ -443,6 +446,9 @@ func executeAction(operation string, username string, path string, target string
|
|||
if len(target) > 0 {
|
||||
q.Add("target_path", target)
|
||||
}
|
||||
if len(sshCmd) > 0 {
|
||||
q.Add("ssh_cmd", sshCmd)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
startTime := time.Now()
|
||||
httpClient := &http.Client{
|
||||
|
|
|
@ -89,8 +89,11 @@ var (
|
|||
allPerms = []string{dataprovider.PermAny}
|
||||
homeBasePath string
|
||||
scpPath string
|
||||
gitPath string
|
||||
sshPath string
|
||||
pubKeyPath string
|
||||
privateKeyPath string
|
||||
gitWrapPath string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -123,16 +126,42 @@ func TestMain(m *testing.M) {
|
|||
// simply does not execute some code so if it works in atomic mode will
|
||||
// work in non atomic mode too
|
||||
sftpdConf.UploadMode = 2
|
||||
var scriptArgs string
|
||||
if runtime.GOOS == "windows" {
|
||||
homeBasePath = "C:\\"
|
||||
scriptArgs = "%*"
|
||||
} else {
|
||||
homeBasePath = "/tmp"
|
||||
sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete"}
|
||||
sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete", "ssh_cmd"}
|
||||
sftpdConf.Actions.Command = "/usr/bin/true"
|
||||
sftpdConf.Actions.HTTPNotificationURL = "http://127.0.0.1:8080/"
|
||||
scriptArgs = "$@"
|
||||
}
|
||||
|
||||
scpPath, err = exec.LookPath("scp")
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||
logger.WarnToConsole("unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||
scpPath = ""
|
||||
}
|
||||
|
||||
gitPath, err = exec.LookPath("git")
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to get git command. GIT tests will be skipped, err: %v", err)
|
||||
logger.WarnToConsole("unable to get git command. GIT tests will be skipped, err: %v", err)
|
||||
gitPath = ""
|
||||
}
|
||||
|
||||
sshPath, err = exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to get ssh command. GIT tests will be skipped, err: %v", err)
|
||||
logger.WarnToConsole("unable to get ssh command. GIT tests will be skipped, err: %v", err)
|
||||
gitPath = ""
|
||||
}
|
||||
|
||||
pubKeyPath = filepath.Join(homeBasePath, "ssh_key.pub")
|
||||
privateKeyPath = filepath.Join(homeBasePath, "ssh_key")
|
||||
gitWrapPath = filepath.Join(homeBasePath, "gitwrap.sh")
|
||||
err = ioutil.WriteFile(pubKeyPath, []byte(testPubKey+"\n"), 0600)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("unable to save public key to file: %v", err)
|
||||
|
@ -141,17 +170,15 @@ func TestMain(m *testing.M) {
|
|||
if err != nil {
|
||||
logger.WarnToConsole("unable to save private key to file: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(gitWrapPath, []byte(fmt.Sprintf("%v -i %v -oStrictHostKeyChecking=no %v\n",
|
||||
sshPath, privateKeyPath, scriptArgs)), 0755)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("unable to save gitwrap shell script: %v", err)
|
||||
}
|
||||
|
||||
sftpd.SetDataProvider(dataProvider)
|
||||
httpd.SetDataProvider(dataProvider)
|
||||
|
||||
scpPath, err = exec.LookPath("scp")
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||
logger.WarnToConsole("unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||
scpPath = ""
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf)
|
||||
if err := sftpdConf.Initialize(configDir); err != nil {
|
||||
|
@ -169,8 +196,11 @@ func TestMain(m *testing.M) {
|
|||
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logfilePath)
|
||||
//os.Remove(logfilePath)
|
||||
os.Remove(loginBannerFile)
|
||||
os.Remove(pubKeyPath)
|
||||
os.Remove(privateKeyPath)
|
||||
os.Remove(gitWrapPath)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
|
@ -249,6 +279,8 @@ func TestBasicSFTPHandling(t *testing.T) {
|
|||
if (expectedQuotaSize - testFileSize) != user.UsedQuotaSize {
|
||||
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
os.Remove(localDownloadPath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -310,6 +342,8 @@ func TestUploadResume(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("file upload resume with invalid offset must fail")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
os.Remove(localDownloadPath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -439,6 +473,7 @@ func TestRemove(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("remove dir error: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -493,6 +528,7 @@ func TestLink(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -557,6 +593,7 @@ func TestStat(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("truncate must be silently ignored: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -616,6 +653,7 @@ func TestStatChownChmod(t *testing.T) {
|
|||
if err != os.ErrNotExist {
|
||||
t.Errorf("unexpected chown error: %v expected: %v", err, os.ErrNotExist)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -677,6 +715,7 @@ func TestChtimes(t *testing.T) {
|
|||
if diff > 1 {
|
||||
t.Errorf("diff between wanted and real modification time too big: %v", diff)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -750,6 +789,7 @@ func TestEscapeHomeDir(t *testing.T) {
|
|||
t.Errorf("setstat on a file outside home dir must fail")
|
||||
}
|
||||
os.Remove(linkPath)
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -797,6 +837,7 @@ func TestHomeSpecialChars(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1120,7 +1161,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
|||
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize)
|
||||
}
|
||||
}
|
||||
// now set a quota size restriction and upload the same fail, upload should fail for space limit exceeded
|
||||
// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded
|
||||
user.QuotaSize = testFileSize - 1
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1143,6 +1184,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
|
@ -1170,6 +1212,7 @@ func TestQuotaScan(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("file upload error: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1184,16 +1227,9 @@ func TestQuotaScan(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error starting quota scan: %v", err)
|
||||
}
|
||||
scans, _, err := httpd.GetQuotaScans(http.StatusOK)
|
||||
err = waitQuotaScans()
|
||||
if err != nil {
|
||||
t.Errorf("error getting active quota scans: %v", err)
|
||||
}
|
||||
for len(scans) > 0 {
|
||||
scans, _, err = httpd.GetQuotaScans(http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("error getting active quota scans: %v", err)
|
||||
break
|
||||
}
|
||||
t.Errorf("error waiting for active quota scans: %v", err)
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1219,7 +1255,11 @@ func TestMultipleQuotaScans(t *testing.T) {
|
|||
if sftpd.AddQuotaScan(defaultUsername) {
|
||||
t.Errorf("add quota must fail if another scan is already active")
|
||||
}
|
||||
sftpd.RemoveQuotaScan(defaultPassword)
|
||||
sftpd.RemoveQuotaScan(defaultUsername)
|
||||
activeScans := sftpd.GetQuotaScans()
|
||||
if len(activeScans) > 0 {
|
||||
t.Errorf("no quota scan must be active: %v", len(activeScans))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotaSize(t *testing.T) {
|
||||
|
@ -1255,6 +1295,7 @@ func TestQuotaSize(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1327,6 +1368,8 @@ func TestBandwidthAndConnections(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("connection closed upload must fail")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
os.Remove(localDownloadPath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1423,6 +1466,8 @@ func TestOpenError(t *testing.T) {
|
|||
t.Errorf("unexpected error: %v expected: %v", err, sftp.ErrSSHFxPermissionDenied)
|
||||
}
|
||||
os.Chmod(filepath.Join(user.GetHomeDir(), "test"), 0755)
|
||||
os.Remove(testFilePath)
|
||||
os.Remove(localDownloadPath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1475,6 +1520,7 @@ func TestOverwriteDirWithFile(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1714,6 +1760,8 @@ func TestPermDownload(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
os.Remove(localDownloadPath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1748,6 +1796,7 @@ func TestPermUpload(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("file upload without permission should not succeed")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1786,6 +1835,7 @@ func TestPermOverwrite(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("file overwrite without permission should not succeed")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1824,6 +1874,7 @@ func TestPermDelete(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("delete without permission should not succeed")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1866,6 +1917,7 @@ func TestPermRename(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1935,6 +1987,7 @@ func TestPermSymlink(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -1977,6 +2030,7 @@ func TestPermChmod(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -2011,7 +2065,7 @@ func TestPermChown(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("file upload error: %v", err)
|
||||
}
|
||||
err = client.Chown(testFileName, 1000, 1000)
|
||||
err = client.Chown(testFileName, os.Getuid(), os.Getgid())
|
||||
if err == nil {
|
||||
t.Errorf("chown without permission should not succeed")
|
||||
}
|
||||
|
@ -2019,6 +2073,7 @@ func TestPermChown(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -2061,6 +2116,7 @@ func TestPermChtimes(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded file: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -2168,6 +2224,7 @@ func TestSSHFileHash(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("hash for an invalid path must fail")
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -2176,6 +2233,92 @@ func TestSSHFileHash(t *testing.T) {
|
|||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
func TestBasicGitCommands(t *testing.T) {
|
||||
if len(gitPath) == 0 || len(sshPath) == 0 {
|
||||
t.Skip("git and/or ssh command not found, unable to execute this test")
|
||||
}
|
||||
usePubKey := true
|
||||
u := getTestUser(usePubKey)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
repoName := "testrepo"
|
||||
clonePath := filepath.Join(homeBasePath, repoName)
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
os.RemoveAll(filepath.Join(homeBasePath, repoName))
|
||||
out, err := initGitRepo(filepath.Join(user.HomeDir, repoName))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v out: %v", err, string(out))
|
||||
}
|
||||
out, err = cloneGitRepo(homeBasePath, "/"+repoName, user.Username)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v out: %v", err, string(out))
|
||||
}
|
||||
out, err = addFileToGitRepo(clonePath, 128)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v out: %v", err, string(out))
|
||||
}
|
||||
user.QuotaFiles = 100000
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
out, err = pushToGitRepo(clonePath)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v out: %v", err, string(out))
|
||||
}
|
||||
err = waitQuotaScans()
|
||||
if err != nil {
|
||||
t.Errorf("error waiting for active quota scans: %v", err)
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get user: %v", err)
|
||||
}
|
||||
user.QuotaSize = user.UsedQuotaSize - 1
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
out, err = pushToGitRepo(clonePath)
|
||||
if err == nil {
|
||||
t.Errorf("git push must fail if quota is exceeded, out: %v", string(out))
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
os.RemoveAll(clonePath)
|
||||
}
|
||||
|
||||
func TestGitErrors(t *testing.T) {
|
||||
if len(gitPath) == 0 || len(sshPath) == 0 {
|
||||
t.Skip("git and/or ssh command not found, unable to execute this test")
|
||||
}
|
||||
usePubKey := true
|
||||
u := getTestUser(usePubKey)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
repoName := "testrepo"
|
||||
clonePath := filepath.Join(homeBasePath, repoName)
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
os.RemoveAll(filepath.Join(homeBasePath, repoName))
|
||||
out, err := cloneGitRepo(homeBasePath, "/"+repoName, user.Username)
|
||||
if err == nil {
|
||||
t.Errorf("cloning a missing repo must fail, out: %v", string(out))
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
os.RemoveAll(clonePath)
|
||||
}
|
||||
|
||||
// Start SCP tests
|
||||
func TestSCPBasicHandling(t *testing.T) {
|
||||
if len(scpPath) == 0 {
|
||||
|
@ -2240,6 +2383,7 @@ func TestSCPBasicHandling(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.Remove(testFilePath)
|
||||
}
|
||||
|
||||
func TestSCPUploadFileOverwrite(t *testing.T) {
|
||||
|
@ -2294,6 +2438,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) {
|
|||
}
|
||||
}
|
||||
os.Remove(localPath)
|
||||
os.Remove(testFilePath)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
if err != nil {
|
||||
t.Errorf("error removing uploaded files")
|
||||
|
@ -2683,6 +2828,7 @@ func TestSCPUploadPaths(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing uploaded files")
|
||||
}
|
||||
os.Remove(localPath)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
|
@ -2830,6 +2976,7 @@ func TestSCPErrors(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("error removing test file")
|
||||
}
|
||||
os.Remove(localPath)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
if err != nil {
|
||||
t.Errorf("error removing uploaded files")
|
||||
|
@ -3187,3 +3334,74 @@ func waitForActiveTransfer() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitQuotaScans() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
scans, _, err := httpd.GetQuotaScans(http.StatusOK)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for len(scans) > 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
scans, _, err = httpd.GetQuotaScans(http.StatusOK)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initGitRepo(path string) ([]byte, error) {
|
||||
os.MkdirAll(path, 0777)
|
||||
args := []string{"init", "--bare"}
|
||||
cmd := exec.Command(gitPath, args...)
|
||||
cmd.Dir = path
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
func pushToGitRepo(repoPath string) ([]byte, error) {
|
||||
cmd := exec.Command(gitPath, "push")
|
||||
cmd.Dir = repoPath
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("GIT_SSH=%v", gitWrapPath))
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
func cloneGitRepo(basePath, remotePath, username string) ([]byte, error) {
|
||||
remoteUrl := fmt.Sprintf("ssh://%v@127.0.0.1:2022%v", username, remotePath)
|
||||
args := []string{"clone", remoteUrl}
|
||||
cmd := exec.Command(gitPath, args...)
|
||||
cmd.Dir = basePath
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("GIT_SSH=%v", gitWrapPath))
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
func addFileToGitRepo(repoPath string, fileSize int64) ([]byte, error) {
|
||||
path := filepath.Join(repoPath, "test")
|
||||
err := createTestFile(path, fileSize)
|
||||
if err != nil {
|
||||
return []byte(""), err
|
||||
}
|
||||
cmd := exec.Command(gitPath, "config", "user.email", "testuser@example.com")
|
||||
cmd.Dir = repoPath
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
cmd = exec.Command(gitPath, "config", "user.name", "testuser")
|
||||
cmd.Dir = repoPath
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
cmd = exec.Command(gitPath, "add", "test")
|
||||
cmd.Dir = repoPath
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
cmd = exec.Command(gitPath, "commit", "-am", "test")
|
||||
cmd.Dir = repoPath
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
|
|
288
sftpd/ssh_cmd.go
288
sftpd/ssh_cmd.go
|
@ -5,25 +5,41 @@ import (
|
|||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/metrics"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
errQuotaExceeded = errors.New("denying write due to space limit")
|
||||
errPermissionDenied = errors.New("Permission denied. You don't have the permissions to execute this command")
|
||||
)
|
||||
|
||||
type sshCommand struct {
|
||||
command string
|
||||
args []string
|
||||
connection Connection
|
||||
}
|
||||
|
||||
type systemCommand struct {
|
||||
cmd *exec.Cmd
|
||||
realPath string
|
||||
}
|
||||
|
||||
func processSSHCommand(payload []byte, connection *Connection, channel ssh.Channel, enabledSSHCommands []string) bool {
|
||||
var msg sshSubsystemExecMsg
|
||||
if err := ssh.Unmarshal(payload, &msg); err == nil {
|
||||
|
@ -67,42 +83,13 @@ func (c *sshCommand) handle() error {
|
|||
defer removeConnection(c.connection)
|
||||
updateConnectionActivity(c.connection.ID)
|
||||
if utils.IsStringInSlice(c.command, sshHashCommands) {
|
||||
var h hash.Hash
|
||||
if c.command == "md5sum" {
|
||||
h = md5.New()
|
||||
} else if c.command == "sha1sum" {
|
||||
h = sha1.New()
|
||||
} else if c.command == "sha256sum" {
|
||||
h = sha256.New()
|
||||
} else if c.command == "sha384sum" {
|
||||
h = sha512.New384()
|
||||
} else {
|
||||
h = sha512.New()
|
||||
return c.handleHashCommands()
|
||||
} else if utils.IsStringInSlice(c.command, gitCommands) {
|
||||
command, err := c.getSystemCommand()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
var response string
|
||||
if len(c.args) == 0 {
|
||||
// without args we need to read the string to hash from stdin
|
||||
buf := make([]byte, 4096)
|
||||
n, err := c.connection.channel.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
h.Write(buf[:n])
|
||||
response = fmt.Sprintf("%x -\n", h.Sum(nil))
|
||||
} else {
|
||||
sshPath := c.getDestPath()
|
||||
path, err := c.connection.buildPath(sshPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
hash, err := computeHashForFile(h, path)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
response = fmt.Sprintf("%v %v\n", hash, sshPath)
|
||||
}
|
||||
c.connection.channel.Write([]byte(response))
|
||||
c.sendExitStatus(nil)
|
||||
return c.executeSystemCommand(command)
|
||||
} else if c.command == "cd" {
|
||||
c.sendExitStatus(nil)
|
||||
} else if c.command == "pwd" {
|
||||
|
@ -113,12 +100,236 @@ func (c *sshCommand) handle() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) handleHashCommands() error {
|
||||
var h hash.Hash
|
||||
if c.command == "md5sum" {
|
||||
h = md5.New()
|
||||
} else if c.command == "sha1sum" {
|
||||
h = sha1.New()
|
||||
} else if c.command == "sha256sum" {
|
||||
h = sha256.New()
|
||||
} else if c.command == "sha384sum" {
|
||||
h = sha512.New384()
|
||||
} else {
|
||||
h = sha512.New()
|
||||
}
|
||||
var response string
|
||||
if len(c.args) == 0 {
|
||||
// without args we need to read the string to hash from stdin
|
||||
buf := make([]byte, 4096)
|
||||
n, err := c.connection.channel.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
h.Write(buf[:n])
|
||||
response = fmt.Sprintf("%x -\n", h.Sum(nil))
|
||||
} else {
|
||||
sshPath := c.getDestPath()
|
||||
path, err := c.connection.buildPath(sshPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
hash, err := computeHashForFile(h, path)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
response = fmt.Sprintf("%v %v\n", hash, sshPath)
|
||||
}
|
||||
c.connection.channel.Write([]byte(response))
|
||||
c.sendExitStatus(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
if c.connection.User.QuotaFiles > 0 && c.connection.User.UsedQuotaFiles > c.connection.User.QuotaFiles {
|
||||
return c.sendErrorResponse(errQuotaExceeded)
|
||||
}
|
||||
perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
|
||||
dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
|
||||
if !c.connection.User.HasPerms(perms) {
|
||||
return c.sendErrorResponse(errPermissionDenied)
|
||||
}
|
||||
|
||||
stdin, err := command.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
stdout, err := command.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
stderr, err := command.cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
err = command.cmd.Start()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
|
||||
closeCmdOnError := func() {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "kill cmd: %#v and close ssh channel after read or write error",
|
||||
c.connection.command)
|
||||
command.cmd.Process.Kill()
|
||||
c.connection.channel.Close()
|
||||
}
|
||||
var once sync.Once
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
remainingQuotaSize := int64(0)
|
||||
if c.connection.User.QuotaSize > 0 {
|
||||
remainingQuotaSize = c.connection.User.QuotaSize - c.connection.User.UsedQuotaSize
|
||||
}
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
user: c.connection.User,
|
||||
connectionID: c.connection.ID,
|
||||
transferType: transferUpload,
|
||||
lastActivity: time.Now(),
|
||||
isNewFile: false,
|
||||
protocol: c.connection.protocol,
|
||||
transferError: nil,
|
||||
isFinished: false,
|
||||
minWriteOffset: 0,
|
||||
}
|
||||
addTransfer(&transfer)
|
||||
defer removeTransfer(&transfer)
|
||||
w, e := transfer.copyFromReaderToWriter(stdin, c.connection.channel, remainingQuotaSize)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy to sdtin ended, written: %v, remaining quota: %v, err: %v",
|
||||
c.connection.command, w, remainingQuotaSize, e)
|
||||
if e != nil {
|
||||
once.Do(closeCmdOnError)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
user: c.connection.User,
|
||||
connectionID: c.connection.ID,
|
||||
transferType: transferDownload,
|
||||
lastActivity: time.Now(),
|
||||
isNewFile: false,
|
||||
protocol: c.connection.protocol,
|
||||
transferError: nil,
|
||||
isFinished: false,
|
||||
minWriteOffset: 0,
|
||||
}
|
||||
addTransfer(&transfer)
|
||||
defer removeTransfer(&transfer)
|
||||
w, e := transfer.copyFromReaderToWriter(c.connection.channel, stdout, 0)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy from sdtout ended, written: %v err: %v",
|
||||
c.connection.command, w, e)
|
||||
if e != nil {
|
||||
once.Do(closeCmdOnError)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
user: c.connection.User,
|
||||
connectionID: c.connection.ID,
|
||||
transferType: transferDownload,
|
||||
lastActivity: time.Now(),
|
||||
isNewFile: false,
|
||||
protocol: c.connection.protocol,
|
||||
transferError: nil,
|
||||
isFinished: false,
|
||||
minWriteOffset: 0,
|
||||
}
|
||||
addTransfer(&transfer)
|
||||
defer removeTransfer(&transfer)
|
||||
w, e := transfer.copyFromReaderToWriter(c.connection.channel.Stderr(), stderr, 0)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy from sdterr ended, written: %v err: %v",
|
||||
c.connection.command, w, e)
|
||||
if e != nil || w > 0 {
|
||||
once.Do(closeCmdOnError)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
err = command.cmd.Wait()
|
||||
c.sendExitStatus(err)
|
||||
c.rescanHomeDir()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *sshCommand) getSystemCommand() (systemCommand, error) {
|
||||
command := systemCommand{
|
||||
cmd: nil,
|
||||
realPath: "",
|
||||
}
|
||||
args := make([]string, len(c.args))
|
||||
copy(args, c.args)
|
||||
var path string
|
||||
if len(c.args) > 0 {
|
||||
var err error
|
||||
sshPath := c.getDestPath()
|
||||
path, err = c.connection.buildPath(sshPath)
|
||||
if err != nil {
|
||||
return command, err
|
||||
}
|
||||
args = args[:len(args)-1]
|
||||
args = append(args, path)
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command: %v, with args: %v path: %v", c.command, args, path)
|
||||
cmd := exec.Command(c.command, args...)
|
||||
uid := c.connection.User.GetUID()
|
||||
gid := c.connection.User.GetGID()
|
||||
cmd = wrapCmd(cmd, uid, gid)
|
||||
command.cmd = cmd
|
||||
command.realPath = path
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) rescanHomeDir() error {
|
||||
quotaTracking := dataprovider.GetQuotaTracking()
|
||||
if (!c.connection.User.HasQuotaRestrictions() && quotaTracking == 2) || quotaTracking == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
var numFiles int
|
||||
var size int64
|
||||
if AddQuotaScan(c.connection.User.Username) {
|
||||
numFiles, size, _, err = utils.ScanDirContents(c.connection.User.HomeDir)
|
||||
if err != nil {
|
||||
c.connection.Log(logger.LevelWarn, logSenderSSH, "error scanning user home dir %#v: %v", c.connection.User.HomeDir, err)
|
||||
} else {
|
||||
err := dataprovider.UpdateUserQuota(dataProvider, c.connection.User, numFiles, size, true)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "user home dir scanned, user: %#v, dir: %#v, error: %v",
|
||||
c.connection.User.Username, c.connection.User.HomeDir, err)
|
||||
}
|
||||
RemoveQuotaScan(c.connection.User.Username)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// for the supported command, the path, if any, is the last argument
|
||||
func (c *sshCommand) getDestPath() string {
|
||||
if len(c.args) == 0 {
|
||||
return ""
|
||||
}
|
||||
destPath := filepath.ToSlash(c.args[len(c.args)-1])
|
||||
destPath := strings.Trim(c.args[len(c.args)-1], "'")
|
||||
destPath = strings.Trim(destPath, "\"")
|
||||
destPath = filepath.ToSlash(destPath)
|
||||
if !path.IsAbs(destPath) {
|
||||
destPath = "/" + destPath
|
||||
}
|
||||
|
@ -151,6 +362,11 @@ func (c *sshCommand) sendExitStatus(err error) {
|
|||
}
|
||||
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||
c.connection.channel.Close()
|
||||
metrics.SSHCommandCompleted(err)
|
||||
// for scp we notify single uploads/downloads
|
||||
if err == nil && c.command != "scp" {
|
||||
go executeAction(operationSSHCmd, c.connection.User.Username, c.getDestPath(), "", c.command)
|
||||
}
|
||||
}
|
||||
|
||||
func computeHashForFile(hasher hash.Hash, path string) (string, error) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package sftpd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -101,10 +102,10 @@ func (t *Transfer) Close() error {
|
|||
elapsed := time.Since(t.start).Nanoseconds() / 1000000
|
||||
if t.transferType == transferDownload {
|
||||
logger.TransferLog(downloadLogSender, t.path, elapsed, t.bytesSent, t.user.Username, t.connectionID, t.protocol)
|
||||
go executeAction(operationDownload, t.user.Username, t.path, "")
|
||||
go executeAction(operationDownload, t.user.Username, t.path, "", "")
|
||||
} else {
|
||||
logger.TransferLog(uploadLogSender, t.path, elapsed, t.bytesReceived, t.user.Username, t.connectionID, t.protocol)
|
||||
go executeAction(operationUpload, t.user.Username, t.path, "")
|
||||
go executeAction(operationUpload, t.user.Username, t.path, "", "")
|
||||
}
|
||||
}
|
||||
metrics.TransferCompleted(t.bytesSent, t.bytesReceived, t.transferType, t.transferError)
|
||||
|
@ -136,3 +137,54 @@ func (t *Transfer) handleThrottle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used for ssh commands.
|
||||
// It reads from src until EOF so it does not treat an EOF from Read as an error to be reported.
|
||||
// EOF from Write is reported as error
|
||||
func (t *Transfer) copyFromReaderToWriter(dst io.Writer, src io.Reader, maxWriteSize int64) (int64, error) {
|
||||
var written int64
|
||||
var err error
|
||||
if maxWriteSize < 0 {
|
||||
return 0, errQuotaExceeded
|
||||
}
|
||||
buf := make([]byte, 32768)
|
||||
for {
|
||||
t.lastActivity = time.Now()
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
if t.transferType == transferDownload {
|
||||
t.bytesSent = written
|
||||
} else {
|
||||
t.bytesReceived = written
|
||||
}
|
||||
if maxWriteSize > 0 && written > maxWriteSize {
|
||||
err = errQuotaExceeded
|
||||
break
|
||||
}
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
t.handleThrottle()
|
||||
}
|
||||
t.transferError = err
|
||||
if t.bytesSent > 0 || t.bytesReceived > 0 || err != nil {
|
||||
metrics.TransferCompleted(t.bytesSent, t.bytesReceived, t.transferType, t.transferError)
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
; You need to change the paths for the source files to match your environment
|
||||
|
||||
#define MyAppName "SFTPGo"
|
||||
#define MyAppVersion "0.9.4"
|
||||
#define MyAppVersion "0.9.4-dev"
|
||||
#define MyAppURL "https://github.com/drakkan/sftpgo"
|
||||
#define MyAppExeName "sftpgo.exe"
|
||||
#define MyAppDir "C:\Users\vbox\Desktop\sftpgo_setup"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package utils
|
||||
|
||||
const version = "0.9.4"
|
||||
const version = "0.9.4-dev"
|
||||
|
||||
var (
|
||||
commit = ""
|
||||
|
|
Loading…
Reference in a new issue