Improve documentation

This commit is contained in:
Nicola Murino 2019-07-30 20:51:29 +02:00
parent 8dbcac15f3
commit 8058178ea0
19 changed files with 264 additions and 147 deletions

View file

@ -27,7 +27,7 @@ Regularly the test cases are manually executed and pass on Windows. Other UNIX v
## Requirements ## Requirements
- Go 1.12 or higher - 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 ## 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: - `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 - 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 - 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 - **"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_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" - `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. - `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 - `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 - `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 - `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 - `quota_files` maximum number of files allowed. 0 means unlimited
- `permissions` the following permissions are supported: - `permissions` the following permissions are supported:
- `*` all permission are granted - `*` all permission are granted
@ -152,7 +152,7 @@ For each account the following properties can be configured:
- `delete` delete files or directories is allowed - `delete` delete files or directories is allowed
- `rename` rename files or directories is allowed - `rename` rename files or directories is allowed
- `create_dirs` create 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 - `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited - `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited

View file

@ -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 package api
import ( import (
@ -22,7 +27,9 @@ var (
// HTTPDConf httpd daemon configuration // HTTPDConf httpd daemon configuration
type HTTPDConf struct { 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"` BindAddress string `json:"bind_address"`
} }
@ -36,7 +43,7 @@ func init() {
initializeRouter() initializeRouter()
} }
// SetDataProvider sets the data provider // SetDataProvider sets the data provider to use to fetch the data about users
func SetDataProvider(provider dataprovider.Provider) { func SetDataProvider(provider dataprovider.Provider) {
dataProvider = provider dataProvider = provider
} }

View file

@ -22,18 +22,19 @@ var (
httpBaseURL = "http://127.0.0.1:8080" 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) { func SetBaseURL(url string) {
httpBaseURL = url httpBaseURL = url
} }
// gets an HTTP Client with a timeout
func getHTTPClient() *http.Client { func getHTTPClient() *http.Client {
return &http.Client{ return &http.Client{
Timeout: 15 * time.Second, 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) { func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, error) {
var newUser dataprovider.User var newUser dataprovider.User
userAsJSON, err := json.Marshal(user) userAsJSON, err := json.Marshal(user)
@ -58,7 +59,7 @@ func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User,
return newUser, err 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) { func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, error) {
var newUser dataprovider.User var newUser dataprovider.User
userAsJSON, err := json.Marshal(user) userAsJSON, err := json.Marshal(user)
@ -87,7 +88,7 @@ func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.Us
return newUser, err 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 { func RemoveUser(user dataprovider.User, expectedStatusCode int) error {
req, err := http.NewRequest(http.MethodDelete, httpBaseURL+userPath+"/"+strconv.FormatInt(user.ID, 10), nil) req, err := http.NewRequest(http.MethodDelete, httpBaseURL+userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
if err != nil { if err != nil {
@ -101,7 +102,7 @@ func RemoveUser(user dataprovider.User, expectedStatusCode int) error {
return checkResponse(resp.StatusCode, expectedStatusCode, resp) 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) { func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, error) {
var user dataprovider.User var user dataprovider.User
resp, err := getHTTPClient().Get(httpBaseURL + userPath + "/" + strconv.FormatInt(userID, 10)) 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 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) { func GetUsers(limit int64, offset int64, username string, expectedStatusCode int) ([]dataprovider.User, error) {
var users []dataprovider.User var users []dataprovider.User
url, err := url.Parse(httpBaseURL + userPath) url, err := url.Parse(httpBaseURL + userPath)
@ -146,7 +150,7 @@ func GetUsers(limit int64, offset int64, username string, expectedStatusCode int
return users, err 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) { func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, error) {
var quotaScans []sftpd.ActiveQuotaScan var quotaScans []sftpd.ActiveQuotaScan
resp, err := getHTTPClient().Get(httpBaseURL + quotaScanPath) resp, err := getHTTPClient().Get(httpBaseURL + quotaScanPath)
@ -161,7 +165,7 @@ func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, error) {
return quotaScans, err 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 { func StartQuotaScan(user dataprovider.User, expectedStatusCode int) error {
userAsJSON, err := json.Marshal(user) userAsJSON, err := json.Marshal(user)
if err != nil { if err != nil {

View file

@ -10,7 +10,7 @@ import (
"github.com/go-chi/render" "github.com/go-chi/render"
) )
// GetHTTPRouter returns the configured HTTP router // GetHTTPRouter returns the configured HTTP handler
func GetHTTPRouter() http.Handler { func GetHTTPRouter() http.Handler {
return router return router
} }

View file

@ -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 package config
import ( import (
@ -62,22 +67,22 @@ func init() {
} }
} }
// GetSFTPDConfig returns sftpd configuration // GetSFTPDConfig returns the configuration for the SFTP server
func GetSFTPDConfig() sftpd.Configuration { func GetSFTPDConfig() sftpd.Configuration {
return globalConf.SFTPD return globalConf.SFTPD
} }
// GetHTTPDConfig returns httpd configuration // GetHTTPDConfig returns the configuration for the HTTP server
func GetHTTPDConfig() api.HTTPDConf { func GetHTTPDConfig() api.HTTPDConf {
return globalConf.HTTPDConfig return globalConf.HTTPDConfig
} }
//GetProviderConf returns data provider configuration //GetProviderConf returns the configuration for the data provider
func GetProviderConf() dataprovider.Config { func GetProviderConf() dataprovider.Config {
return globalConf.ProviderConf 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 { func LoadConfig(configPath string) error {
logger.Debug(logSender, "load config from path: %v", configPath) logger.Debug(logSender, "load config from path: %v", configPath)
file, err := os.Open(configPath) file, err := os.Open(configPath)

View file

@ -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 package dataprovider
import ( import (
@ -28,7 +31,7 @@ const (
) )
var ( 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} SupportedProviders = []string{SQLiteDataProviderName, PGSSQLDataProviderName, MySQLDataProviderName}
dbHandle *sql.DB dbHandle *sql.DB
config Config config Config
@ -40,17 +43,38 @@ var (
// Config provider configuration // Config provider configuration
type Config struct { type Config struct {
Driver string `json:"driver"` // Driver name, must be one of the SupportedProviders
Name string `json:"name"` Driver string `json:"driver"`
Host string `json:"host"` // Database name
Port int `json:"port"` Name string `json:"name"`
Username string `json:"username"` // Database host
Password string `json:"password"` 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"` ConnectionString string `json:"connection_string"`
UsersTable string `json:"users_table"` // Database table for SFTP users
ManageUsers int `json:"manage_users"` UsersTable string `json:"users_table"`
SSLMode int `json:"sslmode"` // Set to 0 to disable users management, 1 to enable
TrackQuota int `json:"track_quota"` 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 // ValidationError raised if input data is not valid
@ -58,25 +82,29 @@ type ValidationError struct {
err string err string
} }
// Validation error details
func (e *ValidationError) Error() string { func (e *ValidationError) Error() string {
return fmt.Sprintf("Validation error: %s", e.err) 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 { type MethodDisabledError struct {
err string err string
} }
// Method disabled error details
func (e *MethodDisabledError) Error() string { func (e *MethodDisabledError) Error() string {
return fmt.Sprintf("Method disabled error: %s", e.err) return fmt.Sprintf("Method disabled error: %s", e.err)
} }
// GetProvider get the configured provider // GetProvider returns the configured provider
func GetProvider() Provider { func GetProvider() Provider {
return provider return provider
} }
// Provider interface for data providers // Provider interface that data providers must implement.
type Provider interface { type Provider interface {
validateUserAndPass(username string, password string) (User, error) validateUserAndPass(username string, password string) (User, error)
validateUserAndPubKey(username string, pubKey string) (User, error) validateUserAndPubKey(username string, pubKey string) (User, error)
@ -90,7 +118,8 @@ type Provider interface {
getUserByID(ID int64) (User, error) 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 { func Initialize(cnf Config, basePath string) error {
config = cnf config = cnf
sqlPlaceholders = getSQLPlaceholders() sqlPlaceholders = getSQLPlaceholders()
@ -107,17 +136,18 @@ func Initialize(cnf Config, basePath string) error {
return fmt.Errorf("Unsupported data provider: %v", config.Driver) 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) { func CheckUserAndPass(p Provider, username string, password string) (User, error) {
return p.validateUserAndPass(username, password) 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) { func CheckUserAndPubKey(p Provider, username string, pubKey string) (User, error) {
return p.validateUserAndPubKey(username, pubKey) 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 { func UpdateUserQuota(p Provider, user User, filesAdd int, sizeAdd int64, reset bool) error {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return &MethodDisabledError{err: trackQuotaDisabledError} 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) 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) { func GetUsedQuota(p Provider, username string) (int, int64, error) {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return 0, 0, &MethodDisabledError{err: trackQuotaDisabledError} return 0, 0, &MethodDisabledError{err: trackQuotaDisabledError}
@ -135,12 +166,13 @@ func GetUsedQuota(p Provider, username string) (int, int64, error) {
return p.getUsedQuota(username) 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) { func UserExists(p Provider, username string) (User, error) {
return p.userExists(username) 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 { func AddUser(p Provider, user User) error {
if config.ManageUsers == 0 { if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError} return &MethodDisabledError{err: manageUsersDisabledError}
@ -148,7 +180,8 @@ func AddUser(p Provider, user User) error {
return p.addUser(user) 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 { func UpdateUser(p Provider, user User) error {
if config.ManageUsers == 0 { if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError} return &MethodDisabledError{err: manageUsersDisabledError}
@ -156,7 +189,8 @@ func UpdateUser(p Provider, user User) error {
return p.updateUser(user) 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 { func DeleteUser(p Provider, user User) error {
if config.ManageUsers == 0 { if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError} return &MethodDisabledError{err: manageUsersDisabledError}
@ -164,12 +198,12 @@ func DeleteUser(p Provider, user User) error {
return p.deleteUser(user) 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) { func GetUsers(p Provider, limit int, offset int, order string, username string) ([]User, error) {
return p.getUsers(limit, offset, order, username) 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) { func GetUserByID(p Provider, ID int64) (User, error) {
return p.getUserByID(ID) return p.getUserByID(ID)
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/logger"
) )
// MySQLProvider auth provider for sqlite database // MySQLProvider auth provider for MySQL/MariaDB database
type MySQLProvider struct { type MySQLProvider struct {
} }

View file

@ -10,7 +10,7 @@ import (
"github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/logger"
) )
// SQLiteProvider auth provider for sqlite database // SQLiteProvider auth provider for SQLite database
type SQLiteProvider struct { type SQLiteProvider struct {
} }

View file

@ -7,36 +7,64 @@ import (
"github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/utils"
) )
// Permissions // Available permissions for SFTP users
const ( const (
PermAny = "*" // All permissions are granted
PermListItems = "list" PermAny = "*"
PermDownload = "download" // List items such as files and directories is allowed
PermUpload = "upload" PermListItems = "list"
PermDelete = "delete" // download files is allowed
PermRename = "rename" PermDownload = "download"
PermCreateDirs = "create_dirs" // 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" PermCreateSymlinks = "create_symlinks"
) )
// User defines an SFTP user // User defines an SFTP user
type User struct { type User struct {
ID int64 `json:"id"` // Database unique identifier
Username string `json:"username"` ID int64 `json:"id"`
Password string `json:"password,omitempty"` // Username
PublicKey string `json:"public_key,omitempty"` Username string `json:"username"`
HomeDir string `json:"home_dir"` // Password used for password authentication.
UID int `json:"uid"` // For users created using SFTPGo REST API the password is be stored using argon2id hashing algo.
GID int `json:"gid"` // Checking passwords stored with bcrypt is supported too.
MaxSessions int `json:"max_sessions"` // Currently, as fallback, there is a clear text password checking but you should not store passwords
QuotaSize int64 `json:"quota_size"` // as clear text and this support could be removed at any time, so please don't depend on it.
QuotaFiles int `json:"quota_files"` Password string `json:"password,omitempty"`
Permissions []string `json:"permissions"` // PublicKey used for public key authentication. At least one between password and public key is mandatory
UsedQuotaSize int64 `json:"used_quota_size"` PublicKey string `json:"public_key,omitempty"`
UsedQuotaFiles int `json:"used_quota_files"` // The user cannot upload or download files outside this directory. Must be an absolute path
LastQuotaUpdate int64 `json:"last_quota_update"` HomeDir string `json:"home_dir"`
UploadBandwidth int64 `json:"upload_bandwidth"` // If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
DownloadBandwidth int64 `json:"download_bandwidth"` 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 // 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) return utils.IsStringInSlice(permission, u.Permissions)
} }
// HasOption returns true if the user has the give option // GetPermissionsAsJSON returns the permissions as json byte array
/*func (u *User) HasOption(option string) bool {
return utils.IsStringInSlice(option, u.Options)
}*/
// GetPermissionsAsJSON returns the permission as json byte array
func (u *User) GetPermissionsAsJSON() ([]byte, error) { func (u *User) GetPermissionsAsJSON() ([]byte, error) {
return json.Marshal(u.Permissions) return json.Marshal(u.Permissions)
} }
// GetOptionsAsJSON returns the permission as json byte array // GetUID returns a validate uid, suitable for use with os.Chown
/*func (u *User) GetOptionsAsJSON() ([]byte, error) {
return json.Marshal(u.Options)
}*/
// GetUID returns a validate uid
func (u *User) GetUID() int { func (u *User) GetUID() int {
if u.UID <= 0 || u.UID > 65535 { if u.UID <= 0 || u.UID > 65535 {
return -1 return -1
@ -70,7 +88,7 @@ func (u *User) GetUID() int {
return u.UID return u.UID
} }
// GetGID returns a validate gid // GetGID returns a validate gid, suitable for use with os.Chown
func (u *User) GetGID() int { func (u *User) GetGID() int {
if u.GID <= 0 || u.GID > 65535 { if u.GID <= 0 || u.GID > 65535 {
return -1 return -1
@ -78,13 +96,12 @@ func (u *User) GetGID() int {
return u.GID 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 { func (u *User) GetHomeDir() string {
return filepath.Clean(u.HomeDir) return filepath.Clean(u.HomeDir)
} }
// HasQuotaRestrictions returns true if there is a quota restriction on number of files // HasQuotaRestrictions returns true if there is a quota restriction on number of files or size or both
// or size or both
func (u *User) HasQuotaRestrictions() bool { func (u *User) HasQuotaRestrictions() bool {
return u.QuotaFiles > 0 || u.QuotaSize > 0 return u.QuotaFiles > 0 || u.QuotaSize > 0
} }

View file

@ -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 package logger
import ( import (
@ -15,12 +21,13 @@ var (
logger zerolog.Logger logger zerolog.Logger
) )
// GetLogger get the logger instance // GetLogger get the configured logger instance
func GetLogger() *zerolog.Logger { func GetLogger() *zerolog.Logger {
return &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) { func InitLogger(logFilePath string, level zerolog.Level) {
logMaxSize := 10 // MB logMaxSize := 10 // MB
logMaxBackups := 5 logMaxBackups := 5
@ -36,22 +43,22 @@ func InitLogger(logFilePath string, level zerolog.Level) {
}).With().Timestamp().Logger().Level(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{}) { func Debug(sender string, format string, v ...interface{}) {
logger.Debug().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) 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{}) { func Info(sender string, format string, v ...interface{}) {
logger.Info().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) 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{}) { func Warn(sender string, format string, v ...interface{}) {
logger.Warn().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) 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{}) { func Error(sender string, format string, v ...interface{}) {
logger.Error().Str("sender", sender).Msg(fmt.Sprintf(format, v...)) 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("") Msg("")
} }
// CommandLog log an SFTP command // CommandLog logs an SFTP command
func CommandLog(command string, path string, target string, user string, connectionID string) { func CommandLog(command string, path string, target string, user string, connectionID string) {
logger.Info(). logger.Info().
Str("sender", command). Str("sender", command).

View file

@ -9,23 +9,28 @@ import (
"github.com/rs/zerolog" "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 { type StructuredLogger struct {
Logger *zerolog.Logger Logger *zerolog.Logger
} }
// StructuredLoggerEntry using zerolog logger // StructuredLoggerEntry defines a log entry.
// It implements chi.middleware.LogEntry interface
type StructuredLoggerEntry struct { type StructuredLoggerEntry struct {
// The zerolog logger
Logger *zerolog.Logger Logger *zerolog.Logger
// fields to write in the log
fields map[string]interface{} 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 { func NewStructuredLogger(logger *zerolog.Logger) func(next http.Handler) http.Handler {
return middleware.RequestLogger(&StructuredLogger{logger}) 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 { func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
@ -47,7 +52,7 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
return &StructuredLoggerEntry{Logger: l.Logger, fields: fields} 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) { func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
l.Logger.Info().Fields(l.fields).Int( l.Logger.Info().Fields(l.fields).Int(
"resp_status", status).Int( "resp_status", status).Int(

View file

@ -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" package main // import "github.com/drakkan/sftpgo"
import ( import (

View file

@ -22,14 +22,20 @@ import (
// Connection details for an authenticated user // Connection details for an authenticated user
type Connection struct { type Connection struct {
ID string // Unique identifier for the connection
User dataprovider.User ID string
// logged in user's details
User dataprovider.User
// client's version string
ClientVersion string ClientVersion string
RemoteAddr net.Addr // Remote address for this connection
StartTime time.Time RemoteAddr net.Addr
lastActivity time.Time // start time for this connection
lock *sync.Mutex StartTime time.Time
sshConn *ssh.ServerConn // 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. // 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 nil, sftp.ErrSshFxFailure
} }
return ListerAt(files), nil return listerAt(files), nil
case "Stat": case "Stat":
if !c.User.HasPerm(dataprovider.PermListItems) { if !c.User.HasPerm(dataprovider.PermListItems) {
return nil, sftp.ErrSshFxPermissionDenied return nil, sftp.ErrSshFxPermissionDenied
@ -288,7 +294,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
return ListerAt([]os.FileInfo{s}), nil return listerAt([]os.FileInfo{s}), nil
default: default:
return nil, sftp.ErrSshFxOpUnsupported return nil, sftp.ErrSshFxOpUnsupported
} }

View file

@ -5,12 +5,11 @@ import (
"os" "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. // 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. // 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)) { if offset >= int64(len(l)) {
return 0, io.EOF return 0, io.EOF
} }

View file

@ -25,15 +25,24 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// Configuration server configuration // Configuration for the SFTP server
type Configuration struct { type Configuration struct {
Banner string `json:"banner"` // Identification string used by the server
BindPort int `json:"bind_port"` Banner string `json:"banner"`
BindAddress string `json:"bind_address"` // The port used for serving SFTP requests
IdleTimeout int `json:"idle_timeout"` BindPort int `json:"bind_port"`
MaxAuthTries int `json:"max_auth_tries"` // The address to listen on. A blank value means listen on all available network interfaces.
Umask string `json:"umask"` BindAddress string `json:"bind_address"`
Actions Actions `json:"actions"` // 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. // 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) { func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) {
defer conn.Close() defer conn.Close()

View file

@ -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 package sftpd
import ( import (
@ -48,29 +51,41 @@ type connectionTransfer struct {
LastActivity int64 `json:"last_activity"` LastActivity int64 `json:"last_activity"`
} }
// ActiveQuotaScan username and start data for a quota scan // ActiveQuotaScan defines an active quota scan
type ActiveQuotaScan struct { type ActiveQuotaScan struct {
Username string `json:"username"` // Username to which the quota scan refers
StartTime int64 `json:"start_time"` 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. // Actions to execute on SFTP create, download, delete and rename.
// A rename trigger delete script for the old file and create script for the new one // An external command can be executed and/or an HTTP notification can be fired
type Actions struct { type Actions struct {
ExecuteOn []string `json:"execute_on"` // Valid values are download, upload, delete, rename. Empty slice to disable
Command string `json:"command"` ExecuteOn []string `json:"execute_on"`
HTTPNotificationURL string `json:"http_notification_url"` // 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 // ConnectionStatus status for an active connection
type ConnectionStatus struct { type ConnectionStatus struct {
Username string `json:"username"` // Logged in username
ConnectionID string `json:"connection_id"` Username string `json:"username"`
ClientVersion string `json:"client_version"` // Unique identifier for the connection
RemoteAddress string `json:"remote_address"` ConnectionID string `json:"connection_id"`
ConnectionTime int64 `json:"connection_time"` // client's version string
LastActivity int64 `json:"last_activity"` ClientVersion string `json:"client_version"`
Transfers []connectionTransfer `json:"active_transfers"` // 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() { func init() {
@ -78,7 +93,7 @@ func init() {
idleConnectionTicker = time.NewTicker(5 * time.Minute) 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) { func SetDataProvider(provider dataprovider.Provider) {
dataProvider = provider dataProvider = provider
} }
@ -121,7 +136,7 @@ func AddQuotaScan(username string) bool {
return true 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 { func RemoveQuotaScan(username string) error {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
@ -143,7 +158,8 @@ func RemoveQuotaScan(username string) error {
return err 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 { func CloseActiveConnection(connectionID string) bool {
result := false result := false
mutex.RLock() 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() { func CheckIdleConnections() {
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()

View file

@ -13,7 +13,8 @@ const (
transferDownload 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 { type Transfer struct {
file *os.File file *os.File
path string path string
@ -27,7 +28,8 @@ type Transfer struct {
isNewFile bool 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) { func (t *Transfer) ReadAt(p []byte, off int64) (n int, err error) {
t.lastActivity = time.Now() t.lastActivity = time.Now()
readed, e := t.file.ReadAt(p, off) 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 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) { func (t *Transfer) WriteAt(p []byte, off int64) (n int, err error) {
t.lastActivity = time.Now() t.lastActivity = time.Now()
written, e := t.file.WriteAt(p, off) 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 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 { func (t *Transfer) Close() error {
err := t.file.Close() err := t.file.Close()
elapsed := time.Since(t.start).Nanoseconds() / 1000000 elapsed := time.Since(t.start).Nanoseconds() / 1000000

View file

@ -8,7 +8,7 @@ import (
"github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/logger"
) )
// SetUmask set umask on unix systems // SetUmask sets umask on unix systems
func SetUmask(umask int, configValue string) { func SetUmask(umask int, configValue string) {
logger.Debug(logSender, "set umask to %v (%v)", configValue, umask) logger.Debug(logSender, "set umask to %v (%v)", configValue, umask)
syscall.Umask(umask) syscall.Umask(umask)

View file

@ -1,3 +1,4 @@
// Package utils provides some common utility methods
package utils package utils
import ( import (
@ -11,7 +12,7 @@ import (
const logSender = "utils" 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 { func IsStringInSlice(obj string, list []string) bool {
for _, v := range list { for _, v := range list {
if v == obj { if v == obj {
@ -26,7 +27,7 @@ func GetTimeAsMsSinceEpoch(t time.Time) int64 {
return t.UnixNano() / 1000000 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) { func ScanDirContents(path string) (int, int64, []string, error) {
var numFiles int var numFiles int
var size int64 var size int64
@ -60,7 +61,7 @@ func isDirectory(path string) (bool, error) {
return fileInfo.IsDir(), err 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) { func SetPathPermissions(path string, uid int, gid int) {
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
if err := os.Chown(path, uid, gid); err != nil { if err := os.Chown(path, uid, gid); err != nil {