diff --git a/common/common.go b/common/common.go index 08d2b79e..47bef7c6 100644 --- a/common/common.go +++ b/common/common.go @@ -312,6 +312,11 @@ type Configuration struct { // If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the // connection will be rejected. ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"` + // Absolute path to an external program or an HTTP URL to invoke as soon as SFTPGo starts. + // If you define an HTTP URL it will be invoked using a `GET` request. + // Please note that SFTPGo services may not yet be available when this hook is run. + // Leave empty do disable. + StartupHook string `json:"startup_hook" mapstructure:"startup_hook"` // Absolute path to an external program or an HTTP URL to invoke after a user connects // and before he tries to login. It allows you to reject the connection based on the source // ip address. Leave empty do disable. @@ -363,6 +368,43 @@ func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Lis return proxyListener, nil } +// ExecuteStartupHook runs the startup hook if defined +func (c *Configuration) ExecuteStartupHook() error { + if c.StartupHook == "" { + return nil + } + if strings.HasPrefix(c.StartupHook, "http") { + var url *url.URL + url, err := url.Parse(c.StartupHook) + if err != nil { + logger.Warn(logSender, "", "Invalid startup hook %#v: %v", c.StartupHook, err) + return err + } + startTime := time.Now() + httpClient := httpclient.GetRetraybleHTTPClient() + resp, err := httpClient.Get(url.String()) + if err != nil { + logger.Warn(logSender, "", "Error executing startup hook: %v", err) + return err + } + defer resp.Body.Close() + logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, response code: %v", time.Since(startTime), resp.StatusCode) + return nil + } + if !filepath.IsAbs(c.StartupHook) { + err := fmt.Errorf("invalid startup hook %#v", c.StartupHook) + logger.Warn(logSender, "", "Invalid startup hook %#v", c.StartupHook) + return err + } + startTime := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, c.StartupHook) + err := cmd.Run() + logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, error: %v", time.Since(startTime), err) + return nil +} + // ExecutePostConnectHook executes the post connect hook if defined func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error { if c.PostConnectHook == "" { diff --git a/common/common_test.go b/common/common_test.go index f0cf4f65..68705b55 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -449,6 +449,33 @@ func TestProxyProtocolVersion(t *testing.T) { assert.Error(t, err) } +func TestStartupHook(t *testing.T) { + Config.StartupHook = "" + + assert.NoError(t, Config.ExecuteStartupHook()) + + Config.StartupHook = "http://foo\x7f.com/startup" + assert.Error(t, Config.ExecuteStartupHook()) + + Config.StartupHook = "http://invalid:5678/" + assert.Error(t, Config.ExecuteStartupHook()) + + Config.StartupHook = fmt.Sprintf("http://%v", httpAddr) + assert.NoError(t, Config.ExecuteStartupHook()) + + Config.StartupHook = "invalidhook" + assert.Error(t, Config.ExecuteStartupHook()) + + if runtime.GOOS != osWindows { + hookCmd, err := exec.LookPath("true") + assert.NoError(t, err) + Config.StartupHook = hookCmd + assert.NoError(t, Config.ExecuteStartupHook()) + } + + Config.StartupHook = "" +} + func TestPostConnectHook(t *testing.T) { Config.PostConnectHook = "" diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 6f77da64..f71df241 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -62,6 +62,7 @@ The configuration file contains the following sections: - `proxy_allowed`, List of IP addresses and IP ranges allowed to send the proxy header: - If `proxy_protocol` is set to 1 and we receive a proxy header from an IP that is not in the list then the connection will be accepted and the header will be ignored - If `proxy_protocol` is set to 2 and we receive a proxy header from an IP that is not in the list then the connection will be rejected + - `startup_hook`, string. Absolute path to an external program or an HTTP URL to invoke as soon as SFTPGo starts. If you define an HTTP URL it will be invoked using a `GET` request. Please note that SFTPGo services may not yet be available when this hook is run. Leave empty do disable - `post_connect_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Post connect hook](./post-connect-hook.md) for more details. Leave empty to disable - `max_total_connections`, integer. Maximum number of concurrent client connections. 0 means unlimited - `defender`, struct containing the defender configuration. See [Defender](./defender.md) for more details. diff --git a/service/service.go b/service/service.go index 481e1298..1fede1ac 100644 --- a/service/service.go +++ b/service/service.go @@ -131,6 +131,7 @@ func (s *Service) Start() error { } s.startServices() + go common.Config.ExecuteStartupHook() //nolint:errcheck return nil } diff --git a/sftpgo.json b/sftpgo.json index 1ee63557..b1c870de 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -9,6 +9,7 @@ "setstat_mode": 0, "proxy_protocol": 0, "proxy_allowed": [], + "startup_hook": "", "post_connect_hook": "", "max_total_connections": 0, "defender": {