httpd: add support for basic auth and HTTPS

This commit is contained in:
Nicola Murino 2020-02-04 00:08:00 +01:00
parent c64c080159
commit 8b039e0447
15 changed files with 683 additions and 159 deletions

View file

@ -185,6 +185,9 @@ The `sftpgo` configuration file contains the following sections:
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
- `auth_user_file`, string. Path to a file used to store usernames and password for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication and the file format must conform to the one generated using the Apache tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty HTTP authentication is disabled.
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided the the server will expect HTTPS connections.
Here is a full example showing the default config in JSON format:
@ -241,7 +244,10 @@ Here is a full example showing the default config in JSON format:
"bind_address": "127.0.0.1",
"templates_path": "templates",
"static_files_path": "static",
"backups_path": "backups"
"backups_path": "backups",
"auth_user_file": "",
"certificate_file": "",
"certificate_key_file": ""
}
}
```
@ -277,9 +283,9 @@ Before starting `sftpgo serve` please ensure that the configured dataprovider is
SQL based data providers (SQLite, MySQL, PostgreSQL) requires the creation of a database containing the required tables. Memory and bolt data providers does not require an initialization.
SQL scripts to create the required database structure can be found inside the source tree [sql](./sql "sql") directory. The SQL scripts filename is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190828.sql` must be applied before `20191112.sql` and so on.
Example for SQLite: `find sql/sqlite/ -type f -iname '*.sql' -print | sort -n |xargs cat | sqlite3 sftpgo.db`.
Example for SQLite: `find sql/sqlite/ -type f -iname '*.sql' -print | sort -n | xargs cat | sqlite3 sftpgo.db`.
The `memory` provider can load users from a dump obtained using the `dumpdata` REST API. This dump file can be configured using the dataprovider `name` configuration key. It will be loaded at startup and can be reloaded on demand using a `SIGHUP` on Unix based systems and a `paramchange` request to the running service on Windows.
The `memory` provider can load users from a dump obtained using the `dumpdata` REST API. The path to this dump file can be configured using the dataprovider `name` configuration key. It will be loaded at startup and can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The `memory` provider will not modify the provided file so quota usage and last login will not be persisted.
### Starting SFTGo in server mode
@ -584,7 +590,7 @@ Here is an example of the advertised service including credentials as seen using
For each account the following properties can be configured:
- `username`
- `password` used for password authentication. For users created using SFTPGo REST API if the password has no known hashing algo prefix it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512`. For example the `pbkdf2-sha256` of the word `password` using 150000 iterations and `E86a9YMX3zC7` as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. For bcrypt the format must be the one supported by golang's [crypto/bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) package, for example the password `secret` with cost `14` must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. Using the REST API you can send a password hashed as bcrypt, pbkdf2, md5crypt or sha512crypt and it will be stored as is.
- `password` used for password authentication. For users created using SFTPGo REST API if the password has no known hashing algo prefix it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512`. For example the `pbkdf2-sha256` of the word `password` using 150000 iterations and `E86a9YMX3zC7` as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. For bcrypt the format must be the one supported by golang's [crypto/bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) package, for example the password `secret` with cost `14` must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. We support Apache md5crypt (`$apr1$` prefix) too. Using the REST API you can send a password hashed as bcrypt, pbkdf2, md5crypt or sha512crypt and it will be stored as is.
- `public_keys` array of public keys. At least one public key or the password is mandatory.
- `status` 1 means "active", 0 "inactive". An inactive account cannot login.
- `expiration_date` expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration.
@ -637,7 +643,7 @@ SFTPGo exposes REST API to manage, backup and restore users and to get real time
If quota tracking is enabled in `sftpgo` configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP/SCP or if you change `track_quota` from `2` to `1`, you can rescan the users home dir and update the used quota using the REST API.
REST API is designed to run on localhost or on a trusted network, if you need HTTPS and/or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
REST API can be protected using HTTP basic authentication and exposed via HTTPS, if you need more advanced security features you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
@ -693,7 +699,7 @@ With the default `httpd` configuration, the web admin is available at the follow
[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web)
If you need HTTPS and/or authentication you can setup a reverse proxy as explained for the REST API.
The web interface can be protected using HTTP basic authentication and exposed via HTTPS, if you need more advanced security features you can setup a reverse proxy as explained for the REST API.
## Logs

View file

@ -87,11 +87,14 @@ func init() {
CredentialsPath: "credentials",
},
HTTPDConfig: httpd.Conf{
BindPort: 8080,
BindAddress: "127.0.0.1",
TemplatesPath: "templates",
StaticFilesPath: "static",
BackupsPath: "backups",
BindPort: 8080,
BindAddress: "127.0.0.1",
TemplatesPath: "templates",
StaticFilesPath: "static",
BackupsPath: "backups",
AuthUserFile: "",
CertificateFile: "",
CertificateKeyFile: "",
},
}

View file

@ -60,6 +60,7 @@ const (
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
md5cryptPwdPrefix = "$1$"
md5cryptApr1PwdPrefix = "$apr1$"
sha512cryptPwdPrefix = "$6$"
manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
@ -79,9 +80,9 @@ var (
provider Provider
sqlPlaceholders []string
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
pbkdf2SHA512Prefix, md5cryptPwdPrefix, sha512cryptPwdPrefix}
pbkdf2SHA512Prefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
unixPwdPrefixes = []string{md5cryptPwdPrefix, sha512cryptPwdPrefix}
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
logSender = "dataProvider"
availabilityTicker *time.Ticker
availabilityTickerDone chan bool
@ -709,7 +710,7 @@ func compareUnixPasswordAndHash(user User, password string) (bool, error) {
return match, errWrongPassword
}
match = true
} else if strings.HasPrefix(user.Password, md5cryptPwdPrefix) {
} else if strings.HasPrefix(user.Password, md5cryptPwdPrefix) || strings.HasPrefix(user.Password, md5cryptApr1PwdPrefix) {
crypter, ok := unixcrypt.MD5.CrypterFound(user.Password)
if !ok {
err = errors.New("cannot found matching MD5 crypter")

24
go.sum
View file

@ -5,7 +5,6 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0 h1:0E3eE8MX426vUOs7aHfI7aN1BrIzzzf4ccKCSfSjGmc=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0 h1:GGslhk/BU052LPlnI1vpp3fcbUs+hQ3E+Doti/3/vF8=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
@ -18,7 +17,6 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
cloud.google.com/go/storage v1.5.0 h1:RPUcBvDeYgQFMfQu1eBMq6piD1SXmLH+vK3qjewZPus=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -82,7 +80,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -91,7 +88,6 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@ -122,7 +118,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -130,7 +125,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@ -165,7 +159,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/nathanaelle/password v1.0.0 h1:1Etka3uuBvATlCb72f7P5vsgedus+C91Fgff1oMloq0=
github.com/nathanaelle/password v1.0.0/go.mod h1:wt9xV3xwQmc3Qi0ofowmzR7N+kF1L4cguCuWjAfdj1Q=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
@ -210,21 +203,17 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@ -269,9 +258,7 @@ golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxT
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299 h1:zQpM52jfKHG6II1ISZY1ZcpygvuSFZpLwfluuF89XOg=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a h1:7Wlg8L54In96HTWOaI4sreLJ6qfyGuvSau5el3fK41Y=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -282,9 +269,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -313,7 +298,6 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -373,11 +357,9 @@ golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4 h1:Toz2IK7k8rbltAXwNAxKcn9OzqyNfMUhUNjz3sL0NMk=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200131211209-ecb101ed6550 h1:3Kc3/T5DQ/majKzDmb+0NzmbXFhKLaeDTp3KqVPV5Eo=
golang.org/x/tools v0.0.0-20200131211209-ecb101ed6550/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -394,7 +376,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -407,7 +388,6 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb h1:ADPHZzpzM4tk4V4S5cnCrr5SwzvlrPRmqqCuJDB8UTs=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b h1:c8OBoXP3kTbDWWB/oVE3FkR851p4iZ3MPadz7zXEIPU=
@ -417,7 +397,6 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@ -426,7 +405,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@ -437,7 +415,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -445,6 +422,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
@ -22,12 +23,17 @@ import (
)
var (
httpBaseURL = "http://127.0.0.1:8080"
httpBaseURL = "http://127.0.0.1:8080"
authUsername = ""
authPassword = ""
)
// SetBaseURL sets the base url to use for HTTP requests, default is "http://127.0.0.1:8080"
func SetBaseURL(url string) {
// SetBaseURLAndCredentials sets the base url and the optional credentials to use for HTTP requests.
// Default URL is "http://127.0.0.1:8080" with empty credentials
func SetBaseURLAndCredentials(url, username, password string) {
httpBaseURL = url
authUsername = username
authPassword = password
}
// gets an HTTP Client with a timeout
@ -37,6 +43,20 @@ func getHTTPClient() *http.Client {
}
}
func sendHTTPRequest(method, url string, body io.Reader, contentType string) (*http.Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if len(contentType) > 0 {
req.Header.Set("Content-Type", "application/json")
}
if len(authUsername) > 0 || len(authPassword) > 0 {
req.SetBasicAuth(authUsername, authPassword)
}
return getHTTPClient().Do(req)
}
func buildURLRelativeToBase(paths ...string) string {
// we need to use path.Join and not filepath.Join
// since filepath.Join will use backslash separator on Windows
@ -79,7 +99,8 @@ func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User,
if err != nil {
return newUser, body, err
}
resp, err := getHTTPClient().Post(buildURLRelativeToBase(userPath), "application/json", bytes.NewBuffer(userAsJSON))
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(userPath), bytes.NewBuffer(userAsJSON),
"application/json")
if err != nil {
return newUser, body, err
}
@ -108,12 +129,8 @@ func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.Us
if err != nil {
return user, body, err
}
req, err := http.NewRequest(http.MethodPut, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)),
bytes.NewBuffer(userAsJSON))
if err != nil {
return user, body, err
}
resp, err := getHTTPClient().Do(req)
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)),
bytes.NewBuffer(userAsJSON), "application/json")
if err != nil {
return user, body, err
}
@ -135,11 +152,7 @@ func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.Us
// RemoveUser removes an existing user and checks the received HTTP Status code against expectedStatusCode.
func RemoveUser(user dataprovider.User, expectedStatusCode int) ([]byte, error) {
var body []byte
req, err := http.NewRequest(http.MethodDelete, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), nil)
if err != nil {
return body, err
}
resp, err := getHTTPClient().Do(req)
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), nil, "")
if err != nil {
return body, err
}
@ -152,7 +165,7 @@ func RemoveUser(user dataprovider.User, expectedStatusCode int) ([]byte, error)
func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, []byte, error) {
var user dataprovider.User
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(userPath, strconv.FormatInt(userID, 10)))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(userPath, strconv.FormatInt(userID, 10)), nil, "")
if err != nil {
return user, body, err
}
@ -188,7 +201,7 @@ func GetUsers(limit int64, offset int64, username string, expectedStatusCode int
q.Add("username", username)
}
url.RawQuery = q.Encode()
resp, err := getHTTPClient().Get(url.String())
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "")
if err != nil {
return users, body, err
}
@ -206,7 +219,7 @@ func GetUsers(limit int64, offset int64, username string, expectedStatusCode int
func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, []byte, error) {
var quotaScans []sftpd.ActiveQuotaScan
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(quotaScanPath))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "")
if err != nil {
return quotaScans, body, err
}
@ -227,7 +240,7 @@ func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, err
if err != nil {
return body, err
}
resp, err := getHTTPClient().Post(buildURLRelativeToBase(quotaScanPath), "application/json", bytes.NewBuffer(userAsJSON))
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanPath), bytes.NewBuffer(userAsJSON), "")
if err != nil {
return body, err
}
@ -240,7 +253,7 @@ func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, err
func GetConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byte, error) {
var connections []sftpd.ConnectionStatus
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(activeConnectionsPath))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(activeConnectionsPath), nil, "")
if err != nil {
return connections, body, err
}
@ -257,11 +270,7 @@ func GetConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byte, e
// CloseConnection closes an active connection identified by connectionID
func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) {
var body []byte
req, err := http.NewRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), nil)
if err != nil {
return body, err
}
resp, err := getHTTPClient().Do(req)
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), nil, "")
if err != nil {
return body, err
}
@ -275,7 +284,7 @@ func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error
func GetVersion(expectedStatusCode int) (utils.VersionInfo, []byte, error) {
var version utils.VersionInfo
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(versionPath))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(versionPath), nil, "")
if err != nil {
return version, body, err
}
@ -293,7 +302,7 @@ func GetVersion(expectedStatusCode int) (utils.VersionInfo, []byte, error) {
func GetProviderStatus(expectedStatusCode int) (map[string]interface{}, []byte, error) {
var response map[string]interface{}
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(providerStatusPath))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(providerStatusPath), nil, "")
if err != nil {
return response, body, err
}
@ -322,7 +331,7 @@ func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]int
q.Add("indent", indent)
}
url.RawQuery = q.Encode()
resp, err := getHTTPClient().Get(url.String())
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "")
if err != nil {
return response, body, err
}
@ -355,7 +364,7 @@ func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[st
q.Add("mode", mode)
}
url.RawQuery = q.Encode()
resp, err := getHTTPClient().Get(url.String())
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "")
if err != nil {
return response, body, err
}

150
httpd/auth.go Normal file
View file

@ -0,0 +1,150 @@
package httpd
import (
"encoding/csv"
"errors"
"fmt"
"net/http"
"os"
"strings"
"sync"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
unixcrypt "github.com/nathanaelle/password"
"golang.org/x/crypto/bcrypt"
)
const (
authenticationHeader = "WWW-Authenticate"
authenticationRealm = "SFTPGo Web"
unauthResponse = "Unauthorized"
)
var (
md5CryptPwdPrefixes = []string{"$1$", "$apr1$"}
bcryptPwdPrefixes = []string{"$2a$", "$2$", "$2x$", "$2y$", "$2b$"}
)
type httpAuthProvider interface {
getHashedPassword(username string) (string, bool)
isEnabled() bool
}
type basicAuthProvider struct {
Path string
Info os.FileInfo
Users map[string]string
lock *sync.RWMutex
}
func newBasicAuthProvider(authUserFile string) (httpAuthProvider, error) {
basicAuthProvider := basicAuthProvider{
Path: authUserFile,
Info: nil,
Users: make(map[string]string),
lock: new(sync.RWMutex),
}
return &basicAuthProvider, basicAuthProvider.loadUsers()
}
func (p *basicAuthProvider) isEnabled() bool {
return len(p.Path) > 0
}
func (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()
}
func (p *basicAuthProvider) loadUsers() error {
if !p.isEnabled() {
return nil
}
info, err := os.Stat(p.Path)
if err != nil {
logger.Debug(logSender, "", "unable to stat basic auth users file: %v", err)
return err
}
if p.isReloadNeeded(info) {
r, err := os.Open(p.Path)
if err != nil {
logger.Debug(logSender, "", "unable to open basic auth users file: %v", err)
return err
}
defer r.Close()
reader := csv.NewReader(r)
reader.Comma = ':'
reader.Comment = '#'
reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
if err != nil {
logger.Debug(logSender, "", "unable to parse basic auth users file: %v", err)
return err
}
p.lock.Lock()
defer p.lock.Unlock()
p.Users = make(map[string]string)
for _, record := range records {
if len(record) == 2 {
p.Users[record[0]] = record[1]
}
}
logger.Debug(logSender, "", "number of users loaded for httpd basic auth: %v", len(p.Users))
p.Info = info
}
return nil
}
func (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {
err := p.loadUsers()
if err != nil {
return "", false
}
p.lock.RLock()
defer p.lock.RUnlock()
pwd, ok := p.Users[username]
return pwd, ok
}
func checkAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateCredentials(r) {
w.Header().Set(authenticationHeader, fmt.Sprintf("Basic realm=\"%v\"", authenticationRealm))
if strings.HasPrefix(r.RequestURI, apiPrefix) {
sendAPIResponse(w, r, errors.New(unauthResponse), "", http.StatusUnauthorized)
} else {
http.Error(w, unauthResponse, http.StatusUnauthorized)
}
return
}
next.ServeHTTP(w, r)
})
}
func validateCredentials(r *http.Request) bool {
if !httpAuth.isEnabled() {
return true
}
username, password, ok := r.BasicAuth()
if !ok {
return false
}
if hashedPwd, ok := httpAuth.getHashedPassword(username); ok {
if utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))
return err == nil
}
if utils.IsStringPrefixInSlice(hashedPwd, md5CryptPwdPrefixes) {
crypter, ok := unixcrypt.MD5.CrypterFound(hashedPwd)
if !ok {
err := errors.New("cannot found matching MD5 crypter")
logger.Debug(logSender, "", "error comparing password with MD5 crypt hash: %v", err)
return false
}
return crypter.Verify([]byte(password))
}
}
return false
}

View file

@ -19,6 +19,7 @@ import (
const (
logSender = "httpd"
apiPrefix = "/api/v1"
activeConnectionsPath = "/api/v1/connection"
quotaScanPath = "/api/v1/quota_scan"
userPath = "/api/v1/user"
@ -40,6 +41,7 @@ var (
router *chi.Mux
dataProvider dataprovider.Provider
backupsPath string
httpAuth httpAuthProvider
)
// Conf httpd daemon configuration
@ -54,6 +56,16 @@ type Conf struct {
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
// Path to the backup directory. This can be an absolute path or a path relative to the config dir
BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
// Path to a file used to store usernames and password for basic authentication.
// This can be an absolute path or a path relative to the config dir.
// We support HTTP basic authentication and the file format must conform to the one generated using the Apache
// htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix).
// If empty HTTP authentication is disabled
AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"`
// If files containing a certificate and matching private key for the server are provided the server will expect
// HTTPS connections
CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
}
type apiResponse struct {
@ -69,27 +81,36 @@ func SetDataProvider(provider dataprovider.Provider) {
// Initialize the HTTP server
func (c Conf) Initialize(configDir string) error {
var err error
logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
backupsPath = c.BackupsPath
if !filepath.IsAbs(backupsPath) {
backupsPath = filepath.Join(configDir, backupsPath)
}
staticFilesPath := c.StaticFilesPath
if !filepath.IsAbs(staticFilesPath) {
staticFilesPath = filepath.Join(configDir, staticFilesPath)
}
templatesPath := c.TemplatesPath
if !filepath.IsAbs(templatesPath) {
templatesPath = filepath.Join(configDir, templatesPath)
backupsPath = getConfigPath(c.BackupsPath, configDir)
staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
templatesPath := getConfigPath(c.TemplatesPath, configDir)
authUserFile := getConfigPath(c.AuthUserFile, configDir)
httpAuth, err = newBasicAuthProvider(authUserFile)
if err != nil {
return err
}
certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
loadTemplates(templatesPath)
initializeRouter(staticFilesPath)
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
Handler: router,
ReadTimeout: 300 * time.Second,
WriteTimeout: 300 * time.Second,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 16, // 64KB
}
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
return httpServer.ListenAndServeTLS(certificateFile, certificateKeyFile)
}
return httpServer.ListenAndServe()
}
func getConfigPath(name, configDir string) string {
if len(name) > 0 && !filepath.IsAbs(name) {
return filepath.Join(configDir, name)
}
return name
}

View file

@ -84,10 +84,9 @@ func TestMain(m *testing.M) {
httpdConf := config.GetHTTPDConfig()
httpdConf.BindPort = 8081
httpd.SetBaseURL("http://127.0.0.1:8081")
httpdConf.BackupsPath = "test_backups"
currentPath, _ := os.Getwd()
backupsPath = filepath.Join(currentPath, "..", httpdConf.BackupsPath)
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8081", "", "")
backupsPath = filepath.Join(os.TempDir(), "test_backups")
httpdConf.BackupsPath = backupsPath
os.MkdirAll(backupsPath, 0777)
sftpd.SetDataProvider(dataProvider)
@ -113,6 +112,25 @@ func TestMain(m *testing.M) {
os.Exit(exitCode)
}
func TestInitialization(t *testing.T) {
config.LoadConfig(configDir, "")
httpdConf := config.GetHTTPDConfig()
httpdConf.BackupsPath = "test_backups"
httpdConf.AuthUserFile = "invalid file"
err := httpdConf.Initialize(configDir)
if err == nil {
t.Error("Inizialize must fail")
}
httpdConf.BackupsPath = backupsPath
httpdConf.AuthUserFile = ""
httpdConf.CertificateFile = "invalid file"
httpdConf.CertificateKeyFile = "invalid file"
err = httpdConf.Initialize(configDir)
if err == nil {
t.Error("Inizialize must fail")
}
}
func TestBasicUserHandling(t *testing.T) {
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
if err != nil {

View file

@ -4,10 +4,13 @@ import (
"context"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@ -318,7 +321,9 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
func TestApiCallsWithBadURL(t *testing.T) {
oldBaseURL := httpBaseURL
SetBaseURL(invalidURL)
oldAuthUsername := authUsername
oldAuthPassword := authPassword
SetBaseURLAndCredentials(invalidURL, oldAuthUsername, oldAuthPassword)
u := dataprovider.User{}
_, _, err := UpdateUser(u, http.StatusBadRequest)
if err == nil {
@ -344,12 +349,14 @@ func TestApiCallsWithBadURL(t *testing.T) {
if err == nil {
t.Error("request with invalid URL must fail")
}
SetBaseURL(oldBaseURL)
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
}
func TestApiCallToNotListeningServer(t *testing.T) {
oldBaseURL := httpBaseURL
SetBaseURL(inactiveURL)
oldAuthUsername := authUsername
oldAuthPassword := authPassword
SetBaseURLAndCredentials(inactiveURL, oldAuthUsername, oldAuthPassword)
u := dataprovider.User{}
_, _, err := AddUser(u, http.StatusBadRequest)
if err == nil {
@ -403,7 +410,79 @@ func TestApiCallToNotListeningServer(t *testing.T) {
if err == nil {
t.Errorf("request to an inactive URL must fail")
}
SetBaseURL(oldBaseURL)
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
}
func TestBasicAuth(t *testing.T) {
oldAuthUsername := authUsername
oldAuthPassword := authPassword
authUserFile := filepath.Join(os.TempDir(), "http_users.txt")
authUserData := []byte("test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\n")
ioutil.WriteFile(authUserFile, authUserData, 0666)
httpAuth, _ = newBasicAuthProvider(authUserFile)
_, _, err := GetVersion(http.StatusUnauthorized)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
SetBaseURLAndCredentials(httpBaseURL, "test1", "password1")
_, _, err = GetVersion(http.StatusOK)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
SetBaseURLAndCredentials(httpBaseURL, "test1", "wrong_password")
resp, _ := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(metricsPath), nil, "")
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("request with wrong password must fail, status code: %v", resp.StatusCode)
}
authUserData = append(authUserData, []byte("test2:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
ioutil.WriteFile(authUserFile, authUserData, 0666)
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
_, _, err = GetVersion(http.StatusOK)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password")
_, _, err = GetVersion(http.StatusOK)
if err == nil {
t.Error("request with wrong password must fail")
}
authUserData = append(authUserData, []byte("test3:$apr1$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
ioutil.WriteFile(authUserFile, authUserData, 0666)
SetBaseURLAndCredentials(httpBaseURL, "test3", "wrong_password")
_, _, err = GetVersion(http.StatusUnauthorized)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
authUserData = append(authUserData, []byte("test4:$invalid$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
ioutil.WriteFile(authUserFile, authUserData, 0666)
SetBaseURLAndCredentials(httpBaseURL, "test3", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if runtime.GOOS != "windows" {
authUserData = append(authUserData, []byte("test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
ioutil.WriteFile(authUserFile, authUserData, 0666)
os.Chmod(authUserFile, 0001)
SetBaseURLAndCredentials(httpBaseURL, "test5", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
os.Chmod(authUserFile, 0666)
}
authUserData = append(authUserData, []byte("\"foo\"bar\"\r\n")...)
ioutil.WriteFile(authUserFile, authUserData, 0666)
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
os.Remove(authUserFile)
SetBaseURLAndCredentials(httpBaseURL, oldAuthUsername, oldAuthPassword)
httpAuth, _ = newBasicAuthProvider("")
}
func TestCloseConnectionHandler(t *testing.T) {

View file

@ -37,91 +37,95 @@ func initializeRouter(staticFilesPath string) {
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
})
router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
})
router.Group(func(router chi.Router) {
router.Use(checkAuth)
router.Handle(metricsPath, promhttp.Handler())
router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
})
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, utils.GetAppVersion())
})
router.Handle(metricsPath, promhttp.Handler())
router.Get(providerStatusPath, func(w http.ResponseWriter, r *http.Request) {
err := dataprovider.GetProviderStatus(dataProvider)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "Alive", http.StatusOK)
}
})
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, utils.GetAppVersion())
})
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, sftpd.GetConnectionsStats())
})
router.Get(providerStatusPath, func(w http.ResponseWriter, r *http.Request) {
err := dataprovider.GetProviderStatus(dataProvider)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "Alive", http.StatusOK)
}
})
router.Delete(activeConnectionsPath+"/{connectionID}", func(w http.ResponseWriter, r *http.Request) {
handleCloseConnection(w, r)
})
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, sftpd.GetConnectionsStats())
})
router.Get(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
getQuotaScans(w, r)
})
router.Delete(activeConnectionsPath+"/{connectionID}", func(w http.ResponseWriter, r *http.Request) {
handleCloseConnection(w, r)
})
router.Post(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
startQuotaScan(w, r)
})
router.Get(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
getQuotaScans(w, r)
})
router.Get(userPath, func(w http.ResponseWriter, r *http.Request) {
getUsers(w, r)
})
router.Post(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
startQuotaScan(w, r)
})
router.Post(userPath, func(w http.ResponseWriter, r *http.Request) {
addUser(w, r)
})
router.Get(userPath, func(w http.ResponseWriter, r *http.Request) {
getUsers(w, r)
})
router.Get(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
getUserByID(w, r)
})
router.Post(userPath, func(w http.ResponseWriter, r *http.Request) {
addUser(w, r)
})
router.Put(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
updateUser(w, r)
})
router.Get(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
getUserByID(w, r)
})
router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
deleteUser(w, r)
})
router.Put(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
updateUser(w, r)
})
router.Get(dumpDataPath, func(w http.ResponseWriter, r *http.Request) {
dumpData(w, r)
})
router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
deleteUser(w, r)
})
router.Get(loadDataPath, func(w http.ResponseWriter, r *http.Request) {
loadData(w, r)
})
router.Get(dumpDataPath, func(w http.ResponseWriter, r *http.Request) {
dumpData(w, r)
})
router.Get(webUsersPath, func(w http.ResponseWriter, r *http.Request) {
handleGetWebUsers(w, r)
})
router.Get(loadDataPath, func(w http.ResponseWriter, r *http.Request) {
loadData(w, r)
})
router.Get(webUserPath, func(w http.ResponseWriter, r *http.Request) {
handleWebAddUserGet(w, r)
})
router.Get(webUsersPath, func(w http.ResponseWriter, r *http.Request) {
handleGetWebUsers(w, r)
})
router.Get(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
handleWebUpdateUserGet(chi.URLParam(r, "userID"), w, r)
})
router.Get(webUserPath, func(w http.ResponseWriter, r *http.Request) {
handleWebAddUserGet(w, r)
})
router.Post(webUserPath, func(w http.ResponseWriter, r *http.Request) {
handleWebAddUserPost(w, r)
})
router.Get(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
handleWebUpdateUserGet(chi.URLParam(r, "userID"), w, r)
})
router.Post(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
handleWebUpdateUserPost(chi.URLParam(r, "userID"), w, r)
})
router.Post(webUserPath, func(w http.ResponseWriter, r *http.Request) {
handleWebAddUserPost(w, r)
})
router.Get(webConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
handleWebGetConnections(w, r)
router.Post(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
handleWebUpdateUserPost(chi.URLParam(r, "userID"), w, r)
})
router.Get(webConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
handleWebGetConnections(w, r)
})
})
router.Group(func(router chi.Router) {

View file

@ -2,10 +2,12 @@ openapi: 3.0.1
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 1.6.2
version: 1.7.0
servers:
- url: /api/v1
security:
- BasicAuth: []
paths:
/version:
get:
@ -22,6 +24,36 @@ paths:
type: array
items:
$ref : '#/components/schemas/VersionInfo'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 403
message: ""
error: "Error description if any"
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 500
message: ""
error: "Error description if any"
/providerstatus:
get:
tags:
@ -39,6 +71,26 @@ paths:
status: 200
message: "Alive"
error: ""
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 403
message: ""
error: "Error description if any"
500:
description: Provider Error
content:
@ -64,6 +116,36 @@ paths:
type: array
items:
$ref : '#/components/schemas/ConnectionStatus'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 403
message: ""
error: "Error description if any"
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 500
message: ""
error: "Error description if any"
/connection/{connectionID}:
delete:
tags:
@ -98,6 +180,26 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 403
message: ""
error: "Error description if any"
404:
description: Not Found
content:
@ -133,6 +235,36 @@ paths:
type: array
items:
$ref : '#/components/schemas/QuotaScan'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 403
message: ""
error: "Error description if any"
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 500
message: ""
error: "Error description if any"
post:
tags:
- quota
@ -166,6 +298,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -265,6 +407,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -313,6 +465,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -365,6 +527,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -435,6 +607,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -499,6 +681,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -575,6 +767,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -655,6 +857,16 @@ paths:
status: 400
message: ""
error: "Error description if any"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
status: 401
message: ""
error: "Error description if any"
403:
description: Forbidden
content:
@ -991,3 +1203,7 @@ components:
type: string
commit_hash:
type: string
securitySchemes:
BasicAuth:
type: http
scheme: basic

View file

@ -412,7 +412,7 @@ func (c *scpCommand) sendDownloadFileData(filePath string, stat os.FileInfo, tra
break
}
}
if err != nil && err != io.EOF {
if err != io.EOF {
c.sendErrorMessage(err.Error())
return err
}

View file

@ -224,17 +224,17 @@ func TestInitialization(t *testing.T) {
sftpdConf.EnabledSSHCommands = append(sftpdConf.EnabledSSHCommands, "ls")
err := sftpdConf.Initialize(configDir)
if err == nil {
t.Errorf("Inizialize must fail, a SFTP server should be already running")
t.Error("Inizialize must fail, a SFTP server should be already running")
}
sftpdConf.KeyboardInteractiveProgram = "invalid_file"
err = sftpdConf.Initialize(configDir)
if err == nil {
t.Errorf("Inizialize must fail, a SFTP server should be already running")
t.Error("Inizialize must fail, a SFTP server should be already running")
}
sftpdConf.KeyboardInteractiveProgram = filepath.Join(homeBasePath, "invalid_file")
err = sftpdConf.Initialize(configDir)
if err == nil {
t.Errorf("Inizialize must fail, a SFTP server should be already running")
t.Error("Inizialize must fail, a SFTP server should be already running")
}
}
@ -2289,6 +2289,39 @@ func TestPasswordsHashMD5Crypt(t *testing.T) {
os.RemoveAll(user.GetHomeDir())
}
func TestPasswordsHashMD5CryptApr1(t *testing.T) {
md5CryptPwd := "$apr1$OBWLeSme$WoJbB736e7kKxMBIAqilb1"
clearPwd := "password"
usePubKey := false
u := getTestUser(usePubKey)
u.Password = md5CryptPwd
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
user.Password = clearPwd
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to login with md5 crypt password: %v", err)
} else {
defer client.Close()
_, err = client.Getwd()
if err != nil {
t.Errorf("unable to get working dir with md5 crypt password: %v", err)
}
}
user.Password = md5CryptPwd
_, err = getSftpClient(user, usePubKey)
if err == nil {
t.Errorf("login with wrong password must fail")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
os.RemoveAll(user.GetHomeDir())
}
func TestPermList(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)

View file

@ -50,6 +50,9 @@
"bind_address": "127.0.0.1",
"templates_path": "templates",
"static_files_path": "static",
"backups_path": "backups"
"backups_path": "backups",
"auth_user_file": "",
"certificate_file": "",
"certificate_key_file": ""
}
}

View file

@ -202,6 +202,11 @@ func (fs S3Fs) Create(name string, flag int) (*os.File, *pipeat.PipeWriterAt, fu
// rename all the contents too and this could take long time: think
// about directories with thousands of files, for each file we should
// execute a CopyObject call.
// TODO: rename does not work for files bigger than 5GB, implement
// multipart copy or wait for this pull request to be merged:
//
// https://github.com/aws/aws-sdk-go/pull/2653
//
func (fs S3Fs) Rename(source, target string) error {
if source == target {
return nil