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

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
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
}

View file

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

View file

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

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

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

View file

@ -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 {
}

View file

@ -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 {
}

View file

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

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

View file

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

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"
import (

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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