allow to customize timeout and env vars for program based hooks
Fixes #847 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
796ea1dde9
commit
751946f47a
16 changed files with 394 additions and 32 deletions
1
.github/workflows/development.yml
vendored
1
.github/workflows/development.yml
vendored
|
@ -96,6 +96,7 @@ jobs:
|
|||
go test -v -p 1 -timeout 5m ./webdavd -covermode=atomic
|
||||
go test -v -p 1 -timeout 2m ./telemetry -covermode=atomic
|
||||
go test -v -p 1 -timeout 2m ./mfa -covermode=atomic
|
||||
go test -v -p 1 -timeout 2m ./command -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: bolt
|
||||
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
|
||||
|
|
|
@ -111,6 +111,11 @@ Command-line flags should be specified in the Subsystem declaration.
|
|||
logger.Error(logSender, connectionID, "unable to initialize http client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
commandConfig := config.GetCommandConfig()
|
||||
if err := commandConfig.Initialize(); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize commands configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||
|
|
100
command/command.go
Normal file
100
command/command.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
minTimeout = 1
|
||||
maxTimeout = 300
|
||||
defaultTimeout = 30
|
||||
)
|
||||
|
||||
var (
|
||||
config Config
|
||||
)
|
||||
|
||||
// Command define the configuration for a specific commands
|
||||
type Command struct {
|
||||
// Path is the command path as defined in the hook configuration
|
||||
Path string `json:"path" mapstructure:"path"`
|
||||
// Timeout specifies a time limit, in seconds, for the command execution.
|
||||
// This value overrides the global timeout if set.
|
||||
// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env
|
||||
// vars that SFTPGo sets
|
||||
Timeout int `json:"timeout" mapstructure:"timeout"`
|
||||
// Env defines additional environment variable for the commands.
|
||||
// Each entry is of the form "key=value".
|
||||
// These values are added to the global environment variables if any
|
||||
Env []string `json:"env" mapstructure:"env"`
|
||||
}
|
||||
|
||||
// Config defines the configuration for external commands such as
|
||||
// program based hooks
|
||||
type Config struct {
|
||||
// Timeout specifies a global time limit, in seconds, for the external commands execution
|
||||
Timeout int `json:"timeout" mapstructure:"timeout"`
|
||||
// Env defines additional environment variable for the commands.
|
||||
// Each entry is of the form "key=value".
|
||||
// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env
|
||||
// vars that SFTPGo sets
|
||||
Env []string `json:"env" mapstructure:"env"`
|
||||
// Commands defines configuration for specific commands
|
||||
Commands []Command `json:"commands" mapstructure:"commands"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
config = Config{
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize configures commands
|
||||
func (c Config) Initialize() error {
|
||||
if c.Timeout < minTimeout || c.Timeout > maxTimeout {
|
||||
return fmt.Errorf("invalid timeout %v", c.Timeout)
|
||||
}
|
||||
for _, env := range c.Env {
|
||||
if len(strings.Split(env, "=")) != 2 {
|
||||
return fmt.Errorf("invalid env var %#v", env)
|
||||
}
|
||||
}
|
||||
for idx, cmd := range c.Commands {
|
||||
if cmd.Path == "" {
|
||||
return fmt.Errorf("invalid path %#v", cmd.Path)
|
||||
}
|
||||
if cmd.Timeout == 0 {
|
||||
c.Commands[idx].Timeout = c.Timeout
|
||||
} else {
|
||||
if cmd.Timeout < minTimeout || cmd.Timeout > maxTimeout {
|
||||
return fmt.Errorf("invalid timeout %v for command %#v", cmd.Timeout, cmd.Path)
|
||||
}
|
||||
}
|
||||
for _, env := range cmd.Env {
|
||||
if len(strings.Split(env, "=")) != 2 {
|
||||
return fmt.Errorf("invalid env var %#v for command %#v", env, cmd.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
config = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig returns the configuration for the specified command
|
||||
func GetConfig(command string) (time.Duration, []string) {
|
||||
env := os.Environ()
|
||||
timeout := time.Duration(config.Timeout) * time.Second
|
||||
env = append(env, config.Env...)
|
||||
for _, cmd := range config.Commands {
|
||||
if cmd.Path == command {
|
||||
timeout = time.Duration(cmd.Timeout) * time.Second
|
||||
env = append(env, cmd.Env...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return timeout, env
|
||||
}
|
105
command/command_test.go
Normal file
105
command/command_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCommandConfig(t *testing.T) {
|
||||
require.Equal(t, defaultTimeout, config.Timeout)
|
||||
cfg := Config{
|
||||
Timeout: 10,
|
||||
Env: []string{"a=b"},
|
||||
}
|
||||
err := cfg.Initialize()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cfg.Timeout, config.Timeout)
|
||||
assert.Equal(t, cfg.Env, config.Env)
|
||||
assert.Len(t, cfg.Commands, 0)
|
||||
timeout, env := GetConfig("cmd")
|
||||
assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)
|
||||
assert.Contains(t, env, "a=b")
|
||||
|
||||
cfg.Commands = []Command{
|
||||
{
|
||||
Path: "cmd1",
|
||||
Timeout: 30,
|
||||
Env: []string{"c=d"},
|
||||
},
|
||||
{
|
||||
Path: "cmd2",
|
||||
Timeout: 0,
|
||||
Env: []string{"e=f"},
|
||||
},
|
||||
}
|
||||
err = cfg.Initialize()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cfg.Timeout, config.Timeout)
|
||||
assert.Equal(t, cfg.Env, config.Env)
|
||||
if assert.Len(t, config.Commands, 2) {
|
||||
assert.Equal(t, cfg.Commands[0].Path, config.Commands[0].Path)
|
||||
assert.Equal(t, cfg.Commands[0].Timeout, config.Commands[0].Timeout)
|
||||
assert.Equal(t, cfg.Commands[0].Env, config.Commands[0].Env)
|
||||
assert.Equal(t, cfg.Commands[1].Path, config.Commands[1].Path)
|
||||
assert.Equal(t, cfg.Timeout, config.Commands[1].Timeout)
|
||||
assert.Equal(t, cfg.Commands[1].Env, config.Commands[1].Env)
|
||||
}
|
||||
timeout, env = GetConfig("cmd1")
|
||||
assert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout)
|
||||
assert.Contains(t, env, "a=b")
|
||||
assert.Contains(t, env, "c=d")
|
||||
assert.NotContains(t, env, "e=f")
|
||||
timeout, env = GetConfig("cmd2")
|
||||
assert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)
|
||||
assert.Contains(t, env, "a=b")
|
||||
assert.NotContains(t, env, "c=d")
|
||||
assert.Contains(t, env, "e=f")
|
||||
}
|
||||
|
||||
func TestConfigErrors(t *testing.T) {
|
||||
c := Config{}
|
||||
err := c.Initialize()
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid timeout")
|
||||
}
|
||||
c.Timeout = 10
|
||||
c.Env = []string{"a"}
|
||||
err = c.Initialize()
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid env var")
|
||||
}
|
||||
c.Env = nil
|
||||
c.Commands = []Command{
|
||||
{
|
||||
Path: "",
|
||||
},
|
||||
}
|
||||
err = c.Initialize()
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid path")
|
||||
}
|
||||
c.Commands = []Command{
|
||||
{
|
||||
Path: "path",
|
||||
Timeout: 10000,
|
||||
},
|
||||
}
|
||||
err = c.Initialize()
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid timeout")
|
||||
}
|
||||
c.Commands = []Command{
|
||||
{
|
||||
Path: "path",
|
||||
Timeout: 30,
|
||||
Env: []string{"b"},
|
||||
},
|
||||
}
|
||||
err = c.Initialize()
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "invalid env var")
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -17,6 +16,7 @@ import (
|
|||
"github.com/sftpgo/sdk"
|
||||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
|
@ -223,11 +223,12 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
|
|||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
timeout, env := command.GetConfig(Config.Actions.Hook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, Config.Actions.Hook)
|
||||
cmd.Env = append(os.Environ(), notificationAsEnvVars(event)...)
|
||||
cmd.Env = append(env, notificationAsEnvVars(event)...)
|
||||
|
||||
startTime := time.Now()
|
||||
err := cmd.Run()
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
"github.com/pires/go-proxyproto"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
|
@ -577,9 +578,12 @@ func (c *Configuration) ExecuteStartupHook() error {
|
|||
return err
|
||||
}
|
||||
startTime := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
timeout, env := command.GetConfig(c.StartupHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.StartupHook)
|
||||
cmd.Env = env
|
||||
err := cmd.Run()
|
||||
logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, error: %v", time.Since(startTime), err)
|
||||
return nil
|
||||
|
@ -617,12 +621,13 @@ func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username
|
|||
logger.Debug(protocol, connID, "invalid post disconnect hook %#v", c.PostDisconnectHook)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
timeout, env := command.GetConfig(c.PostDisconnectHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
cmd := exec.CommandContext(ctx, c.PostDisconnectHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%v", username),
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_DURATION=%v", connDuration),
|
||||
|
@ -676,10 +681,12 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
|
|||
logger.Warn(protocol, "", "Login from ip %#v denied: %v", ipAddr, err)
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
timeout, env := command.GetConfig(c.PostConnectHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.PostConnectHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
|
||||
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
|
||||
err := cmd.Run()
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
|
@ -450,11 +451,12 @@ func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck er
|
|||
c.conn.Log(logger.LevelError, "%v", err)
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
timeout, env := command.GetConfig(Config.DataRetentionHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, Config.DataRetentionHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%v", string(jsonData)))
|
||||
err := cmd.Run()
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/ftpd"
|
||||
|
@ -139,6 +140,7 @@ type globalConfig struct {
|
|||
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
|
||||
HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"`
|
||||
HTTPConfig httpclient.Config `json:"http" mapstructure:"http"`
|
||||
CommandConfig command.Config `json:"command" mapstructure:"command"`
|
||||
KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"`
|
||||
MFAConfig mfa.Config `json:"mfa" mapstructure:"mfa"`
|
||||
TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"`
|
||||
|
@ -353,6 +355,11 @@ func Init() {
|
|||
SkipTLSVerify: false,
|
||||
Headers: nil,
|
||||
},
|
||||
CommandConfig: command.Config{
|
||||
Timeout: 30,
|
||||
Env: nil,
|
||||
Commands: nil,
|
||||
},
|
||||
KMSConfig: kms.Configuration{
|
||||
Secrets: kms.Secrets{
|
||||
URL: "",
|
||||
|
@ -461,6 +468,11 @@ func GetHTTPConfig() httpclient.Config {
|
|||
return globalConf.HTTPConfig
|
||||
}
|
||||
|
||||
// GetCommandConfig returns the configuration for external commands
|
||||
func GetCommandConfig() command.Config {
|
||||
return globalConf.CommandConfig
|
||||
}
|
||||
|
||||
// GetKMSConfig returns the KMS configuration
|
||||
func GetKMSConfig() kms.Configuration {
|
||||
return globalConf.KMSConfig
|
||||
|
@ -674,6 +686,7 @@ func loadBindingsFromEnv() {
|
|||
getHTTPDBindingFromEnv(idx)
|
||||
getHTTPClientCertificatesFromEnv(idx)
|
||||
getHTTPClientHeadersFromEnv(idx)
|
||||
getCommandConfigsFromEnv(idx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1546,6 +1559,9 @@ func getHTTPClientCertificatesFromEnv(idx int) {
|
|||
|
||||
func getHTTPClientHeadersFromEnv(idx int) {
|
||||
header := httpclient.Header{}
|
||||
if len(globalConf.HTTPConfig.Headers) > idx {
|
||||
header = globalConf.HTTPConfig.Headers[idx]
|
||||
}
|
||||
|
||||
key, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTP__HEADERS__%v__KEY", idx))
|
||||
if ok {
|
||||
|
@ -1571,6 +1587,36 @@ func getHTTPClientHeadersFromEnv(idx int) {
|
|||
}
|
||||
}
|
||||
|
||||
func getCommandConfigsFromEnv(idx int) {
|
||||
cfg := command.Command{}
|
||||
if len(globalConf.CommandConfig.Commands) > idx {
|
||||
cfg = globalConf.CommandConfig.Commands[idx]
|
||||
}
|
||||
|
||||
path, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__PATH", idx))
|
||||
if ok {
|
||||
cfg.Path = path
|
||||
}
|
||||
|
||||
timeout, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT", idx))
|
||||
if ok {
|
||||
cfg.Timeout = int(timeout)
|
||||
}
|
||||
|
||||
env, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__ENV", idx))
|
||||
if ok {
|
||||
cfg.Env = env
|
||||
}
|
||||
|
||||
if cfg.Path != "" {
|
||||
if len(globalConf.CommandConfig.Commands) > idx {
|
||||
globalConf.CommandConfig.Commands[idx] = cfg
|
||||
} else {
|
||||
globalConf.CommandConfig.Commands = append(globalConf.CommandConfig.Commands, cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setViperDefaults() {
|
||||
viper.SetDefault("common.idle_timeout", globalConf.Common.IdleTimeout)
|
||||
viper.SetDefault("common.upload_mode", globalConf.Common.UploadMode)
|
||||
|
@ -1714,6 +1760,8 @@ func setViperDefaults() {
|
|||
viper.SetDefault("http.retry_max", globalConf.HTTPConfig.RetryMax)
|
||||
viper.SetDefault("http.ca_certificates", globalConf.HTTPConfig.CACertificates)
|
||||
viper.SetDefault("http.skip_tls_verify", globalConf.HTTPConfig.SkipTLSVerify)
|
||||
viper.SetDefault("command.timeout", globalConf.CommandConfig.Timeout)
|
||||
viper.SetDefault("command.env", globalConf.CommandConfig.Env)
|
||||
viper.SetDefault("kms.secrets.url", globalConf.KMSConfig.Secrets.URL)
|
||||
viper.SetDefault("kms.secrets.master_key", globalConf.KMSConfig.Secrets.MasterKeyString)
|
||||
viper.SetDefault("kms.secrets.master_key_path", globalConf.KMSConfig.Secrets.MasterKeyPath)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/config"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
|
@ -679,6 +680,69 @@ func TestSFTPDBindingsFromEnv(t *testing.T) {
|
|||
require.True(t, bindings[1].ApplyProxyConfig) // default value
|
||||
}
|
||||
|
||||
func TestCommandsFromEnv(t *testing.T) {
|
||||
reset()
|
||||
|
||||
configDir := ".."
|
||||
confName := tempConfigName + ".json"
|
||||
configFilePath := filepath.Join(configDir, confName)
|
||||
err := config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
commandConfig := config.GetCommandConfig()
|
||||
commandConfig.Commands = append(commandConfig.Commands, command.Command{
|
||||
Path: "cmd",
|
||||
Timeout: 10,
|
||||
Env: []string{"a=a"},
|
||||
})
|
||||
c := make(map[string]command.Config)
|
||||
c["command"] = commandConfig
|
||||
jsonConf, err := json.Marshal(c)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(configFilePath, jsonConf, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, confName)
|
||||
require.NoError(t, err)
|
||||
commandConfig = config.GetCommandConfig()
|
||||
require.Equal(t, 30, commandConfig.Timeout)
|
||||
require.Len(t, commandConfig.Env, 0)
|
||||
require.Len(t, commandConfig.Commands, 1)
|
||||
require.Equal(t, "cmd", commandConfig.Commands[0].Path)
|
||||
require.Equal(t, 10, commandConfig.Commands[0].Timeout)
|
||||
require.Equal(t, []string{"a=a"}, commandConfig.Commands[0].Env)
|
||||
|
||||
os.Setenv("SFTPGO_COMMAND__TIMEOUT", "25")
|
||||
os.Setenv("SFTPGO_COMMAND__ENV", "a=b,c=d")
|
||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__0__PATH", "cmd1")
|
||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT", "11")
|
||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2")
|
||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20")
|
||||
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
|
||||
os.Unsetenv("SFTPGO_COMMAND__ENV")
|
||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
|
||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT")
|
||||
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__ENV")
|
||||
})
|
||||
|
||||
err = config.LoadConfig(configDir, confName)
|
||||
assert.NoError(t, err)
|
||||
commandConfig = config.GetCommandConfig()
|
||||
require.Equal(t, 25, commandConfig.Timeout)
|
||||
require.Equal(t, []string{"a=b", "c=d"}, commandConfig.Env)
|
||||
require.Len(t, commandConfig.Commands, 2)
|
||||
require.Equal(t, "cmd1", commandConfig.Commands[0].Path)
|
||||
require.Equal(t, 11, commandConfig.Commands[0].Timeout)
|
||||
require.Equal(t, []string{"a=a"}, commandConfig.Commands[0].Env)
|
||||
require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
|
||||
require.Equal(t, 20, commandConfig.Commands[1].Timeout)
|
||||
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
|
||||
|
||||
err = os.Remove(configFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFTPDBindingsFromEnv(t *testing.T) {
|
||||
reset()
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -13,6 +12,7 @@ import (
|
|||
|
||||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/plugin"
|
||||
|
@ -98,11 +98,12 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName
|
|||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
timeout, env := command.GetConfig(config.Actions.Hook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.Actions.Hook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%v", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%v", objectType),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%v", objectName),
|
||||
|
|
|
@ -47,6 +47,7 @@ import (
|
|||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/command"
|
||||
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
|
@ -3029,10 +3030,12 @@ func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge,
|
|||
|
||||
func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
||||
authResult := 0
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
timeout, env := command.GetConfig(authHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, authHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", user.Password))
|
||||
|
@ -3160,10 +3163,12 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
|
|||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
timeout, env := command.GetConfig(config.CheckPasswordHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.CheckPasswordHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
||||
|
@ -3219,10 +3224,12 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
|
|||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
timeout, env := command.GetConfig(config.PreLoginHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.PreLoginHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
|
||||
|
@ -3352,10 +3359,12 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
|
|||
user.Username, ip, protocol, respCode, time.Since(startTime), err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
timeout, env := command.GetConfig(config.PostLoginHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.PostLoginHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
|
||||
|
@ -3418,11 +3427,12 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
|
|||
return nil, fmt.Errorf("unable to serialize user as JSON: %w", err)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
timeout, env := command.GetConfig(config.ExternalAuthHook)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, config.ExternalAuthHook)
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
||||
|
|
|
@ -325,6 +325,13 @@ The configuration file contains the following sections:
|
|||
- `key`, string
|
||||
- `value`, string. The header is silently ignored if `key` or `value` are empty
|
||||
- `url`, string, optional. If not empty, the header will be added only if the request URL starts with the one specified here
|
||||
- **command**, configuration for external commands such as program based hooks
|
||||
- `timeout`, integer. Timeout specifies a time limit, in seconds, to execute external commands. Valid range: `1-300`. Default: `30`
|
||||
- `env`, list of strings. Additional environment variable to pass to all the external commands. Each entry is of the form `key=value`. Default: empty
|
||||
- `commands`, list of structs. Allow to customize configuration per-command. Each struct has the following fields:
|
||||
- `path`, string. Define the command path as defined in the hook configuration
|
||||
- `timeout`, integer. This value overrides the global timeout if set
|
||||
- `env`, list of strings. These values are added to the environment variables defined for all commands, if any
|
||||
- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md)
|
||||
- `secrets`
|
||||
- `url`, string. Defines the URI to the KMS service. Default: blank.
|
||||
|
|
2
go.mod
2
go.mod
|
@ -67,7 +67,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
|
||||
golang.org/x/net v0.0.0-20220513224357-95641704303c
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
||||
google.golang.org/api v0.80.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -951,8 +951,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 h1:MpIuURY70f0iKp/oooEFtB2oENcHITo/z1b6u41pKCw=
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
|
@ -141,12 +141,6 @@ func (s *Service) Start(disableAWSInstallationCode bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = s.LoadInitialData()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to load initial data: %v", err)
|
||||
logger.ErrorToConsole("unable to load initial data: %v", err)
|
||||
}
|
||||
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
err = httpConfig.Initialize(s.ConfigDir)
|
||||
if err != nil {
|
||||
|
@ -154,6 +148,12 @@ func (s *Service) Start(disableAWSInstallationCode bool) error {
|
|||
logger.ErrorToConsole("error initializing http client: %v", err)
|
||||
return err
|
||||
}
|
||||
commandConfig := config.GetCommandConfig()
|
||||
if err := commandConfig.Initialize(); err != nil {
|
||||
logger.Error(logSender, "", "error initializing commands configuration: %v", err)
|
||||
logger.ErrorToConsole("error initializing commands configuration: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.startServices()
|
||||
go common.Config.ExecuteStartupHook() //nolint:errcheck
|
||||
|
@ -162,6 +162,12 @@ func (s *Service) Start(disableAWSInstallationCode bool) error {
|
|||
}
|
||||
|
||||
func (s *Service) startServices() {
|
||||
err := s.LoadInitialData()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to load initial data: %v", err)
|
||||
logger.ErrorToConsole("unable to load initial data: %v", err)
|
||||
}
|
||||
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
ftpdConf := config.GetFTPDConfig()
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
|
|
|
@ -326,6 +326,11 @@
|
|||
"skip_tls_verify": false,
|
||||
"headers": []
|
||||
},
|
||||
"command": {
|
||||
"timeout": 30,
|
||||
"env": [],
|
||||
"commands": []
|
||||
},
|
||||
"kms": {
|
||||
"secrets": {
|
||||
"url": "",
|
||||
|
|
Loading…
Reference in a new issue