also capture logs for pre-login and check-password commands

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-02-26 15:15:34 +01:00
parent ec67b67e9e
commit 874776bd12
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
10 changed files with 143 additions and 130 deletions

View file

@ -22,6 +22,8 @@ Global environment variables are cleared, for security reasons, when the script
The program must write, on its standard output, the expected JSON serialized response described above.
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `check_password_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `username`

View file

@ -15,6 +15,8 @@ The program must write, on its standard output:
- an empty string (or no response at all) if the user should not be created/updated
- or the SFTPGo user, JSON serialized, if you want to create or update the given user
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `pre_login_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`.
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.

View file

@ -21,7 +21,7 @@ The program must write, on its standard output:
- an empty string, or no response at all, if authentication succeeds and the existing SFTPGo user does not need to be updated. This means that the credentials already stored in SFTPGo must match those used for the current authentication.
- a user with an empty username if the authentication fails
Any output of the program on its standard error will be recorded in the sftpgo logs with sender `external_auth_hook`.
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `external_auth_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:

4
go.mod
View file

@ -24,7 +24,7 @@ require (
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.21.0
github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.10.0
github.com/go-acme/lego/v4 v4.10.2
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/go-chi/render v1.0.2
@ -133,7 +133,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/miekg/dns v1.1.51 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect

9
go.sum
View file

@ -920,8 +920,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-acme/lego/v4 v4.10.0 h1:G4Cgq4lsPxCjqsTKsqhUjRs3oKAGVMFPhvrl6kzzs44=
github.com/go-acme/lego/v4 v4.10.0/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U=
github.com/go-acme/lego/v4 v4.10.2 h1:5eW3qmda5v/LP21v1Hj70edKY1jeFZQwO617tdkwp6Q=
github.com/go-acme/lego/v4 v4.10.2/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
@ -1516,8 +1516,9 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
@ -2098,6 +2099,7 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -2513,6 +2515,7 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -307,7 +307,7 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
startTime := time.Now()
err := cmd.Run()
logger.Debug(event.Protocol, "", "executed command %#v, elapsed: %v, error: %v",
logger.Debug(event.Protocol, "", "executed command %q, elapsed: %s, error: %v",
Config.Actions.Hook, time.Since(startTime), err)
return err

View file

@ -662,7 +662,7 @@ func (c *Configuration) ExecuteStartupHook() error {
cmd := exec.CommandContext(ctx, c.StartupHook, args...)
cmd.Env = env
err := cmd.Run()
logger.Debug(logSender, "", "Startup hook executed, elapsed: %v, error: %v", time.Since(startTime), err)
logger.Debug(logSender, "", "Startup hook executed, elapsed: %s, error: %v", time.Since(startTime), err)
return nil
}
@ -708,12 +708,12 @@ func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username
startTime := time.Now()
cmd := exec.CommandContext(ctx, c.PostDisconnectHook, args...)
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),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
fmt.Sprintf("SFTPGO_CONNECTION_IP=%s", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_CONNECTION_DURATION=%d", connDuration),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%s", protocol))
err := cmd.Run()
logger.Debug(protocol, connID, "Post disconnect hook executed, elapsed: %v error: %v", time.Since(startTime), err)
logger.Debug(protocol, connID, "Post disconnect hook executed, elapsed: %s error: %v", time.Since(startTime), err)
}
func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {
@ -767,11 +767,11 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
cmd := exec.CommandContext(ctx, c.PostConnectHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
fmt.Sprintf("SFTPGO_CONNECTION_IP=%s", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%s", protocol))
err := cmd.Run()
if err != nil {
logger.Warn(protocol, "", "Login from ip %#v denied, connect hook error: %v", ipAddr, err)
logger.Warn(protocol, "", "Login from ip %q denied, connect hook error: %v", ipAddr, err)
}
return err
}

View file

@ -460,10 +460,10 @@ func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck er
cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%v", string(jsonData)))
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", string(jsonData)))
err := cmd.Run()
c.conn.Log(logger.LevelDebug, "notified result using command: %v, elapsed: %v err: %v",
c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
Config.DataRetentionHook, time.Since(startTime), err)
return err
}

View file

@ -152,7 +152,7 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName,
startTime := time.Now()
err := cmd.Run()
providerLog(logger.LevelDebug, "executed command %#v, elapsed: %v, error: %v", config.Actions.Hook,
providerLog(logger.LevelDebug, "executed command %q, elapsed: %s, error: %v", config.Actions.Hook,
time.Since(startTime), err)
return err
}

View file

@ -857,7 +857,7 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
cnf.BackupsPath = getConfigPath(cnf.BackupsPath, basePath)
if cnf.BackupsPath == "" {
return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath)
return fmt.Errorf("required directory is invalid, backup path %q", cnf.BackupsPath)
}
absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)
if err != nil {
@ -936,7 +936,7 @@ func validateHooks() error {
for _, hook := range hooks {
if !filepath.IsAbs(hook) {
return fmt.Errorf("invalid hook: %#v must be an absolute path", hook)
return fmt.Errorf("invalid hook: %q must be an absolute path", hook)
}
_, err := os.Stat(hook)
if err != nil {
@ -1100,7 +1100,7 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco
}
if loginMethod == LoginMethodTLSCertificate {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return fmt.Errorf("certificate login method is not allowed for user %#v", user.User.Username)
return fmt.Errorf("certificate login method is not allowed for user %q", user.User.Username)
}
return nil
}
@ -1143,7 +1143,7 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
return user, loginMethod, err
}
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %q", user.Username)
}
if loginMethod == LoginMethodTLSCertificateAndPwd {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
@ -1577,7 +1577,7 @@ func DeleteShare(shareID string, executor, ipAddress, role string) error {
// ShareExists returns the share with the given ID if it exists
func ShareExists(shareID, username string) (Share, error) {
if shareID == "" {
return Share{}, util.NewRecordNotFoundError(fmt.Sprintf("Share %#v does not exist", shareID))
return Share{}, util.NewRecordNotFoundError(fmt.Sprintf("Share %q does not exist", shareID))
}
return provider.shareExists(shareID, username)
}
@ -1780,7 +1780,7 @@ func DeleteAPIKey(keyID string, executor, ipAddress, role string) error {
// APIKeyExists returns the API key with the given ID if it exists
func APIKeyExists(keyID string) (APIKey, error) {
if keyID == "" {
return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %#v does not exist", keyID))
return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %q does not exist", keyID))
}
return provider.apiKeyExists(keyID)
}
@ -2126,7 +2126,7 @@ func GetActiveTransfers(from time.Time) ([]ActiveTransfer, error) {
func AddSharedSession(session Session) error {
err := provider.addSharedSession(session)
if err != nil {
providerLog(logger.LevelError, "unable to add shared session, key %#v, type: %v, err: %v",
providerLog(logger.LevelError, "unable to add shared session, key %q, type: %v, err: %v",
session.Key, session.Type, err)
}
return err
@ -2136,7 +2136,7 @@ func AddSharedSession(session Session) error {
func DeleteSharedSession(key string) error {
err := provider.deleteSharedSession(key)
if err != nil {
providerLog(logger.LevelError, "unable to add shared session, key %#v, err: %v", key, err)
providerLog(logger.LevelError, "unable to add shared session, key %q, err: %v", key, err)
}
return err
}
@ -2487,10 +2487,10 @@ func buildUserHomeDir(user *User) {
func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
if folder.QuotaSize < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath))
return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath))
}
if folder.QuotaFiles < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath))
return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath))
}
if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) {
return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v",
@ -2537,7 +2537,7 @@ func validateUserGroups(user *User) error {
hasPrimary = true
}
if groupNames[g.Name] {
return util.NewValidationError(fmt.Sprintf("the group %#v is duplicated", g.Name))
return util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name))
}
groupNames[g.Name] = true
}
@ -2594,7 +2594,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
return util.NewValidationError("totp: config name is mandatory")
}
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
return util.NewValidationError(fmt.Sprintf("totp: config name %#v not found", c.ConfigName))
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
}
if c.Secret.IsEmpty() {
return util.NewValidationError("totp: secret is mandatory")
@ -2610,7 +2610,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
}
for _, protocol := range c.Protocols {
if !util.Contains(MFAProtocols, protocol) {
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %#v", protocol))
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
}
}
return nil
@ -2636,14 +2636,14 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
permissions := make(map[string][]string)
for dir, perms := range permsToCheck {
if len(perms) == 0 && dir == "/" {
return permissions, util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir))
return permissions, util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %q", dir))
}
if len(perms) > len(ValidPerms) {
return permissions, util.NewValidationError("invalid permissions")
}
for _, p := range perms {
if !util.Contains(ValidPerms, p) {
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %#v", p))
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
}
}
cleanedDir := filepath.ToSlash(path.Clean(dir))
@ -2651,10 +2651,10 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
cleanedDir = strings.TrimSuffix(cleanedDir, "/")
}
if !path.IsAbs(cleanedDir) {
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir))
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %q", dir))
}
if dir != cleanedDir && cleanedDir == "/" {
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir))
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
}
if util.Contains(perms, PermAny) {
permissions[cleanedDir] = []string{PermAny}
@ -2710,16 +2710,16 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, f := range baseFilters.FilePatterns {
cleanedPath := filepath.ToSlash(path.Clean(f.Path))
if !path.IsAbs(cleanedPath) {
return util.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path))
return util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path))
}
if util.Contains(filteredPaths, cleanedPath) {
return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path))
return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path))
}
if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 {
return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path))
return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %q", f.Path))
}
if f.DenyPolicy < sdk.DenyPolicyDefault || f.DenyPolicy > sdk.DenyPolicyHide {
return util.NewValidationError(fmt.Sprintf("invalid deny policy %v for path %#v", f.DenyPolicy, f.Path))
return util.NewValidationError(fmt.Sprintf("invalid deny policy %v for path %q", f.DenyPolicy, f.Path))
}
f.Path = cleanedPath
allowed := make([]string, 0, len(f.AllowedPatterns))
@ -2727,14 +2727,14 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, pattern := range f.AllowedPatterns {
_, err := path.Match(pattern, "abc")
if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern))
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern))
}
allowed = append(allowed, strings.ToLower(pattern))
}
for _, pattern := range f.DeniedPatterns {
_, err := path.Match(pattern, "abc")
if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern))
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern))
}
denied = append(denied, strings.ToLower(pattern))
}
@ -2767,14 +2767,14 @@ func validateIPFilters(filters *sdk.BaseUserFilters) error {
for _, IPMask := range filters.DeniedIP {
_, _, err := net.ParseCIDR(IPMask)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v: %v", IPMask, err))
return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %q: %v", IPMask, err))
}
}
filters.AllowedIP = util.RemoveDuplicates(filters.AllowedIP, false)
for _, IPMask := range filters.AllowedIP {
_, _, err := net.ParseCIDR(IPMask)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v: %v", IPMask, err))
return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %q: %v", IPMask, err))
}
}
return nil
@ -2787,7 +2787,7 @@ func validateBandwidthLimit(bl sdk.BandwidthLimit) error {
for _, source := range bl.Sources {
_, _, err := net.ParseCIDR(source)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse bandwidth limit source %#v: %v", source, err))
return util.NewValidationError(fmt.Sprintf("could not parse bandwidth limit source %q: %v", source, err))
}
}
return nil
@ -2817,7 +2817,7 @@ func validateTransferLimitsFilter(filters *sdk.BaseUserFilters) error {
for _, source := range limit.Sources {
_, _, err := net.ParseCIDR(source)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %#v: %v", source, err))
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %q: %v", source, err))
}
}
if limit.TotalDataTransfer > 0 {
@ -2843,13 +2843,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
}
for _, p := range filters.DeniedProtocols {
if !util.Contains(ValidProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %#v", p))
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
}
}
for _, p := range filters.TwoFactorAuthProtocols {
if !util.Contains(MFAProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %#v", p))
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
}
}
return nil
@ -2871,7 +2871,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
}
for _, loginMethod := range filters.DeniedLoginMethods {
if !util.Contains(ValidLoginMethods, loginMethod) {
return util.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod))
return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
}
}
if err := validateFilterProtocols(filters); err != nil {
@ -2879,12 +2879,12 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
}
if filters.TLSUsername != "" {
if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", filters.TLSUsername))
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
}
}
for _, opts := range filters.WebClient {
if !util.Contains(sdk.WebClientOptions, opts) {
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
}
}
updateFiltersValues(filters)
@ -2910,7 +2910,7 @@ func validateBaseParams(user *User) error {
return err
}
if user.Email != "" && !util.IsEmailValid(user.Email) {
return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email))
return util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email))
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
@ -2993,7 +2993,7 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
folder.MappedPath != "" {
cleanedMPath := filepath.Clean(folder.MappedPath)
if !filepath.IsAbs(cleanedMPath) {
return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath))
return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %q", folder.MappedPath))
}
folder.MappedPath = cleanedMPath
}
@ -3122,7 +3122,7 @@ func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certi
if user.Username == tlsCert.Subject.CommonName {
return *user, nil
}
return *user, fmt.Errorf("CN %#v does not match username %#v", tlsCert.Subject.CommonName, user.Username)
return *user, fmt.Errorf("CN %q does not match username %q", tlsCert.Subject.CommonName, user.Username)
}
return *user, errors.New("TLS certificate is not valid")
default:
@ -3156,7 +3156,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if !user.Filters.Hooks.CheckPasswordDisabled {
hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol)
if err != nil {
providerLog(logger.LevelDebug, "error executing check password hook for user %#v, ip %v, protocol %v: %v",
providerLog(logger.LevelDebug, "error executing check password hook for user %q, ip %v, protocol %v: %v",
user.Username, ip, protocol, err)
return *user, errors.New("unable to check credentials")
}
@ -3164,15 +3164,15 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
case -1:
// no hook configured
case 1:
providerLog(logger.LevelDebug, "password accepted by check password hook for user %#v, ip %v, protocol %v",
providerLog(logger.LevelDebug, "password accepted by check password hook for user %q, ip %v, protocol %v",
user.Username, ip, protocol)
return *user, nil
case 2:
providerLog(logger.LevelDebug, "partial success from check password hook for user %#v, ip %v, protocol %v",
providerLog(logger.LevelDebug, "partial success from check password hook for user %q, ip %v, protocol %v",
user.Username, ip, protocol)
password = hookResponse.ToVerify
default:
providerLog(logger.LevelDebug, "password rejected by check password hook for user %#v, ip %v, protocol %v, status: %v",
providerLog(logger.LevelDebug, "password rejected by check password hook for user %q, ip %v, protocol %v, status: %v",
user.Username, ip, protocol, hookResponse.Status)
return *user, ErrInvalidCredentials
}
@ -3193,13 +3193,13 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
// the TOTP passcode has six digits
pwdLen := len(password)
if pwdLen < 7 {
providerLog(logger.LevelDebug, "password len %v is too short to contain a passcode, user %#v, protocol %v",
providerLog(logger.LevelDebug, "password len %v is too short to contain a passcode, user %q, protocol %v",
pwdLen, user.Username, protocol)
return "", util.NewValidationError("password too short, cannot contain the passcode")
}
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v",
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return "", err
}
@ -3208,7 +3208,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,
user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil {
providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v",
providerLog(logger.LevelWarn, "invalid passcode for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return "", util.NewValidationError("invalid passcode")
}
@ -3384,7 +3384,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
}
err = user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v",
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return 0, err
}
@ -3398,7 +3398,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],
user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil {
providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v",
providerLog(logger.LevelWarn, "invalid passcode for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return 0, util.NewValidationError("invalid passcode")
}
@ -3504,26 +3504,26 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
if len(answers) == 1 && response.CheckPwd > 0 {
if response.CheckPwd == 2 {
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %#v",
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
user.Username)
return answers, errors.New("TOTP not enabled for SSH protocol")
}
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v",
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return answers, fmt.Errorf("unable to decrypt TOTP secret: %w", err)
}
match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],
user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil {
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to validate passcode for user %#v, match? %v, err: %v",
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to validate passcode for user %q, match? %v, err: %v",
user.Username, match, err)
return answers, errors.New("unable to validate TOTP passcode")
}
} else {
_, err = checkUserAndPass(user, answers[0], ip, protocol)
providerLog(logger.LevelInfo, "interactive auth hook requested password validation for user %#v, validation error: %v",
providerLog(logger.LevelInfo, "interactive auth hook requested password validation for user %q, validation error: %v",
user.Username, err)
if err != nil {
return answers, err
@ -3563,9 +3563,9 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
cmd := exec.CommandContext(ctx, authHook, args...)
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))
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", user.Username),
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", user.Password))
stdout, err := cmd.StdoutPipe()
if err != nil {
return authResult, err
@ -3609,7 +3609,7 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
go func() {
_, err := cmd.Process.Wait()
if err != nil {
providerLog(logger.LevelWarn, "error waiting for #%v process to exit: %v", authHook, err)
providerLog(logger.LevelWarn, "error waiting for %q process to exit: %v", authHook, err)
}
}()
@ -3696,12 +3696,12 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
cmd := exec.CommandContext(ctx, config.CheckPasswordHook, args...)
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),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol),
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
)
return cmd.Output()
return getCmdOutput(cmd, "check_password_hook")
}
func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) {
@ -3728,7 +3728,7 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
var result []byte
url, err := url.Parse(config.PreLoginHook)
if err != nil {
providerLog(logger.LevelError, "invalid url for pre-login hook %#v, error: %v", config.PreLoginHook, err)
providerLog(logger.LevelError, "invalid url for pre-login hook %q, error: %v", config.PreLoginHook, err)
return result, err
}
q := url.Query()
@ -3757,12 +3757,12 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
cmd := exec.CommandContext(ctx, config.PreLoginHook, args...)
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),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol),
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol),
)
return cmd.Output()
return getCmdOutput(cmd, "pre_login_hook")
}
func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {
@ -3776,15 +3776,15 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
startTime := time.Now()
out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON)
if err != nil {
return u, fmt.Errorf("pre-login hook error: %v, username %#v, ip %v, protocol %v elapsed %v",
return u, fmt.Errorf("pre-login hook error: %v, username %q, ip %v, protocol %v elapsed %v",
err, username, ip, protocol, time.Since(startTime))
}
providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime))
providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %s", time.Since(startTime))
if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v",
providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %q id: %d",
username, u.ID)
if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
}
return u, nil
}
@ -3805,7 +3805,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
recoveryCodes := u.Filters.RecoveryCodes
err = json.Unmarshal(out, &u)
if err != nil {
return u, fmt.Errorf("invalid pre-login hook response %#v, error: %v", string(out), err)
return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", string(out), err)
}
u.ID = userID
u.UsedQuotaSize = userUsedQuotaSize
@ -3836,7 +3836,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
if err != nil {
return u, err
}
providerLog(logger.LevelDebug, "user %#v added/updated from pre-login hook response, id: %v", username, userID)
providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, userID)
if userID == 0 {
return provider.userExists(username, "")
}
@ -3876,7 +3876,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
var url *url.URL
url, err := url.Parse(config.PostLoginHook)
if err != nil {
providerLog(logger.LevelDebug, "Invalid post-login hook %#v", config.PostLoginHook)
providerLog(logger.LevelDebug, "Invalid post-login hook %q", config.PostLoginHook)
return
}
q := url.Query()
@ -3893,7 +3893,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
respCode = resp.StatusCode
resp.Body.Close()
}
providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, response code: %v, elapsed: %v err: %v",
providerLog(logger.LevelDebug, "post login hook executed for user %q, ip %v, protocol %v, response code: %v, elapsed: %v err: %v",
user.Username, ip, protocol, respCode, time.Since(startTime), err)
return
}
@ -3903,14 +3903,14 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
cmd := exec.CommandContext(ctx, config.PostLoginHook, args...)
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),
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%v", status),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol))
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%s", status),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol))
startTime := time.Now()
err = cmd.Run()
providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, elapsed %v err: %v",
providerLog(logger.LevelDebug, "post login hook executed for user %q, ip %v, protocol %v, elapsed %v err: %v",
user.Username, ip, protocol, time.Since(startTime), err)
}()
}
@ -3971,34 +3971,16 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)
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),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password),
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol),
fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%v", strings.ReplaceAll(tlsCert, "\n", "\\n")),
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_AUTHD_USER=%s", string(userAsJSON)),
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%s", pkey),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%s", strings.ReplaceAll(tlsCert, "\n", "\\n")),
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
var stdout bytes.Buffer
cmd.Stdout = &stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
in := bufio.NewScanner(stderr)
for in.Scan() {
logger.Log(logger.LevelWarn, "external_auth_hook", "", "%s", in.Text())
}
return stdout.Bytes(), cmd.Wait()
return getCmdOutput(cmd, "external_auth_hook")
}
func updateUserFromExtAuthResponse(user *User, password, pkey string) {
@ -4037,14 +4019,14 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
startTime := time.Now()
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, u)
if err != nil {
return user, fmt.Errorf("external auth error for user %#v: %v, elapsed: %v", username, err, time.Since(startTime))
return user, fmt.Errorf("external auth error for user %q, elapsed: %s: %w", username, time.Since(startTime), err)
}
providerLog(logger.LevelDebug, "external auth completed for user %#v, elapsed: %v", username, time.Since(startTime))
providerLog(logger.LevelDebug, "external auth completed for user %q, elapsed: %s", username, time.Since(startTime))
if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v",
providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %q, id: %d",
username, u.ID)
if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
}
return u, nil
}
@ -4121,16 +4103,16 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
if err != nil {
return user, fmt.Errorf("plugin auth error for user %#v: %v, elapsed: %v, auth scope: %v",
return user, fmt.Errorf("plugin auth error for user %q: %v, elapsed: %v, auth scope: %v",
username, err, time.Since(startTime), authScope)
}
providerLog(logger.LevelDebug, "plugin auth completed for user %#v, elapsed: %v,auth scope: %v",
providerLog(logger.LevelDebug, "plugin auth completed for user %q, elapsed: %v,auth scope: %v",
username, time.Since(startTime), authScope)
if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %#v id: %v",
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %q id: %v",
username, u.ID)
if u.ID == 0 {
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
}
return u, nil
}
@ -4228,6 +4210,30 @@ func checkReservedUsernames(username string) error {
return nil
}
func getCmdOutput(cmd *exec.Cmd, sender string) ([]byte, error) {
var stdout bytes.Buffer
cmd.Stdout = &stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
if out := scanner.Text(); out != "" {
logger.Log(logger.LevelWarn, sender, "", out)
}
}
err = cmd.Wait()
return stdout.Bytes(), err
}
func providerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, logSender, "", format, v...)
}