diff --git a/README.md b/README.md index 0ff507db..da813496 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Regularly the test cases are manually executed and pass on Windows. Other UNIX v ## Requirements - Go 1.12 or higher -- A suitable SQL server to use as data provider: MySQL (4.1+) or SQLite 3.x or PostreSQL (9+) +- A suitable SQL server to use as data provider: PostreSQL (9+) or MySQL (4.1+) or SQLite 3.x ## Installation @@ -91,7 +91,7 @@ The `sftpgo.conf` configuration file contains the following sections: - `track_quota`, integer. Set the preferred way to track users quota between the following choices: - 0, disable quota tracking. REST API to scan user dir and update quota will do nothing - 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions - - 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration you can still use the "quota scan" REST API to periodically update space usage for users without quota restrictions + - 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions - **"httpd"**, the configuration for the HTTP server used to serve REST API - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080 - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1" @@ -140,9 +140,9 @@ For each account the following properties can be configured: - `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt too. Currently, as fallback, there is a clear text password checking but you should not store passwords as clear text and this support could be removed at any time, so please don't depend on it. - `public_key` used for public key authentication. At least one between password and public key is mandatory - `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path -- `uid`, `gid`. If sftpgo runs as root then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo. +- `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo. - `max_sessions` maximum concurrent sessions. 0 means unlimited -- `quota_size` maximum size allowed. 0 means unlimited +- `quota_size` maximum size allowed as bytes. 0 means unlimited - `quota_files` maximum number of files allowed. 0 means unlimited - `permissions` the following permissions are supported: - `*` all permission are granted @@ -152,7 +152,7 @@ For each account the following properties can be configured: - `delete` delete files or directories is allowed - `rename` rename files or directories is allowed - `create_dirs` create directories is allowed - - `create_symlinks` create links is allowed + - `create_symlinks` create symbolic links is allowed - `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited - `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited diff --git a/api/api.go b/api/api.go index 36fb5b49..c6cb253f 100644 --- a/api/api.go +++ b/api/api.go @@ -1,3 +1,8 @@ +// Package api implements REST API for sftpgo. +// REST API allows to manage users and quota and to get real time reports for the active connections +// with possibility of forcibly closing a connection. +// The OpenAPI 3 schema for the exposed API can be found inside the source tree: +// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml package api import ( @@ -22,7 +27,9 @@ var ( // HTTPDConf httpd daemon configuration type HTTPDConf struct { - BindPort int `json:"bind_port"` + // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080 + BindPort int `json:"bind_port"` + // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" BindAddress string `json:"bind_address"` } @@ -36,7 +43,7 @@ func init() { initializeRouter() } -// SetDataProvider sets the data provider +// SetDataProvider sets the data provider to use to fetch the data about users func SetDataProvider(provider dataprovider.Provider) { dataProvider = provider } diff --git a/api/api_utils.go b/api/api_utils.go index 693f3bfe..d3e7a539 100644 --- a/api/api_utils.go +++ b/api/api_utils.go @@ -22,18 +22,19 @@ var ( httpBaseURL = "http://127.0.0.1:8080" ) -// SetBaseURL sets the url to use for HTTP request, default is "http://127.0.0.1:8080" +// SetBaseURL sets the base url to use for HTTP requests, default is "http://127.0.0.1:8080" func SetBaseURL(url string) { httpBaseURL = url } +// gets an HTTP Client with a timeout func getHTTPClient() *http.Client { return &http.Client{ Timeout: 15 * time.Second, } } -// AddUser add a new user, useful for tests +// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode. func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, error) { var newUser dataprovider.User userAsJSON, err := json.Marshal(user) @@ -58,7 +59,7 @@ func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, return newUser, err } -// UpdateUser update an user, useful for tests +// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode. func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, error) { var newUser dataprovider.User userAsJSON, err := json.Marshal(user) @@ -87,7 +88,7 @@ func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.Us return newUser, err } -// RemoveUser remove user, useful for tests +// RemoveUser removes an existing user and checks the received HTTP Status code against expectedStatusCode. func RemoveUser(user dataprovider.User, expectedStatusCode int) error { req, err := http.NewRequest(http.MethodDelete, httpBaseURL+userPath+"/"+strconv.FormatInt(user.ID, 10), nil) if err != nil { @@ -101,7 +102,7 @@ func RemoveUser(user dataprovider.User, expectedStatusCode int) error { return checkResponse(resp.StatusCode, expectedStatusCode, resp) } -// GetUserByID get user by id, useful for tests +// GetUserByID gets an user by database id and checks the received HTTP Status code against expectedStatusCode. func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, error) { var user dataprovider.User resp, err := getHTTPClient().Get(httpBaseURL + userPath + "/" + strconv.FormatInt(userID, 10)) @@ -116,7 +117,10 @@ func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, error return user, err } -// GetUsers useful for tests +// GetUsers allows to get a list of users and checks the received HTTP Status code against expectedStatusCode. +// The number of results can be limited specifying a limit. +// Some results can be skipped specifying an offset. +// The results can be filtered specifying an username, the username filter is an exact match func GetUsers(limit int64, offset int64, username string, expectedStatusCode int) ([]dataprovider.User, error) { var users []dataprovider.User url, err := url.Parse(httpBaseURL + userPath) @@ -146,7 +150,7 @@ func GetUsers(limit int64, offset int64, username string, expectedStatusCode int return users, err } -// GetQuotaScans get active quota scans, useful for tests +// GetQuotaScans gets active quota scans and checks the received HTTP Status code against expectedStatusCode. func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, error) { var quotaScans []sftpd.ActiveQuotaScan resp, err := getHTTPClient().Get(httpBaseURL + quotaScanPath) @@ -161,7 +165,7 @@ func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, error) { return quotaScans, err } -// StartQuotaScan start a new quota scan +// StartQuotaScan start a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode. func StartQuotaScan(user dataprovider.User, expectedStatusCode int) error { userAsJSON, err := json.Marshal(user) if err != nil { diff --git a/api/router.go b/api/router.go index 22bebde1..b13d3516 100644 --- a/api/router.go +++ b/api/router.go @@ -10,7 +10,7 @@ import ( "github.com/go-chi/render" ) -// GetHTTPRouter returns the configured HTTP router +// GetHTTPRouter returns the configured HTTP handler func GetHTTPRouter() http.Handler { return router } diff --git a/config/config.go b/config/config.go index 48935bbf..26d2bc5e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,8 @@ +// Package config manages the configuration. +// Configuration is loaded from sftpgo.conf file. +// If sftpgo.conf is not found or cannot be readed or decoded as json the default configuration is used. +// The default configuration an be found inside the source tree: +// https://github.com/drakkan/sftpgo/blob/master/sftpgo.conf package config import ( @@ -62,22 +67,22 @@ func init() { } } -// GetSFTPDConfig returns sftpd configuration +// GetSFTPDConfig returns the configuration for the SFTP server func GetSFTPDConfig() sftpd.Configuration { return globalConf.SFTPD } -// GetHTTPDConfig returns httpd configuration +// GetHTTPDConfig returns the configuration for the HTTP server func GetHTTPDConfig() api.HTTPDConf { return globalConf.HTTPDConfig } -//GetProviderConf returns data provider configuration +//GetProviderConf returns the configuration for the data provider func GetProviderConf() dataprovider.Config { return globalConf.ProviderConf } -// LoadConfig loads configuration from sftpgo.conf +// LoadConfig loads the configuration from sftpgo.conf or use the default configuration. func LoadConfig(configPath string) error { logger.Debug(logSender, "load config from path: %v", configPath) file, err := os.Open(configPath) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index a6d90308..f6011771 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -1,3 +1,6 @@ +// Package dataprovider provides data access. +// It abstract different data providers and exposes a common API. +// Currently the supported data providers are: PostreSQL (9+), MySQL (4.1+) and SQLite 3.x package dataprovider import ( @@ -28,7 +31,7 @@ const ( ) var ( - // SupportedProviders data provider in config file must be one of these strings + // SupportedProviders data provider configured in the sftpgo.conf file must match of these strings SupportedProviders = []string{SQLiteDataProviderName, PGSSQLDataProviderName, MySQLDataProviderName} dbHandle *sql.DB config Config @@ -40,17 +43,38 @@ var ( // Config provider configuration type Config struct { - Driver string `json:"driver"` - Name string `json:"name"` - Host string `json:"host"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` + // Driver name, must be one of the SupportedProviders + Driver string `json:"driver"` + // Database name + Name string `json:"name"` + // Database host + Host string `json:"host"` + // Database port + Port int `json:"port"` + // Database username + Username string `json:"username"` + // Database password + Password string `json:"password"` + // Used for drivers mysql and postgresql. + // 0 disable SSL/TLS connections. + // 1 require ssl. + // 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql. + // 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql. + SSLMode int `json:"sslmode"` + // Custom database connection string. + // If not empty this connection string will be used instead of build one using the previous parameters ConnectionString string `json:"connection_string"` - UsersTable string `json:"users_table"` - ManageUsers int `json:"manage_users"` - SSLMode int `json:"sslmode"` - TrackQuota int `json:"track_quota"` + // Database table for SFTP users + UsersTable string `json:"users_table"` + // Set to 0 to disable users management, 1 to enable + ManageUsers int `json:"manage_users"` + // Set the preferred way to track users quota between the following choices: + // 0, disable quota tracking. REST API to scan user dir and update quota will do nothing + // 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions + // 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. + // With this configuration the "quota scan" REST API can still be used to periodically update space usage + // for users without quota restrictions + TrackQuota int `json:"track_quota"` } // ValidationError raised if input data is not valid @@ -58,25 +82,29 @@ type ValidationError struct { err string } +// Validation error details func (e *ValidationError) Error() string { return fmt.Sprintf("Validation error: %s", e.err) } -// MethodDisabledError raised if a method is disable in config file +// MethodDisabledError raised if a method is disabled in config file. +// For example, if user management is disabled, this error is raised +// every time an user operation is done using the REST API type MethodDisabledError struct { err string } +// Method disabled error details func (e *MethodDisabledError) Error() string { return fmt.Sprintf("Method disabled error: %s", e.err) } -// GetProvider get the configured provider +// GetProvider returns the configured provider func GetProvider() Provider { return provider } -// Provider interface for data providers +// Provider interface that data providers must implement. type Provider interface { validateUserAndPass(username string, password string) (User, error) validateUserAndPubKey(username string, pubKey string) (User, error) @@ -90,7 +118,8 @@ type Provider interface { getUserByID(ID int64) (User, error) } -// Initialize auth provider +// Initialize the data provider. +// An error is returned if the configured driver is invalid or if the data provider cannot be initialized func Initialize(cnf Config, basePath string) error { config = cnf sqlPlaceholders = getSQLPlaceholders() @@ -107,17 +136,18 @@ func Initialize(cnf Config, basePath string) error { return fmt.Errorf("Unsupported data provider: %v", config.Driver) } -// CheckUserAndPass returns the user with the given username and password if exists +// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error func CheckUserAndPass(p Provider, username string, password string) (User, error) { return p.validateUserAndPass(username, password) } -// CheckUserAndPubKey returns the user with the given username and public key if exists +// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error func CheckUserAndPubKey(p Provider, username string, pubKey string) (User, error) { return p.validateUserAndPubKey(username, pubKey) } -// UpdateUserQuota update the quota for the given user +// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd. +// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference. func UpdateUserQuota(p Provider, user User, filesAdd int, sizeAdd int64, reset bool) error { if config.TrackQuota == 0 { return &MethodDisabledError{err: trackQuotaDisabledError} @@ -127,7 +157,8 @@ func UpdateUserQuota(p Provider, user User, filesAdd int, sizeAdd int64, reset b return p.updateQuota(user.Username, filesAdd, sizeAdd, reset) } -// GetUsedQuota returns the used quota for the given user +// GetUsedQuota returns the used quota for the given SFTP user. +// TrackQuota must be >=1 to enable this method func GetUsedQuota(p Provider, username string) (int, int64, error) { if config.TrackQuota == 0 { return 0, 0, &MethodDisabledError{err: trackQuotaDisabledError} @@ -135,12 +166,13 @@ func GetUsedQuota(p Provider, username string) (int, int64, error) { return p.getUsedQuota(username) } -// UserExists checks if the given username exists +// UserExists checks if the given SFTP username exists, returns an error if no match is found func UserExists(p Provider, username string) (User, error) { return p.userExists(username) } -// AddUser adds a new user, ManageUsers configuration must be set to 1 to enable this method +// AddUser adds a new SFTP user. +// ManageUsers configuration must be set to 1 to enable this method func AddUser(p Provider, user User) error { if config.ManageUsers == 0 { return &MethodDisabledError{err: manageUsersDisabledError} @@ -148,7 +180,8 @@ func AddUser(p Provider, user User) error { return p.addUser(user) } -// UpdateUser updates an existing user, ManageUsers configuration must be set to 1 to enable this method +// UpdateUser updates an existing SFTP user. +// ManageUsers configuration must be set to 1 to enable this method func UpdateUser(p Provider, user User) error { if config.ManageUsers == 0 { return &MethodDisabledError{err: manageUsersDisabledError} @@ -156,7 +189,8 @@ func UpdateUser(p Provider, user User) error { return p.updateUser(user) } -// DeleteUser deletes an existing user, ManageUsers configuration must be set to 1 to enable this method +// DeleteUser deletes an existing SFTP user. +// ManageUsers configuration must be set to 1 to enable this method func DeleteUser(p Provider, user User) error { if config.ManageUsers == 0 { return &MethodDisabledError{err: manageUsersDisabledError} @@ -164,12 +198,12 @@ func DeleteUser(p Provider, user User) error { return p.deleteUser(user) } -// GetUsers returns an array of users respecting limit and offset +// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty func GetUsers(p Provider, limit int, offset int, order string, username string) ([]User, error) { return p.getUsers(limit, offset, order, username) } -// GetUserByID returns the user with the given ID +// GetUserByID returns the user with the given database ID if a match is found or an error func GetUserByID(p Provider, ID int64) (User, error) { return p.getUserByID(ID) } diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index dda6553d..e6986f68 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -9,7 +9,7 @@ import ( "github.com/drakkan/sftpgo/logger" ) -// MySQLProvider auth provider for sqlite database +// MySQLProvider auth provider for MySQL/MariaDB database type MySQLProvider struct { } diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index e1f0b690..e14f7df6 100644 --- a/dataprovider/sqlite.go +++ b/dataprovider/sqlite.go @@ -10,7 +10,7 @@ import ( "github.com/drakkan/sftpgo/logger" ) -// SQLiteProvider auth provider for sqlite database +// SQLiteProvider auth provider for SQLite database type SQLiteProvider struct { } diff --git a/dataprovider/user.go b/dataprovider/user.go index c6cb2d58..2a4570fc 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -7,36 +7,64 @@ import ( "github.com/drakkan/sftpgo/utils" ) -// Permissions +// Available permissions for SFTP users const ( - PermAny = "*" - PermListItems = "list" - PermDownload = "download" - PermUpload = "upload" - PermDelete = "delete" - PermRename = "rename" - PermCreateDirs = "create_dirs" + // All permissions are granted + PermAny = "*" + // List items such as files and directories is allowed + PermListItems = "list" + // download files is allowed + PermDownload = "download" + // upload files is allowed + PermUpload = "upload" + // delete files or directories is allowed + PermDelete = "delete" + // rename files or directories is allowed + PermRename = "rename" + // create directories is allowed + PermCreateDirs = "create_dirs" + // create symbolic links is allowed PermCreateSymlinks = "create_symlinks" ) // User defines an SFTP user type User struct { - ID int64 `json:"id"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - PublicKey string `json:"public_key,omitempty"` - HomeDir string `json:"home_dir"` - UID int `json:"uid"` - GID int `json:"gid"` - MaxSessions int `json:"max_sessions"` - QuotaSize int64 `json:"quota_size"` - QuotaFiles int `json:"quota_files"` - Permissions []string `json:"permissions"` - UsedQuotaSize int64 `json:"used_quota_size"` - UsedQuotaFiles int `json:"used_quota_files"` - LastQuotaUpdate int64 `json:"last_quota_update"` - UploadBandwidth int64 `json:"upload_bandwidth"` - DownloadBandwidth int64 `json:"download_bandwidth"` + // Database unique identifier + ID int64 `json:"id"` + // Username + Username string `json:"username"` + // Password used for password authentication. + // For users created using SFTPGo REST API the password is be stored using argon2id hashing algo. + // Checking passwords stored with bcrypt is supported too. + // Currently, as fallback, there is a clear text password checking but you should not store passwords + // as clear text and this support could be removed at any time, so please don't depend on it. + Password string `json:"password,omitempty"` + // PublicKey used for public key authentication. At least one between password and public key is mandatory + PublicKey string `json:"public_key,omitempty"` + // The user cannot upload or download files outside this directory. Must be an absolute path + HomeDir string `json:"home_dir"` + // If sftpgo runs as root system user then the created files and directories will be assigned to this system UID + UID int `json:"uid"` + // If sftpgo runs as root system user then the created files and directories will be assigned to this system GID + GID int `json:"gid"` + // Maximum concurrent sessions. 0 means unlimited + MaxSessions int `json:"max_sessions"` + // Maximum size allowed as bytes. 0 means unlimited + QuotaSize int64 `json:"quota_size"` + // Maximum number of files allowed. 0 means unlimited + QuotaFiles int `json:"quota_files"` + // List of the granted permissions + Permissions []string `json:"permissions"` + // Used quota as bytes + UsedQuotaSize int64 `json:"used_quota_size"` + // Used quota as number of files + UsedQuotaFiles int `json:"used_quota_files"` + // Last quota update as unix timestamp in milliseconds + LastQuotaUpdate int64 `json:"last_quota_update"` + // Maximum upload bandwidth as KB/s, 0 means unlimited + UploadBandwidth int64 `json:"upload_bandwidth"` + // Maximum download bandwidth as KB/s, 0 means unlimited + DownloadBandwidth int64 `json:"download_bandwidth"` } // HasPerm returns true if the user has the given permission or any permission @@ -47,22 +75,12 @@ func (u *User) HasPerm(permission string) bool { return utils.IsStringInSlice(permission, u.Permissions) } -// HasOption returns true if the user has the give option -/*func (u *User) HasOption(option string) bool { - return utils.IsStringInSlice(option, u.Options) -}*/ - -// GetPermissionsAsJSON returns the permission as json byte array +// GetPermissionsAsJSON returns the permissions as json byte array func (u *User) GetPermissionsAsJSON() ([]byte, error) { return json.Marshal(u.Permissions) } -// GetOptionsAsJSON returns the permission as json byte array -/*func (u *User) GetOptionsAsJSON() ([]byte, error) { - return json.Marshal(u.Options) -}*/ - -// GetUID returns a validate uid +// GetUID returns a validate uid, suitable for use with os.Chown func (u *User) GetUID() int { if u.UID <= 0 || u.UID > 65535 { return -1 @@ -70,7 +88,7 @@ func (u *User) GetUID() int { return u.UID } -// GetGID returns a validate gid +// GetGID returns a validate gid, suitable for use with os.Chown func (u *User) GetGID() int { if u.GID <= 0 || u.GID > 65535 { return -1 @@ -78,13 +96,12 @@ func (u *User) GetGID() int { return u.GID } -// GetHomeDir returns user home dir cleaned +// GetHomeDir returns the shortest path name equivalent to the user's home directory func (u *User) GetHomeDir() string { return filepath.Clean(u.HomeDir) } -// HasQuotaRestrictions returns true if there is a quota restriction on number of files -// or size or both +// HasQuotaRestrictions returns true if there is a quota restriction on number of files or size or both func (u *User) HasQuotaRestrictions() bool { return u.QuotaFiles > 0 || u.QuotaSize > 0 } diff --git a/logger/logger.go b/logger/logger.go index 62f6cd6b..c90082b9 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,3 +1,9 @@ +// Package logger provides logging capabilities. +// It is a wrapper around zerolog for logging and lumberjack for log rotation. +// It provides a request logger to log the HTTP requests for REST API too. +// The request logger uses chi.middleware.RequestLogger, +// chi.middleware.LogFormatter and chi.middleware.LogEntry to build a structured +// logger using zerlog package logger import ( @@ -15,12 +21,13 @@ var ( logger zerolog.Logger ) -// GetLogger get the logger instance +// GetLogger get the configured logger instance func GetLogger() *zerolog.Logger { return &logger } -// InitLogger initialize loggers +// InitLogger configures the logger. +// It sets the log file path and the log level func InitLogger(logFilePath string, level zerolog.Level) { logMaxSize := 10 // MB logMaxBackups := 5 @@ -36,22 +43,22 @@ func InitLogger(logFilePath string, level zerolog.Level) { }).With().Timestamp().Logger().Level(level) } -// Debug log at debug level for sender +// Debug logs at debug level for the specified sender func Debug(sender string, format string, v ...interface{}) { logger.Debug().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) } -// Info log at info level for sender +// Info logs at info level for the specified sender func Info(sender string, format string, v ...interface{}) { logger.Info().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) } -// Warn log at warn level for sender +// Warn logs at warn level for the specified sender func Warn(sender string, format string, v ...interface{}) { logger.Warn().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) } -// Error log at error level for sender +// Error logs at error level for the specified sender func Error(sender string, format string, v ...interface{}) { logger.Error().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) } @@ -68,7 +75,7 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user Msg("") } -// CommandLog log an SFTP command +// CommandLog logs an SFTP command func CommandLog(command string, path string, target string, user string, connectionID string) { logger.Info(). Str("sender", command). diff --git a/logger/request_logger.go b/logger/request_logger.go index 9fd4575f..a21cdb3a 100644 --- a/logger/request_logger.go +++ b/logger/request_logger.go @@ -9,23 +9,28 @@ import ( "github.com/rs/zerolog" ) -// StructuredLogger that uses zerolog +// StructuredLogger defines a simple wrapper around zerolog logger. +// It implements chi.middleware.LogFormatter interface type StructuredLogger struct { Logger *zerolog.Logger } -// StructuredLoggerEntry using zerolog logger +// StructuredLoggerEntry defines a log entry. +// It implements chi.middleware.LogEntry interface type StructuredLoggerEntry struct { + // The zerolog logger Logger *zerolog.Logger + // fields to write in the log fields map[string]interface{} } -// NewStructuredLogger returns RequestLogger +// NewStructuredLogger returns a chi.middleware.RequestLogger using our StructuredLogger. +// This structured logger is called by the chi.middleware.Logger handler to log each HTTP request func NewStructuredLogger(logger *zerolog.Logger) func(next http.Handler) http.Handler { return middleware.RequestLogger(&StructuredLogger{logger}) } -// NewLogEntry creates a new log entry +// NewLogEntry creates a new log entry for an HTTP request func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { scheme := "http" if r.TLS != nil { @@ -47,7 +52,7 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { return &StructuredLoggerEntry{Logger: l.Logger, fields: fields} } -// Write a new entry +// Write logs a new entry at the end of the HTTP request func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) { l.Logger.Info().Fields(l.fields).Int( "resp_status", status).Int( diff --git a/main.go b/main.go index e3723721..e8efc457 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,6 @@ +// Full featured and highly configurable SFTP server. +// For more details about features, installation, configuration and usage please refer to the README inside the source tree: +// https://github.com/drakkan/sftpgo/blob/master/README.md package main // import "github.com/drakkan/sftpgo" import ( diff --git a/sftpd/handler.go b/sftpd/handler.go index 803a0ef5..8fb9ff32 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -22,14 +22,20 @@ import ( // Connection details for an authenticated user type Connection struct { - ID string - User dataprovider.User + // Unique identifier for the connection + ID string + // logged in user's details + User dataprovider.User + // client's version string ClientVersion string - RemoteAddr net.Addr - StartTime time.Time - lastActivity time.Time - lock *sync.Mutex - sshConn *ssh.ServerConn + // Remote address for this connection + RemoteAddr net.Addr + // start time for this connection + StartTime time.Time + // last activity for this connection + lastActivity time.Time + lock *sync.Mutex + sshConn *ssh.ServerConn } // Fileread creates a reader for a file on the system and returns the reader back. @@ -273,7 +279,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) { return nil, sftp.ErrSshFxFailure } - return ListerAt(files), nil + return listerAt(files), nil case "Stat": if !c.User.HasPerm(dataprovider.PermListItems) { return nil, sftp.ErrSshFxPermissionDenied @@ -288,7 +294,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) { return nil, sftp.ErrSshFxFailure } - return ListerAt([]os.FileInfo{s}), nil + return listerAt([]os.FileInfo{s}), nil default: return nil, sftp.ErrSshFxOpUnsupported } diff --git a/sftpd/lister.go b/sftpd/lister.go index 8429f94f..5ec491d6 100644 --- a/sftpd/lister.go +++ b/sftpd/lister.go @@ -5,12 +5,11 @@ import ( "os" ) -// ListerAt .. -type ListerAt []os.FileInfo +type listerAt []os.FileInfo // ListAt returns the number of entries copied and an io.EOF error if we made it to the end of the file list. // Take a look at the pkg/sftp godoc for more information about how this function should work. -func (l ListerAt) ListAt(f []os.FileInfo, offset int64) (int, error) { +func (l listerAt) ListAt(f []os.FileInfo, offset int64) (int, error) { if offset >= int64(len(l)) { return 0, io.EOF } diff --git a/sftpd/server.go b/sftpd/server.go index e61bb621..f6329584 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -25,15 +25,24 @@ import ( "golang.org/x/crypto/ssh" ) -// Configuration server configuration +// Configuration for the SFTP server type Configuration struct { - Banner string `json:"banner"` - BindPort int `json:"bind_port"` - BindAddress string `json:"bind_address"` - IdleTimeout int `json:"idle_timeout"` - MaxAuthTries int `json:"max_auth_tries"` - Umask string `json:"umask"` - Actions Actions `json:"actions"` + // Identification string used by the server + Banner string `json:"banner"` + // The port used for serving SFTP requests + BindPort int `json:"bind_port"` + // The address to listen on. A blank value means listen on all available network interfaces. + BindAddress string `json:"bind_address"` + // Maximum idle timeout as minutes. If a client is idle for a time that exceeds this setting it will be disconnected + IdleTimeout int `json:"idle_timeout"` + // Maximum number of authentication attempts permitted per connection. + // If set to a negative number, the number of attempts are unlimited. + // If set to zero, the number of attempts are limited to 6. + MaxAuthTries int `json:"max_auth_tries"` + // Umask for new files + Umask string `json:"umask"` + // Actions to execute on SFTP create, download, delete and rename + Actions Actions `json:"actions"` } // Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections. @@ -108,7 +117,7 @@ func (c Configuration) Initialize(configDir string) error { } } -// AcceptInboundConnection handles an inbound connection to the instance and determines if we should serve the request or not. +// AcceptInboundConnection handles an inbound connection to the server instance and determines if the request should be served or not. func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) { defer conn.Close() diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index c4b144cc..c53b6c61 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -1,3 +1,6 @@ +// Package sftpd implements the SSH File Transfer Protocol as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02. +// It uses pkg/sftp library: +// https://github.com/pkg/sftp package sftpd import ( @@ -48,29 +51,41 @@ type connectionTransfer struct { LastActivity int64 `json:"last_activity"` } -// ActiveQuotaScan username and start data for a quota scan +// ActiveQuotaScan defines an active quota scan type ActiveQuotaScan struct { - Username string `json:"username"` - StartTime int64 `json:"start_time"` + // Username to which the quota scan refers + Username string `json:"username"` + // quota scan start time as unix timestamp in milliseconds + StartTime int64 `json:"start_time"` } -// Actions configuration for external script to execute on create, download, delete. -// A rename trigger delete script for the old file and create script for the new one +// Actions to execute on SFTP create, download, delete and rename. +// An external command can be executed and/or an HTTP notification can be fired type Actions struct { - ExecuteOn []string `json:"execute_on"` - Command string `json:"command"` - HTTPNotificationURL string `json:"http_notification_url"` + // Valid values are download, upload, delete, rename. Empty slice to disable + ExecuteOn []string `json:"execute_on"` + // Absolute path to the command to execute, empty to disable + Command string `json:"command"` + // The URL to notify using an HTTP GET, empty to disable + HTTPNotificationURL string `json:"http_notification_url"` } // ConnectionStatus status for an active connection type ConnectionStatus struct { - Username string `json:"username"` - ConnectionID string `json:"connection_id"` - ClientVersion string `json:"client_version"` - RemoteAddress string `json:"remote_address"` - ConnectionTime int64 `json:"connection_time"` - LastActivity int64 `json:"last_activity"` - Transfers []connectionTransfer `json:"active_transfers"` + // Logged in username + Username string `json:"username"` + // Unique identifier for the connection + ConnectionID string `json:"connection_id"` + // client's version string + ClientVersion string `json:"client_version"` + // Remote address for this connection + RemoteAddress string `json:"remote_address"` + // Connection time as unix timestamp in milliseconds + ConnectionTime int64 `json:"connection_time"` + // Last activity as unix timestamp in milliseconds + LastActivity int64 `json:"last_activity"` + // active uploads/downloads + Transfers []connectionTransfer `json:"active_transfers"` } func init() { @@ -78,7 +93,7 @@ func init() { idleConnectionTicker = time.NewTicker(5 * time.Minute) } -// SetDataProvider sets the data provider +// SetDataProvider sets the data provider to use to authenticate users and to get/update their disk quota func SetDataProvider(provider dataprovider.Provider) { dataProvider = provider } @@ -121,7 +136,7 @@ func AddQuotaScan(username string) bool { return true } -// RemoveQuotaScan remove and user from the ones with active quota scans +// RemoveQuotaScan removes an user from the ones with active quota scans func RemoveQuotaScan(username string) error { mutex.Lock() defer mutex.Unlock() @@ -143,7 +158,8 @@ func RemoveQuotaScan(username string) error { return err } -// CloseActiveConnection close an active SFTP connection, returns true on success +// CloseActiveConnection closes an active SFTP connection. +// It returns true on success func CloseActiveConnection(connectionID string) bool { result := false mutex.RLock() @@ -212,7 +228,7 @@ func startIdleTimer(maxIdleTime time.Duration) { }() } -// CheckIdleConnections disconnects idle clients +// CheckIdleConnections disconnects clients idle for too long, based on IdleTimeout setting func CheckIdleConnections() { mutex.RLock() defer mutex.RUnlock() diff --git a/sftpd/transfer.go b/sftpd/transfer.go index 558ed728..42087690 100644 --- a/sftpd/transfer.go +++ b/sftpd/transfer.go @@ -13,7 +13,8 @@ const ( transferDownload ) -// Transfer struct, it contains the transfer details for an upload or a download +// Transfer contains the transfer details for an upload or a download. +// It implements the io Reader and Writer interface to handle files downloads and uploads type Transfer struct { file *os.File path string @@ -27,7 +28,8 @@ type Transfer struct { isNewFile bool } -// ReadAt update sent bytes +// ReadAt reads len(p) bytes from the File to download starting at byte offset off and updates the bytes sent. +// It handles download bandwidth throttling too func (t *Transfer) ReadAt(p []byte, off int64) (n int, err error) { t.lastActivity = time.Now() readed, e := t.file.ReadAt(p, off) @@ -36,7 +38,8 @@ func (t *Transfer) ReadAt(p []byte, off int64) (n int, err error) { return readed, e } -// WriteAt update received bytes +// WriteAt writes len(p) bytes to the uploaded file starting at byte offset off and updates the bytes received. +// It handles upload bandwidth throttling too func (t *Transfer) WriteAt(p []byte, off int64) (n int, err error) { t.lastActivity = time.Now() written, e := t.file.WriteAt(p, off) @@ -45,7 +48,8 @@ func (t *Transfer) WriteAt(p []byte, off int64) (n int, err error) { return written, e } -// Close method called when the transfer is completed, we log the transfer info +// Close it is called when the transfer is completed. +// It closes the underlying file, log the transfer info, update the user quota, for uploads, and execute any defined actions. func (t *Transfer) Close() error { err := t.file.Close() elapsed := time.Since(t.start).Nanoseconds() / 1000000 diff --git a/utils/umask_unix.go b/utils/umask_unix.go index a08f85ef..006984d7 100644 --- a/utils/umask_unix.go +++ b/utils/umask_unix.go @@ -8,7 +8,7 @@ import ( "github.com/drakkan/sftpgo/logger" ) -// SetUmask set umask on unix systems +// SetUmask sets umask on unix systems func SetUmask(umask int, configValue string) { logger.Debug(logSender, "set umask to %v (%v)", configValue, umask) syscall.Umask(umask) diff --git a/utils/utils.go b/utils/utils.go index b46fa0cc..bc5c390b 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,3 +1,4 @@ +// Package utils provides some common utility methods package utils import ( @@ -11,7 +12,7 @@ import ( const logSender = "utils" -// IsStringInSlice search a string in a slice +// IsStringInSlice searches a string in a slice and returns true if the string is found func IsStringInSlice(obj string, list []string) bool { for _, v := range list { if v == obj { @@ -26,7 +27,7 @@ func GetTimeAsMsSinceEpoch(t time.Time) int64 { return t.UnixNano() / 1000000 } -// ScanDirContents returns the number of files contained in a directory and their size +// ScanDirContents returns the number of files contained in a directory, their size and a slice with the file paths func ScanDirContents(path string) (int, int64, []string, error) { var numFiles int var size int64 @@ -60,7 +61,7 @@ func isDirectory(path string) (bool, error) { return fileInfo.IsDir(), err } -// SetPathPermissions call os.Chown on unix does nothing on windows +// SetPathPermissions call os.Chown on unix, it does nothing on windows func SetPathPermissions(path string, uid int, gid int) { if runtime.GOOS != "windows" { if err := os.Chown(path, uid, gid); err != nil {