mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
Improve documentation
This commit is contained in:
parent
8dbcac15f3
commit
8058178ea0
19 changed files with 264 additions and 147 deletions
10
README.md
10
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
11
api/api.go
11
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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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(
|
||||||
|
|
3
main.go
3
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"
|
package main // import "github.com/drakkan/sftpgo"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue