httpd: add support for basic auth and HTTPS
This commit is contained in:
parent
c64c080159
commit
8b039e0447
15 changed files with 683 additions and 159 deletions
18
README.md
18
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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: "",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
24
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
150
httpd/auth.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
134
httpd/router.go
134
httpd/router.go
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue