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:
Nicola Murino 2022-05-20 19:30:54 +02:00
parent 796ea1dde9
commit 751946f47a
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
16 changed files with 394 additions and 32 deletions

View file

@ -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'

View file

@ -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
View 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
View 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")
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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),

View file

@ -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),

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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()

View file

@ -326,6 +326,11 @@
"skip_tls_verify": false,
"headers": []
},
"command": {
"timeout": 30,
"env": [],
"commands": []
},
"kms": {
"secrets": {
"url": "",