mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
sftpd: add support for some SSH commands
md5sum, sha1sum are used by rclone. cd, pwd improve the support for RemoteFiles mobile app. These commands are all implemented inside SFTPGo so they work even if the matching system commands are not available, for example on Windows
This commit is contained in:
parent
ca6cb34d98
commit
9c4dbbc3f8
14 changed files with 564 additions and 131 deletions
11
README.md
11
README.md
|
@ -145,12 +145,16 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `target_path`, added for `rename` 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 SCP support. 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.
|
||||
- `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
|
||||
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos")
|
||||
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
|
||||
- `macs`, list of strings. available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
|
||||
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to send no login banner
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, 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`. `*` 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.
|
||||
- **"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.
|
||||
|
@ -209,7 +213,8 @@ Here is a full example showing the default config in JSON format:
|
|||
"ciphers": [],
|
||||
"macs": [],
|
||||
"login_banner_file": "",
|
||||
"setstat_mode": 0
|
||||
"setstat_mode": 0,
|
||||
"enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"]
|
||||
},
|
||||
"data_provider": {
|
||||
"driver": "sqlite",
|
||||
|
@ -324,8 +329,8 @@ Flags:
|
|||
-p, --password string Leave empty to use an auto generated value
|
||||
-g, --permissions strings User's permissions. "*" means any permission (default [list,download])
|
||||
-k, --public-key strings
|
||||
--scp Enable SCP
|
||||
-s, --sftpd-port int 0 means a random non privileged port
|
||||
-c, --ssh-commands strings SSH commands to enable. "*" means any supported SSH command including scp (default [md5sum,sha1sum,cd,pwd])
|
||||
-u, --username string Leave empty to use an auto generated value
|
||||
```
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/service"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
directoryToServe string
|
||||
portableSFTPDPort int
|
||||
portableEnableSCP bool
|
||||
portableAdvertiseService bool
|
||||
portableAdvertiseCredentials bool
|
||||
portableUsername string
|
||||
|
@ -19,6 +19,7 @@ var (
|
|||
portableLogFile string
|
||||
portablePublicKeys []string
|
||||
portablePermissions []string
|
||||
portableSSHCommands []string
|
||||
portableCmd = &cobra.Command{
|
||||
Use: "portable",
|
||||
Short: "Serve a single directory",
|
||||
|
@ -52,7 +53,7 @@ Please take a look at the usage below to customize the serving parameters`,
|
|||
Status: 1,
|
||||
},
|
||||
}
|
||||
if err := service.StartPortableMode(portableSFTPDPort, portableEnableSCP, portableAdvertiseService,
|
||||
if err := service.StartPortableMode(portableSFTPDPort, portableSSHCommands, portableAdvertiseService,
|
||||
portableAdvertiseCredentials); err == nil {
|
||||
service.Wait()
|
||||
}
|
||||
|
@ -64,7 +65,8 @@ func init() {
|
|||
portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".",
|
||||
"Path to the directory to serve. This can be an absolute path or a path relative to the current directory")
|
||||
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, "0 means a random non privileged port")
|
||||
portableCmd.Flags().BoolVar(&portableEnableSCP, "scp", false, "Enable SCP")
|
||||
portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
|
||||
"SSH commands to enable. \"*\" means any supported SSH command including scp")
|
||||
portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", "Leave empty to use an auto generated value")
|
||||
portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", "Leave empty to use an auto generated value")
|
||||
portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
|
||||
|
|
|
@ -54,12 +54,13 @@ func init() {
|
|||
Command: "",
|
||||
HTTPNotificationURL: "",
|
||||
},
|
||||
Keys: []sftpd.Key{},
|
||||
IsSCPEnabled: false,
|
||||
KexAlgorithms: []string{},
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
LoginBannerFile: "",
|
||||
Keys: []sftpd.Key{},
|
||||
IsSCPEnabled: false,
|
||||
KexAlgorithms: []string{},
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
LoginBannerFile: "",
|
||||
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
|
||||
},
|
||||
ProviderConf: dataprovider.Config{
|
||||
Driver: "sqlite",
|
||||
|
|
|
@ -288,7 +288,7 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- users
|
||||
summary: Adds a new SFTP/SCP user
|
||||
summary: Adds a new user
|
||||
operationId: add_user
|
||||
requestBody:
|
||||
required: true
|
||||
|
@ -656,7 +656,7 @@ components:
|
|||
- download
|
||||
path:
|
||||
type: string
|
||||
description: SFTP/SCP file path for the upload/download
|
||||
description: file path for the upload/download
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
|
@ -680,14 +680,17 @@ components:
|
|||
description: unique connection identifier
|
||||
client_version:
|
||||
type: string
|
||||
description: SFTP/SCP client version
|
||||
description: client version
|
||||
remote_address:
|
||||
type: string
|
||||
description: Remote address for the connected SFTP/SCP client
|
||||
description: Remote address for the connected client
|
||||
connection_time:
|
||||
type: integer
|
||||
format: int64
|
||||
description: connection time as unix timestamp in milliseconds
|
||||
ssh_command:
|
||||
type: string
|
||||
description: SSH command. This is not empty for protocol SSH
|
||||
last_activity:
|
||||
type: integer
|
||||
format: int64
|
||||
|
@ -697,6 +700,7 @@ components:
|
|||
enum:
|
||||
- SFTP
|
||||
- SCP
|
||||
- SSH
|
||||
active_transfers:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -184,6 +184,7 @@ Output:
|
|||
"connection_time": 1564696137971,
|
||||
"last_activity": 1564696159605,
|
||||
"protocol": "SFTP",
|
||||
"ssh_command": "",
|
||||
"active_transfers": [
|
||||
{
|
||||
"operation_type": "upload",
|
||||
|
|
|
@ -128,7 +128,7 @@ func (s *Service) Stop() {
|
|||
}
|
||||
|
||||
// StartPortableMode starts the service in portable mode
|
||||
func (s *Service) StartPortableMode(sftpdPort int, enableSCP, advertiseService, advertiseCredentials bool) error {
|
||||
func (s *Service) StartPortableMode(sftpdPort int, enabledSSHCommands []string, advertiseService, advertiseCredentials bool) error {
|
||||
if s.PortableMode != 1 {
|
||||
return fmt.Errorf("service is not configured for portable mode")
|
||||
}
|
||||
|
@ -158,7 +158,11 @@ func (s *Service) StartPortableMode(sftpdPort int, enableSCP, advertiseService,
|
|||
// dynamic ports starts from 49152
|
||||
sftpdConf.BindPort = 49152 + rand.Intn(15000)
|
||||
}
|
||||
sftpdConf.IsSCPEnabled = enableSCP
|
||||
if utils.IsStringInSlice("*", enabledSSHCommands) {
|
||||
sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
|
||||
} else {
|
||||
sftpdConf.EnabledSSHCommands = enabledSSHCommands
|
||||
}
|
||||
config.SetSFTPDConfig(sftpdConf)
|
||||
|
||||
err = s.Start()
|
||||
|
@ -206,8 +210,8 @@ func (s *Service) StartPortableMode(sftpdPort int, enableSCP, advertiseService,
|
|||
s.Stop()
|
||||
}()
|
||||
logger.InfoToConsole("Portable mode ready, SFTP port: %v, user: %#v, password: %#v, public keys: %v, directory: %#v, "+
|
||||
"permissions: %v, SCP enabled: %v", sftpdConf.BindPort, s.PortableUser.Username, s.PortableUser.Password,
|
||||
s.PortableUser.PublicKeys, s.PortableUser.HomeDir, s.PortableUser.Permissions, sftpdConf.IsSCPEnabled)
|
||||
"permissions: %v, enabled ssh commands: %v", sftpdConf.BindPort, s.PortableUser.Username, s.PortableUser.Password,
|
||||
s.PortableUser.PublicKeys, s.PortableUser.HomeDir, s.PortableUser.Permissions, sftpdConf.EnabledSSHCommands)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ type Connection struct {
|
|||
lock *sync.Mutex
|
||||
netConn net.Conn
|
||||
channel ssh.Channel
|
||||
command string
|
||||
}
|
||||
|
||||
// Log outputs a log entry to the configured logger
|
||||
|
@ -511,8 +512,8 @@ func (c Connection) hasSpace(checkFiles bool) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Normalizes a directory we get from the SFTP request to ensure the user is not able to escape
|
||||
// from their data directory. After normalization if the directory is still within their home
|
||||
// Normalizes a file/directory we get from the SFTP request to ensure the user is not able to escape
|
||||
// from their data directory. After normalization if the file/directory is still within their home
|
||||
// path it is returned. If they managed to "escape" an error will be returned.
|
||||
func (c Connection) buildPath(rawPath string) (string, error) {
|
||||
r := filepath.Clean(filepath.Join(c.User.HomeDir, rawPath))
|
||||
|
@ -520,7 +521,7 @@ func (c Connection) buildPath(rawPath string) (string, error) {
|
|||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", err
|
||||
} else if os.IsNotExist(err) {
|
||||
// The requested directory doesn't exist, so at this point we need to iterate up the
|
||||
// The requested path doesn't exist, so at this point we need to iterate up the
|
||||
// path chain until we hit a directory that _does_ exist and can be validated.
|
||||
_, err = c.findFirstExistingDir(r)
|
||||
if err != nil {
|
||||
|
@ -602,8 +603,8 @@ func (c Connection) isSubDir(sub string) error {
|
|||
return err
|
||||
}
|
||||
if !strings.HasPrefix(sub, parent) {
|
||||
c.Log(logger.LevelWarn, logSender, "dir %#v is not inside: %#v ", sub, parent)
|
||||
return fmt.Errorf("dir %#v is not inside: %#v", sub, parent)
|
||||
c.Log(logger.LevelWarn, logSender, "path %#v is not inside: %#v ", sub, parent)
|
||||
return fmt.Errorf("path %#v is not inside: %#v", sub, parent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -240,6 +241,107 @@ func TestSFTPGetUsedQuota(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSupportedSSHCommands(t *testing.T) {
|
||||
cmds := GetSupportedSSHCommands()
|
||||
if len(cmds) != len(supportedSSHCommands) {
|
||||
t.Errorf("supported ssh commands does not match")
|
||||
}
|
||||
for _, c := range cmds {
|
||||
if !utils.IsStringInSlice(c, supportedSSHCommands) {
|
||||
t.Errorf("invalid ssh command: %v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandPath(t *testing.T) {
|
||||
buf := make([]byte, 65535)
|
||||
stdErrBuf := make([]byte, 65535)
|
||||
mockSSHChannel := MockChannel{
|
||||
Buffer: bytes.NewBuffer(buf),
|
||||
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||
ReadError: nil,
|
||||
}
|
||||
connection := Connection{
|
||||
channel: &mockSSHChannel,
|
||||
}
|
||||
sshCommand := sshCommand{
|
||||
command: "test",
|
||||
connection: connection,
|
||||
args: []string{},
|
||||
}
|
||||
path := sshCommand.getDestPath()
|
||||
if path != "" {
|
||||
t.Errorf("path must be empty")
|
||||
}
|
||||
sshCommand.args = []string{"-t", "/tmp/../path"}
|
||||
path = sshCommand.getDestPath()
|
||||
if path != "/path" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
sshCommand.args = []string{"-t", "/tmp/"}
|
||||
path = sshCommand.getDestPath()
|
||||
if path != "/tmp/" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
sshCommand.args = []string{"-t", "tmp/"}
|
||||
path = sshCommand.getDestPath()
|
||||
if path != "/tmp/" {
|
||||
t.Errorf("unexpected path: %v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandErrors(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,
|
||||
}
|
||||
cmd := sshCommand{
|
||||
command: "md5sum",
|
||||
connection: connection,
|
||||
args: []string{},
|
||||
}
|
||||
err := cmd.handle()
|
||||
if err == nil {
|
||||
t.Errorf("ssh command must fail, we are sending a fake error")
|
||||
}
|
||||
cmd = sshCommand{
|
||||
command: "md5sum",
|
||||
connection: connection,
|
||||
args: []string{"/../../test_file.dat"},
|
||||
}
|
||||
err = cmd.handle()
|
||||
if err == nil {
|
||||
t.Errorf("ssh command must fail, we are requesting an invalid path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConnectionInfo(t *testing.T) {
|
||||
c := ConnectionStatus{
|
||||
Username: "test_user",
|
||||
ConnectionID: "123",
|
||||
ClientVersion: "client",
|
||||
RemoteAddress: "127.0.0.1:1234",
|
||||
Protocol: protocolSSH,
|
||||
SSHCommand: "sha1sum /test_file.dat",
|
||||
}
|
||||
info := c.GetConnectionInfo()
|
||||
if !strings.Contains(info, "sha1sum /test_file.dat") {
|
||||
t.Errorf("ssh command not found in connection info")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSCPFileMode(t *testing.T) {
|
||||
mode := getFileModeAsString(0, true)
|
||||
if mode != "0755" {
|
||||
|
@ -316,8 +418,11 @@ func TestSCPParseUploadMessage(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-t", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-t", "/tmp"},
|
||||
},
|
||||
}
|
||||
_, _, err := scpCommand.parseUploadMessage("invalid")
|
||||
if err == nil {
|
||||
|
@ -352,8 +457,11 @@ func TestSCPProtocolMessages(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-t", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-t", "/tmp"},
|
||||
},
|
||||
}
|
||||
_, err := scpCommand.readProtocolMessage()
|
||||
if err == nil || err != readErr {
|
||||
|
@ -414,8 +522,11 @@ func TestSCPTestDownloadProtocolMessages(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-f", "-p", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-f", "-p", "/tmp"},
|
||||
},
|
||||
}
|
||||
path := "testDir"
|
||||
os.Mkdir(path, 0777)
|
||||
|
@ -483,8 +594,11 @@ func TestSCPCommandHandleErrors(t *testing.T) {
|
|||
netConn: client,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-f", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-f", "/tmp"},
|
||||
},
|
||||
}
|
||||
err := scpCommand.handle()
|
||||
if err == nil || err != readErr {
|
||||
|
@ -516,8 +630,11 @@ func TestSCPRecursiveDownloadErrors(t *testing.T) {
|
|||
netConn: client,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-r", "-f", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-r", "-f", "/tmp"},
|
||||
},
|
||||
}
|
||||
path := "testDir"
|
||||
os.Mkdir(path, 0777)
|
||||
|
@ -556,8 +673,11 @@ func TestSCPRecursiveUploadErrors(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
},
|
||||
}
|
||||
err := scpCommand.handleRecursiveUpload()
|
||||
if err == nil {
|
||||
|
@ -594,8 +714,11 @@ func TestSCPCreateDirs(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
},
|
||||
}
|
||||
err := scpCommand.handleCreateDir("invalid_dir")
|
||||
if err == nil {
|
||||
|
@ -625,8 +748,11 @@ func TestSCPDownloadFileData(t *testing.T) {
|
|||
channel: &mockSSHChannelReadErr,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-r", "-f", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-r", "-f", "/tmp"},
|
||||
},
|
||||
}
|
||||
ioutil.WriteFile(testfile, []byte("test"), 0666)
|
||||
stat, _ := os.Stat(testfile)
|
||||
|
@ -672,8 +798,11 @@ func TestSCPUploadFiledata(t *testing.T) {
|
|||
channel: &mockSSHChannel,
|
||||
}
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
sshCommand: sshCommand{
|
||||
command: "scp",
|
||||
connection: connection,
|
||||
args: []string{"-r", "-t", "/tmp"},
|
||||
},
|
||||
}
|
||||
file, _ := os.Create(testfile)
|
||||
transfer := Transfer{
|
||||
|
|
33
sftpd/scp.go
33
sftpd/scp.go
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -24,13 +23,8 @@ var (
|
|||
newLine = []byte{0x0A}
|
||||
)
|
||||
|
||||
type execMsg struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
type scpCommand struct {
|
||||
connection Connection
|
||||
args []string
|
||||
sshCommand
|
||||
}
|
||||
|
||||
func (c *scpCommand) handle() error {
|
||||
|
@ -494,16 +488,6 @@ func (c *scpCommand) handleDownload(filePath string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// returns the SCP destination path.
|
||||
// We ensure that the path is absolute and in SFTP (UNIX) format
|
||||
func (c *scpCommand) getDestPath() string {
|
||||
destPath := filepath.ToSlash(c.args[len(c.args)-1])
|
||||
if !filepath.IsAbs(destPath) {
|
||||
destPath = "/" + destPath
|
||||
}
|
||||
return destPath
|
||||
}
|
||||
|
||||
func (c *scpCommand) getCommandType() string {
|
||||
return c.args[len(c.args)-2]
|
||||
}
|
||||
|
@ -597,21 +581,6 @@ func (c *scpCommand) sendProtocolMessage(message string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// sends the SCP command exit status
|
||||
func (c *scpCommand) sendExitStatus(err error) {
|
||||
status := uint32(0)
|
||||
if err != nil {
|
||||
status = uint32(1)
|
||||
}
|
||||
exitStatus := sshSubsystemExitStatus{
|
||||
Status: status,
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSCP, "send exit status for command with args: %v user: %v err: %v",
|
||||
c.args, c.connection.User.Username, err)
|
||||
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||
c.connection.channel.Close()
|
||||
}
|
||||
|
||||
// get the next upload protocol message ignoring T command if any
|
||||
// we use our own user setting for permissions
|
||||
func (c *scpCommand) getNextUploadProtocolMessage() (string, error) {
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -61,13 +60,8 @@ type Configuration struct {
|
|||
// Keys are a list of host keys
|
||||
Keys []Key `json:"keys" mapstructure:"keys"`
|
||||
// IsSCPEnabled determines if experimental SCP support is enabled.
|
||||
// We have our own SCP implementation since we can't rely on scp system
|
||||
// command to properly 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
|
||||
// This setting is deprecated and will be removed in future versions,
|
||||
// please add "scp" to the EnabledSSHCommands list to enable it.
|
||||
IsSCPEnabled bool `json:"enable_scp" mapstructure:"enable_scp"`
|
||||
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
|
||||
// preference order.
|
||||
|
@ -83,6 +77,27 @@ type Configuration struct {
|
|||
// SetstatMode 0 means "normal mode": requests for changing permissions and owner/group are executed.
|
||||
// 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
|
||||
SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
|
||||
// List of enabled SSH 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 mobile 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.
|
||||
//
|
||||
// The following SSH commands are enabled by default: "md5sum", "sha1sum", "cd", "pwd".
|
||||
// "*" enables all supported SSH commands.
|
||||
EnabledSSHCommands []string `json:"enabled_ssh_commands" mapstructure:"enabled_ssh_commands"`
|
||||
}
|
||||
|
||||
// Key contains information about host keys
|
||||
|
@ -159,6 +174,7 @@ func (c Configuration) Initialize(configDir string) error {
|
|||
c.configureSecurityOptions(serverConfig)
|
||||
c.configureLoginBanner(serverConfig, configDir)
|
||||
c.configureSFTPExtensions()
|
||||
c.checkSSHCommands()
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort))
|
||||
if err != nil {
|
||||
|
@ -298,24 +314,7 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
|
|||
go c.handleSftpConnection(channel, connection)
|
||||
}
|
||||
case "exec":
|
||||
if c.IsSCPEnabled {
|
||||
var msg execMsg
|
||||
if err := ssh.Unmarshal(req.Payload, &msg); err == nil {
|
||||
name, scpArgs, err := parseCommandPayload(msg.Command)
|
||||
connection.Log(logger.LevelDebug, logSender, "new exec command: %#v args: %v user: %v, error: %v",
|
||||
name, scpArgs, connection.User.Username, err)
|
||||
if err == nil && name == "scp" && len(scpArgs) >= 2 {
|
||||
ok = true
|
||||
connection.protocol = protocolSCP
|
||||
connection.channel = channel
|
||||
scpCommand := scpCommand{
|
||||
connection: connection,
|
||||
args: scpArgs,
|
||||
}
|
||||
go scpCommand.handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
ok = processSSHCommand(req.Payload, &connection, channel, c.EnabledSSHCommands)
|
||||
}
|
||||
req.Reply(ok, nil)
|
||||
}
|
||||
|
@ -389,6 +388,26 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro
|
|||
return p, nil
|
||||
}
|
||||
|
||||
func (c *Configuration) checkSSHCommands() {
|
||||
if utils.IsStringInSlice("*", c.EnabledSSHCommands) {
|
||||
c.EnabledSSHCommands = GetSupportedSSHCommands()
|
||||
return
|
||||
}
|
||||
sshCommands := []string{}
|
||||
if c.IsSCPEnabled {
|
||||
sshCommands = append(sshCommands, "scp")
|
||||
}
|
||||
for _, command := range c.EnabledSSHCommands {
|
||||
if utils.IsStringInSlice(command, supportedSSHCommands) {
|
||||
sshCommands = append(sshCommands, command)
|
||||
} else {
|
||||
logger.Warn(logSender, "", "unsupported ssh command: %#v ignored", command)
|
||||
logger.WarnToConsole("unsupported ssh command: %#v ignored", command)
|
||||
}
|
||||
}
|
||||
c.EnabledSSHCommands = sshCommands
|
||||
}
|
||||
|
||||
// If no host keys are defined we try to use or generate the default one.
|
||||
func (c *Configuration) checkHostKeys(configDir string) error {
|
||||
var err error
|
||||
|
@ -460,11 +479,3 @@ func (c Configuration) generatePrivateKey(file string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCommandPayload(command string) (string, []string, error) {
|
||||
parts := strings.Split(command, " ")
|
||||
if len(parts) < 2 {
|
||||
return parts[0], []string{}, nil
|
||||
}
|
||||
return parts[0], parts[1:], nil
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
const (
|
||||
logSender = "sftpd"
|
||||
logSenderSCP = "scp"
|
||||
logSenderSSH = "ssh"
|
||||
uploadLogSender = "Upload"
|
||||
downloadLogSender = "Download"
|
||||
renameLogSender = "Rename"
|
||||
|
@ -38,6 +39,7 @@ const (
|
|||
operationRename = "rename"
|
||||
protocolSFTP = "SFTP"
|
||||
protocolSCP = "SCP"
|
||||
protocolSSH = "SSH"
|
||||
handshakeTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
|
@ -58,6 +60,9 @@ 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"}
|
||||
)
|
||||
|
||||
type connectionTransfer struct {
|
||||
|
@ -101,21 +106,41 @@ type ConnectionStatus struct {
|
|||
ConnectionTime int64 `json:"connection_time"`
|
||||
// Last activity as unix timestamp in milliseconds
|
||||
LastActivity int64 `json:"last_activity"`
|
||||
// Protocol for this connection: SFTP or SCP
|
||||
// Protocol for this connection: SFTP, SCP, SSH
|
||||
Protocol string `json:"protocol"`
|
||||
// active uploads/downloads
|
||||
Transfers []connectionTransfer `json:"active_transfers"`
|
||||
// for protocol SSH this is the issued command
|
||||
SSHCommand string `json:"ssh_command"`
|
||||
}
|
||||
|
||||
type sshSubsystemExitStatus struct {
|
||||
Status uint32
|
||||
}
|
||||
|
||||
type sshSubsystemExecMsg struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
func init() {
|
||||
openConnections = make(map[string]Connection)
|
||||
idleConnectionTicker = time.NewTicker(5 * time.Minute)
|
||||
}
|
||||
|
||||
// GetDefaultSSHCommands returns the SSH commands enabled as default
|
||||
func GetDefaultSSHCommands() []string {
|
||||
result := make([]string, len(defaultSSHCommands))
|
||||
copy(result, defaultSSHCommands)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetSupportedSSHCommands returns the supported SSH commands
|
||||
func GetSupportedSSHCommands() []string {
|
||||
result := make([]string, len(supportedSSHCommands))
|
||||
copy(result, supportedSSHCommands)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetConnectionDuration returns the connection duration as string
|
||||
func (c ConnectionStatus) GetConnectionDuration() string {
|
||||
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
|
||||
|
@ -123,9 +148,14 @@ func (c ConnectionStatus) GetConnectionDuration() string {
|
|||
}
|
||||
|
||||
// GetConnectionInfo returns connection info.
|
||||
// Protocol,Client Version and RemoteAddress are returned
|
||||
// Protocol,Client Version and RemoteAddress are returned.
|
||||
// For SSH commands the issued command is returned too.
|
||||
func (c ConnectionStatus) GetConnectionInfo() string {
|
||||
return fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress)
|
||||
result := fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress)
|
||||
if c.Protocol == protocolSSH && len(c.SSHCommand) > 0 {
|
||||
result += fmt.Sprintf(". Command: %#v", c.SSHCommand)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTransfersAsString returns the active transfers as string
|
||||
|
@ -251,6 +281,7 @@ func GetConnectionsStats() []ConnectionStatus {
|
|||
LastActivity: utils.GetTimeAsMsSinceEpoch(c.lastActivity),
|
||||
Protocol: c.protocol,
|
||||
Transfers: []connectionTransfer{},
|
||||
SSHCommand: c.command,
|
||||
}
|
||||
for _, t := range activeTransfers {
|
||||
if t.connectionID == c.ID {
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
|
@ -115,8 +117,8 @@ func TestMain(m *testing.M) {
|
|||
"aes256-ctr"}
|
||||
sftpdConf.MACs = []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256"}
|
||||
sftpdConf.LoginBannerFile = loginBannerFileName
|
||||
// we need to test SCP support
|
||||
sftpdConf.IsSCPEnabled = true
|
||||
// we need to test all supported ssh commands
|
||||
sftpdConf.EnabledSSHCommands = []string{"*"}
|
||||
// we run the test cases with UploadMode atomic and resume support. The non atomic code path
|
||||
// simply does not execute some code so if it works in atomic mode will
|
||||
// work in non atomic mode too
|
||||
|
@ -178,6 +180,8 @@ func TestInitialization(t *testing.T) {
|
|||
sftpdConf.Umask = "invalid umask"
|
||||
sftpdConf.BindPort = 2022
|
||||
sftpdConf.LoginBannerFile = "invalid_file"
|
||||
sftpdConf.IsSCPEnabled = true
|
||||
sftpdConf.EnabledSSHCommands = append(sftpdConf.EnabledSSHCommands, "ls")
|
||||
err := sftpdConf.Initialize(configDir)
|
||||
if err == nil {
|
||||
t.Errorf("Inizialize must fail, a SFTP server should be already running")
|
||||
|
@ -291,11 +295,11 @@ func TestUploadResume(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("file download error: %v", err)
|
||||
}
|
||||
initialHash, err := computeFileHash(localDownloadPath)
|
||||
initialHash, err := computeHashForFile(sha256.New(), testFilePath)
|
||||
if err != nil {
|
||||
t.Errorf("error computing file hash: %v", err)
|
||||
}
|
||||
donwloadedFileHash, err := computeFileHash(localDownloadPath)
|
||||
donwloadedFileHash, err := computeHashForFile(sha256.New(), localDownloadPath)
|
||||
if err != nil {
|
||||
t.Errorf("error computing downloaded file hash: %v", err)
|
||||
}
|
||||
|
@ -2065,15 +2069,60 @@ func TestPermChtimes(t *testing.T) {
|
|||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
func TestSSHConnection(t *testing.T) {
|
||||
func TestSSHCommands(t *testing.T) {
|
||||
usePubKey := false
|
||||
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
err = doSSH(user, usePubKey)
|
||||
_, err = runSSHCommand("ls", user, usePubKey)
|
||||
if err == nil {
|
||||
t.Errorf("ssh connection must fail: %v", err)
|
||||
t.Errorf("unsupported ssh command must fail")
|
||||
}
|
||||
_, err = runSSHCommand("cd", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for ssh cd command: %v", err)
|
||||
}
|
||||
out, err := runSSHCommand("pwd", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
if string(out) != "/\n" {
|
||||
t.Errorf("invalid response for ssh pwd command: %v", string(out))
|
||||
}
|
||||
out, err = runSSHCommand("md5sum", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
// echo -n '' | md5sum
|
||||
if !strings.Contains(string(out), "d41d8cd98f00b204e9800998ecf8427e") {
|
||||
t.Errorf("invalid md5sum: %v", string(out))
|
||||
}
|
||||
out, err = runSSHCommand("sha1sum", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
if !strings.Contains(string(out), "da39a3ee5e6b4b0d3255bfef95601890afd80709") {
|
||||
t.Errorf("invalid sha1sum: %v", string(out))
|
||||
}
|
||||
out, err = runSSHCommand("sha256sum", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
if !strings.Contains(string(out), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") {
|
||||
t.Errorf("invalid sha256sum: %v", string(out))
|
||||
}
|
||||
out, err = runSSHCommand("sha384sum", user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
if !strings.Contains(string(out), "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b") {
|
||||
t.Errorf("invalid sha384sum: %v", string(out))
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
|
@ -2081,6 +2130,52 @@ func TestSSHConnection(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSSHFileHash(t *testing.T) {
|
||||
usePubKey := true
|
||||
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unable to create sftp client: %v", err)
|
||||
} else {
|
||||
defer client.Close()
|
||||
testFileName := "test_file.dat"
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
if err != nil {
|
||||
t.Errorf("unable to create test file: %v", err)
|
||||
}
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
if err != nil {
|
||||
t.Errorf("file upload error: %v", err)
|
||||
}
|
||||
initialHash, err := computeHashForFile(sha512.New(), testFilePath)
|
||||
if err != nil {
|
||||
t.Errorf("error computing file hash: %v", err)
|
||||
}
|
||||
out, err := runSSHCommand("sha512sum "+testFileName, user, usePubKey)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
if !strings.Contains(string(out), initialHash) {
|
||||
t.Errorf("invalid sha512sum: %v", string(out))
|
||||
}
|
||||
_, err = runSSHCommand("sha512sum invalid_path", user, usePubKey)
|
||||
if err == nil {
|
||||
t.Errorf("hash for an invalid path must fail")
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
os.RemoveAll(user.GetHomeDir())
|
||||
}
|
||||
|
||||
// Start SCP tests
|
||||
func TestSCPBasicHandling(t *testing.T) {
|
||||
if len(scpPath) == 0 {
|
||||
|
@ -2777,8 +2872,9 @@ func getTestUser(usePubKey bool) dataprovider.User {
|
|||
return user
|
||||
}
|
||||
|
||||
func doSSH(user dataprovider.User, usePubKey bool) error {
|
||||
func runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]byte, error) {
|
||||
var sshSession *ssh.Session
|
||||
var output []byte
|
||||
config := &ssh.ClientConfig{
|
||||
User: defaultUsername,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
|
@ -2788,7 +2884,7 @@ func doSSH(user dataprovider.User, usePubKey bool) error {
|
|||
if usePubKey {
|
||||
key, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
if err != nil {
|
||||
return err
|
||||
return output, err
|
||||
}
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(key)}
|
||||
} else {
|
||||
|
@ -2796,15 +2892,21 @@ func doSSH(user dataprovider.User, usePubKey bool) error {
|
|||
}
|
||||
conn, err := ssh.Dial("tcp", sftpServerAddr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
return output, err
|
||||
}
|
||||
defer conn.Close()
|
||||
sshSession, err = conn.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
return output, err
|
||||
}
|
||||
_, err = sshSession.CombinedOutput("ls")
|
||||
return err
|
||||
var stdout, stderr bytes.Buffer
|
||||
sshSession.Stdout = &stdout
|
||||
sshSession.Stderr = &stderr
|
||||
err = sshSession.Run(command)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to run command %v: %v", command, stderr.Bytes())
|
||||
}
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) {
|
||||
|
@ -3047,18 +3149,17 @@ func getScpUploadCommand(localPath, remotePath string, preserveTime, remoteToRem
|
|||
return exec.Command(scpPath, args...)
|
||||
}
|
||||
|
||||
func computeFileHash(path string) (string, error) {
|
||||
func computeHashForFile(hasher hash.Hash, path string) (string, error) {
|
||||
hash := ""
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return hash, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return hash, err
|
||||
_, err = io.Copy(hasher, f)
|
||||
if err == nil {
|
||||
hash = fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
}
|
||||
hash = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return hash, err
|
||||
}
|
||||
|
||||
|
|
173
sftpd/ssh_cmd.go
Normal file
173
sftpd/ssh_cmd.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package sftpd
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sshCommand struct {
|
||||
command string
|
||||
args []string
|
||||
connection Connection
|
||||
}
|
||||
|
||||
func processSSHCommand(payload []byte, connection *Connection, channel ssh.Channel, enabledSSHCommands []string) bool {
|
||||
var msg sshSubsystemExecMsg
|
||||
if err := ssh.Unmarshal(payload, &msg); err == nil {
|
||||
name, args, err := parseCommandPayload(msg.Command)
|
||||
connection.Log(logger.LevelDebug, logSenderSSH, "new ssh command: %#v args: %v user: %v, error: %v",
|
||||
name, args, connection.User.Username, err)
|
||||
if err == nil && utils.IsStringInSlice(name, enabledSSHCommands) {
|
||||
connection.command = fmt.Sprintf("%v %v", name, strings.Join(args, " "))
|
||||
if name == "scp" && len(args) >= 2 {
|
||||
connection.protocol = protocolSCP
|
||||
connection.channel = channel
|
||||
scpCommand := scpCommand{
|
||||
sshCommand: sshCommand{
|
||||
command: name,
|
||||
connection: *connection,
|
||||
args: args},
|
||||
}
|
||||
go scpCommand.handle()
|
||||
return true
|
||||
}
|
||||
if name != "scp" {
|
||||
connection.protocol = protocolSSH
|
||||
connection.channel = channel
|
||||
sshCommand := sshCommand{
|
||||
command: name,
|
||||
connection: *connection,
|
||||
args: args,
|
||||
}
|
||||
go sshCommand.handle()
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
connection.Log(logger.LevelInfo, logSenderSSH, "ssh command not enabled/supported: %#v", name)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *sshCommand) handle() error {
|
||||
addConnection(c.connection)
|
||||
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()
|
||||
}
|
||||
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)
|
||||
} else if c.command == "cd" {
|
||||
c.sendExitStatus(nil)
|
||||
} else if c.command == "pwd" {
|
||||
// hard coded response to "/"
|
||||
c.connection.channel.Write([]byte("/\n"))
|
||||
c.sendExitStatus(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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])
|
||||
if !path.IsAbs(destPath) {
|
||||
destPath = "/" + destPath
|
||||
}
|
||||
result := path.Clean(destPath)
|
||||
if strings.HasSuffix(destPath, "/") && !strings.HasSuffix(result, "/") {
|
||||
result += "/"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *sshCommand) sendErrorResponse(err error) error {
|
||||
errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err)
|
||||
c.connection.channel.Write([]byte(errorString))
|
||||
c.sendExitStatus(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *sshCommand) sendExitStatus(err error) {
|
||||
status := uint32(0)
|
||||
if err != nil {
|
||||
status = uint32(1)
|
||||
}
|
||||
exitStatus := sshSubsystemExitStatus{
|
||||
Status: status,
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "send exit status for command %#v with args: %v user: %v err: %v",
|
||||
c.command, c.args, c.connection.User.Username, err)
|
||||
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||
c.connection.channel.Close()
|
||||
}
|
||||
|
||||
func computeHashForFile(hasher hash.Hash, path string) (string, error) {
|
||||
hash := ""
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return hash, err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(hasher, f)
|
||||
if err == nil {
|
||||
hash = fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
}
|
||||
return hash, err
|
||||
}
|
||||
|
||||
func parseCommandPayload(command string) (string, []string, error) {
|
||||
parts := strings.Split(command, " ")
|
||||
if len(parts) < 2 {
|
||||
return parts[0], []string{}, nil
|
||||
}
|
||||
return parts[0], parts[1:], nil
|
||||
}
|
|
@ -18,7 +18,8 @@
|
|||
"ciphers": [],
|
||||
"macs": [],
|
||||
"login_banner_file": "",
|
||||
"setstat_mode": 0
|
||||
"setstat_mode": 0,
|
||||
"enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"]
|
||||
},
|
||||
"data_provider": {
|
||||
"driver": "sqlite",
|
||||
|
|
Loading…
Reference in a new issue