From 778ec9b88f294e7d46215598c0ce782eabe39763 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 17 Jan 2021 22:29:08 +0100 Subject: [PATCH] REST API v2 - add JWT authentication - admins are now stored inside the data provider - admin access can be restricted based on the source IP: both proxy header and connection IP are checked - deprecate REST API CLI: it is not relevant anymore Some other changes to the REST API can still happen before releasing SFTPGo 2.0.0 Fixes #197 --- .github/workflows/development.yml | 2 - .github/workflows/release.yml | 22 +- README.md | 3 +- cmd/startsubsys.go | 2 +- common/common_test.go | 2 +- common/connection_test.go | 14 +- common/transfer.go | 4 +- common/transfer_test.go | 4 +- config/config.go | 4 - dataprovider/admin.go | 228 ++ dataprovider/bolt.go | 384 ++- dataprovider/dataprovider.go | 169 +- dataprovider/memory.go | 275 +- dataprovider/mysql.go | 126 +- dataprovider/pgsql.go | 127 +- dataprovider/sqlcommon.go | 295 +- dataprovider/sqlite.go | 126 +- dataprovider/sqlqueries.go | 40 +- dataprovider/user.go | 20 +- docs/account.md | 2 +- docs/defender.md | 2 +- docs/full-configuration.md | 2 - docs/rest-api.md | 48 +- docs/service.md | 4 - docs/web-admin.md | 9 +- examples/convertusers/README.md | 49 + examples/convertusers/convertusers | 208 ++ examples/rest-api-cli/README.md | 2 + ftpd/cryptfs_test.go | 18 +- ftpd/ftpd_test.go | 265 +- go.mod | 21 +- go.sum | 64 +- httpd/api_admin.go | 211 ++ httpd/api_folder.go | 31 +- httpd/api_maintenance.go | 39 +- httpd/api_user.go | 91 +- httpd/api_utils.go | 996 +------ httpd/auth.go | 34 - httpd/auth_utils.go | 147 + httpd/httpd.go | 124 +- httpd/httpd_test.go | 2403 ++++++++++++----- httpd/internal_test.go | 916 +++---- httpd/middleware.go | 95 + httpd/router.go | 138 - httpd/schema/openapi.yaml | 400 ++- httpd/server.go | 346 +++ httpd/web.go | 438 ++- httpdtest/httpdtest.go | 1246 +++++++++ pkgs/build.sh | 9 - pkgs/debian/sftpgo.install | 1 - pkgs/debian/sftpgo.install.arm64 | 1 - pkgs/debian/sftpgo.install.ppc64el | 1 - service/service.go | 2 +- sftpd/cryptfs_test.go | 68 +- sftpd/scp.go | 2 + sftpd/sftpd_test.go | 1118 ++++---- sftpgo.json | 2 - static/img/undraw_profile.svg | 38 + .../vendor/fontawesome-free/css/all.min.css | 5 - .../fontawesome-free/css/fontawesome.min.css | 5 + .../vendor/fontawesome-free/css/solid.min.css | 5 + .../fontawesome-free/svgs/solid/calendar.svg | 1 - .../svgs/solid/exchange-alt.svg | 1 - .../svgs/solid/folder-open.svg | 1 - .../fontawesome-free/svgs/solid/folder.svg | 1 - .../svgs/solid/info-circle.svg | 1 - .../fontawesome-free/svgs/solid/user.svg | 1 - .../webfonts/fa-solid-900.svg | 2238 +++++++-------- templates/admin.html | 92 + templates/admins.html | 185 ++ templates/base.html | 79 +- templates/changepwd.html | 38 + templates/connections.html | 19 +- templates/folders.html | 45 +- templates/login.html | 109 + templates/message.html | 32 + templates/user.html | 2 +- templates/users.html | 78 +- utils/utils.go | 21 +- webdavd/internal_test.go | 10 +- webdavd/webdavd_test.go | 220 +- windows-installer/sftpgo.iss | 2 - 82 files changed, 9302 insertions(+), 5327 deletions(-) create mode 100644 dataprovider/admin.go create mode 100644 examples/convertusers/README.md create mode 100755 examples/convertusers/convertusers create mode 100644 httpd/api_admin.go delete mode 100644 httpd/auth.go create mode 100644 httpd/auth_utils.go create mode 100644 httpd/middleware.go delete mode 100644 httpd/router.go create mode 100644 httpd/server.go create mode 100644 httpdtest/httpdtest.go create mode 100644 static/img/undraw_profile.svg delete mode 100644 static/vendor/fontawesome-free/css/all.min.css create mode 100644 static/vendor/fontawesome-free/css/fontawesome.min.css create mode 100644 static/vendor/fontawesome-free/css/solid.min.css delete mode 100644 static/vendor/fontawesome-free/svgs/solid/calendar.svg delete mode 100644 static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg delete mode 100644 static/vendor/fontawesome-free/svgs/solid/folder-open.svg delete mode 100644 static/vendor/fontawesome-free/svgs/solid/folder.svg delete mode 100644 static/vendor/fontawesome-free/svgs/solid/info-circle.svg delete mode 100644 static/vendor/fontawesome-free/svgs/solid/user.svg create mode 100644 templates/admin.html create mode 100644 templates/admins.html create mode 100644 templates/changepwd.html create mode 100644 templates/login.html diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index e32a3038..9803aa98 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -98,13 +98,11 @@ jobs: if: startsWith(matrix.os, 'windows-') != true run: | mkdir -p output/{bash_completion,zsh_completion} - mkdir -p output/examples/rest-api-cli cp sftpgo output/ cp sftpgo.json output/ cp -r templates output/ cp -r static output/ cp -r init output/ - cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/ ./sftpgo gen completion bash > output/bash_completion/sftpgo ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen man -d output/man/man1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3e29897..8e608309 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,12 +94,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - - name: Set up Python - if: startsWith(matrix.os, 'windows-') - uses: actions/setup-python@v2 - with: - python-version: 3.x - - name: Build for Linux/macOS if: startsWith(matrix.os, 'windows-') != true run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo @@ -136,15 +130,6 @@ jobs: env: MATRIX_OS: ${{ matrix.os }} - - name: Build REST API CLI for Windows - if: startsWith(matrix.os, 'windows-') - run: | - python -m pip install --upgrade pip setuptools wheel - pip install requests - pip install pygments - pip install pyinstaller - pyinstaller --hidden-import="pkg_resources.py2_warn" --noupx --onefile examples\rest-api-cli\sftpgo_api_cli - - name: Gather cross build info id: cross_info if: ${{ matrix.os == 'ubuntu-latest' }} @@ -170,7 +155,7 @@ jobs: - name: Prepare Release for Linux/macOS if: startsWith(matrix.os, 'windows-') != true run: | - mkdir -p output/{init,examples/rest-api-cli,sqlite,bash_completion,zsh_completion} + mkdir -p output/{init,sqlite,bash_completion,zsh_completion} echo "For documentation please take a look here:" > output/README.txt echo "" >> output/README.txt echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt @@ -190,7 +175,6 @@ jobs: ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen man -d output/man/man1 gzip output/man/man1/* - cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/ if [ $OS == 'linux' ] then cp -r output output_arm64 @@ -254,7 +238,6 @@ jobs: copy .\sftpgo.exe .\output copy .\sftpgo.json .\output copy .\sftpgo.db .\output - copy .\dist\sftpgo_api_cli.exe .\output copy .\LICENSE .\output\LICENSE.txt mkdir output\templates xcopy .\templates .\output\templates\ /E @@ -268,11 +251,10 @@ jobs: - name: Prepare Portable Release for Windows if: startsWith(matrix.os, 'windows-') run: | - mkdir win-portable\examples\rest-api-cli + mkdir win-portable copy .\sftpgo.exe .\win-portable copy .\sftpgo.json .\win-portable copy .\sftpgo.db .\win-portable - copy .\dist\sftpgo_api_cli.exe .\win-portable\examples\rest-api-cli copy .\LICENSE .\win-portable\LICENSE.txt mkdir win-portable\templates xcopy .\templates .\win-portable\templates\ /E diff --git a/README.md b/README.md index ff149535..268a864a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy - Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP/FTP/WebDAV service without losing the information about the client's address. - [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection. - [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections. -- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts. +- Easy [migration](./examples/convertusers) from Linux system user accounts. - [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand. - [SFTP subsystem mode](./docs/sftp-subsystem.md): you can use SFTPGo as OpenSSH's SFTP subsystem. - Performance analysis using built-in [profiler](./docs/profiling.md). @@ -147,7 +147,6 @@ After starting SFTPGo you can manage users and folders using: - the [web based administration interface](./docs/web-admin.md) - the [REST API](./docs/rest-api.md) -- the sample [REST API CLI](./examples/rest-api-cli) To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API. diff --git a/cmd/startsubsys.go b/cmd/startsubsys.go index 5761bd13..9680db8d 100644 --- a/cmd/startsubsys.go +++ b/cmd/startsubsys.go @@ -84,7 +84,7 @@ Command-line flags should be specified in the Subsystem declaration. dataProviderConf.PreferDatabaseCredentials = true } config.SetProviderConf(dataProviderConf) - err = dataprovider.Initialize(dataProviderConf, configDir) + err = dataprovider.Initialize(dataProviderConf, configDir, false) if err != nil { logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err) os.Exit(1) diff --git a/common/common_test.go b/common/common_test.go index c4e413fb..7eb0e332 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -179,7 +179,7 @@ func initializeDataprovider(trackQuota int) (string, error) { if trackQuota >= 0 && trackQuota <= 2 { cfg.Config.TrackQuota = trackQuota } - return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir) + return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir, true) } func closeDataprovider() error { diff --git a/common/connection_test.go b/common/connection_test.go index 4fb3bc3c..f2e2553b 100644 --- a/common/connection_test.go +++ b/common/connection_test.go @@ -1057,10 +1057,10 @@ func TestHasSpace(t *testing.T) { quotaResult = c.HasSpace(true, "/vdir/file1") assert.False(t, quotaResult.HasSpace) - err = dataprovider.DeleteUser(&user) + err = dataprovider.DeleteUser(user.Username) assert.NoError(t, err) - err = dataprovider.DeleteFolder(&folder) + err = dataprovider.DeleteFolder(folder.MappedPath) assert.NoError(t, err) } @@ -1133,7 +1133,7 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, folder2.UsedQuotaFiles) assert.Equal(t, int64(100), folder2.UsedQuotaSize) - user, err = dataprovider.GetUserByID(user.ID) + user, err = dataprovider.UserExists(user.Username) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(100), user.UsedQuotaSize) @@ -1143,16 +1143,16 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 2, folder2.UsedQuotaFiles) assert.Equal(t, int64(200), folder2.UsedQuotaSize) - user, err = dataprovider.GetUserByID(user.ID) + user, err = dataprovider.UserExists(user.Username) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(100), user.UsedQuotaSize) - err = dataprovider.DeleteUser(&user) + err = dataprovider.DeleteUser(user.Username) assert.NoError(t, err) - err = dataprovider.DeleteFolder(&folder1) + err = dataprovider.DeleteFolder(folder1.MappedPath) assert.NoError(t, err) - err = dataprovider.DeleteFolder(&folder2) + err = dataprovider.DeleteFolder(folder2.MappedPath) assert.NoError(t, err) } diff --git a/common/transfer.go b/common/transfer.go index 858bdae7..093e2cb7 100644 --- a/common/transfer.go +++ b/common/transfer.go @@ -293,8 +293,8 @@ func (t *BaseTransfer) HandleThrottle() { if wantedBandwidth > 0 { // real and wanted elapsed as milliseconds, bytes as kilobytes realElapsed := time.Since(t.start).Nanoseconds() / 1000000 - // trasferredBytes / 1000 = KB/s, we multiply for 1000 to get milliseconds - wantedElapsed := 1000 * (trasferredBytes / 1000) / wantedBandwidth + // trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds + wantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth if wantedElapsed > realElapsed { toSleep := time.Duration(wantedElapsed - realElapsed) time.Sleep(toSleep * time.Millisecond) diff --git a/common/transfer_test.go b/common/transfer_test.go index c7956add..35042d61 100644 --- a/common/transfer_test.go +++ b/common/transfer_test.go @@ -57,8 +57,8 @@ func TestTransferThrottling(t *testing.T) { } fs := vfs.NewOsFs("", os.TempDir(), nil) testFileSize := int64(131072) - wantedUploadElapsed := 1000 * (testFileSize / 1000) / u.UploadBandwidth - wantedDownloadElapsed := 1000 * (testFileSize / 1000) / u.DownloadBandwidth + wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth + wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth // some tolerance wantedUploadElapsed -= wantedDownloadElapsed / 10 wantedDownloadElapsed -= wantedDownloadElapsed / 10 diff --git a/config/config.go b/config/config.go index f5030ea9..ecf9088c 100644 --- a/config/config.go +++ b/config/config.go @@ -175,7 +175,6 @@ func Init() { Password: "", ConnectionString: "", SQLTablesPrefix: "", - ManageUsers: 1, SSLMode: 0, TrackQuota: 1, PoolSize: 0, @@ -208,7 +207,6 @@ func Init() { TemplatesPath: "templates", StaticFilesPath: "static", BackupsPath: "backups", - AuthUserFile: "", CertificateFile: "", CertificateKeyFile: "", }, @@ -749,7 +747,6 @@ func setViperDefaults() { viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode) viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString) viper.SetDefault("data_provider.sql_tables_prefix", globalConf.ProviderConf.SQLTablesPrefix) - viper.SetDefault("data_provider.manage_users", globalConf.ProviderConf.ManageUsers) viper.SetDefault("data_provider.track_quota", globalConf.ProviderConf.TrackQuota) viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize) viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir) @@ -773,7 +770,6 @@ func setViperDefaults() { viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath) viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath) viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath) - viper.SetDefault("httpd.auth_user_file", globalConf.HTTPDConfig.AuthUserFile) viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile) viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile) viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout) diff --git a/dataprovider/admin.go b/dataprovider/admin.go new file mode 100644 index 00000000..dc4287b6 --- /dev/null +++ b/dataprovider/admin.go @@ -0,0 +1,228 @@ +package dataprovider + +import ( + "encoding/base64" + "errors" + "fmt" + "net" + "regexp" + "strings" + + "github.com/alexedwards/argon2id" + "github.com/minio/sha256-simd" + + "github.com/drakkan/sftpgo/utils" +) + +// Available permissions for SFTPGo admins +const ( + PermAdminAny = "*" + PermAdminAddUsers = "add_users" + PermAdminChangeUsers = "edit_users" + PermAdminDeleteUsers = "del_users" + PermAdminViewUsers = "view_users" + PermAdminViewConnections = "view_conns" + PermAdminCloseConnections = "close_conns" + PermAdminViewServerStatus = "view_status" + PermAdminManageAdmins = "manage_admins" + PermAdminQuotaScans = "quota_scans" + PermAdminManageSystem = "manage_system" + PermAdminManageDefender = "manage_defender" + PermAdminViewDefender = "view_defender" +) + +var ( + emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$") + validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers, + PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus, + PermAdminManageAdmins, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender, + PermAdminViewDefender} +) + +// AdminFilters defines additional restrictions for SFTPGo admins +type AdminFilters struct { + // only clients connecting from these IP/Mask are allowed. + // IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291 + // for example "192.0.2.0/24" or "2001:db8::/32" + AllowList []string `json:"allow_list,omitempty"` +} + +// Admin defines a SFTPGo admin +type Admin struct { + // Database unique identifier + ID int64 `json:"id"` + // 1 enabled, 0 disabled (login is not allowed) + Status int `json:"status"` + // Username + Username string `json:"username"` + Password string `json:"password,omitempty"` + Email string `json:"email"` + Permissions []string `json:"permissions"` + Filters AdminFilters `json:"filters,omitempty"` + AdditionalInfo string `json:"additional_info,omitempty"` +} + +func (a *Admin) validate() error { + if a.Username == "" { + return &ValidationError{err: "username is mandatory"} + } + if a.Password == "" { + return &ValidationError{err: "please set a password"} + } + if !usernameRegex.MatchString(a.Username) { + return &ValidationError{err: fmt.Sprintf("username %#v is not valid", a.Username)} + } + if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) { + pwd, err := argon2id.CreateHash(a.Password, argon2Params) + if err != nil { + return err + } + a.Password = pwd + } + a.Permissions = utils.RemoveDuplicates(a.Permissions) + if len(a.Permissions) == 0 { + return &ValidationError{err: "please grant some permissions to this admin"} + } + if utils.IsStringInSlice(PermAdminAny, a.Permissions) { + a.Permissions = []string{PermAdminAny} + } + for _, perm := range a.Permissions { + if !utils.IsStringInSlice(perm, validAdminPerms) { + return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", perm)} + } + } + if a.Email != "" && !emailRegex.MatchString(a.Email) { + return &ValidationError{err: fmt.Sprintf("email %#v is not valid", a.Email)} + } + for _, IPMask := range a.Filters.AllowList { + _, _, err := net.ParseCIDR(IPMask) + if err != nil { + return &ValidationError{err: fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)} + } + } + + return nil +} + +// CheckPassword verifies the admin password +func (a *Admin) CheckPassword(password string) (bool, error) { + return argon2id.ComparePasswordAndHash(password, a.Password) +} + +// CanLoginFromIP returns true if login from the given IP is allowed +func (a *Admin) CanLoginFromIP(ip string) bool { + if len(a.Filters.AllowList) == 0 { + return true + } + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return len(a.Filters.AllowList) == 0 + } + + for _, ipMask := range a.Filters.AllowList { + _, network, err := net.ParseCIDR(ipMask) + if err != nil { + continue + } + if network.Contains(parsedIP) { + return true + } + } + return false +} + +func (a *Admin) checkUserAndPass(password, ip string) error { + if a.Status != 1 { + return fmt.Errorf("admin %#v is disabled", a.Username) + } + if a.Password == "" || password == "" { + return errors.New("credentials cannot be null or empty") + } + match, err := a.CheckPassword(password) + if err != nil { + return err + } + if !match { + return ErrInvalidCredentials + } + if !a.CanLoginFromIP(ip) { + return fmt.Errorf("login from IP %v not allowed", ip) + } + return nil +} + +// HideConfidentialData hides admin confidential data +func (a *Admin) HideConfidentialData() { + a.Password = "" +} + +// HasPermission returns true if the admin has the specified permission +func (a *Admin) HasPermission(perm string) bool { + if utils.IsStringInSlice(PermAdminAny, a.Permissions) { + return true + } + return utils.IsStringInSlice(perm, a.Permissions) +} + +// GetPermissionsAsString returns permission as string +func (a *Admin) GetPermissionsAsString() string { + return strings.Join(a.Permissions, ", ") +} + +// GetAllowedIPAsString returns the allowed IP as comma separated string +func (a *Admin) GetAllowedIPAsString() string { + return strings.Join(a.Filters.AllowList, ",") +} + +// GetValidPerms returns the allowed admin permissions +func (a *Admin) GetValidPerms() []string { + return validAdminPerms +} + +// GetInfoString returns admin's info as string. +func (a *Admin) GetInfoString() string { + var result string + if a.Email != "" { + result = fmt.Sprintf("Email: %v. ", a.Email) + } + if len(a.Filters.AllowList) > 0 { + result += fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList)) + } + return result +} + +// GetSignature returns a signature for this admin. +// It could change after an update +func (a *Admin) GetSignature() string { + data := []byte(a.Username) + data = append(data, []byte(a.Password)...) + signature := sha256.Sum256(data) + return base64.StdEncoding.EncodeToString(signature[:]) +} + +func (a *Admin) getACopy() Admin { + permissions := make([]string, len(a.Permissions)) + copy(permissions, a.Permissions) + filters := AdminFilters{} + filters.AllowList = make([]string, len(a.Filters.AllowList)) + copy(filters.AllowList, a.Filters.AllowList) + + return Admin{ + ID: a.ID, + Status: a.Status, + Username: a.Username, + Password: a.Password, + Email: a.Email, + Permissions: permissions, + Filters: filters, + AdditionalInfo: a.AdditionalInfo, + } +} + +// setDefaults sets the appropriate value for the default admin +func (a *Admin) setDefaults() { + a.Username = "admin" + a.Password = "password" + a.Status = 1 + a.Permissions = []string{PermAdminAny} +} diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index 036c6714..e6e76f64 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -3,7 +3,6 @@ package dataprovider import ( - "encoding/binary" "encoding/json" "errors" "fmt" @@ -23,11 +22,12 @@ const ( ) var ( - usersBucket = []byte("users") - usersIDIdxBucket = []byte("users_id_idx") - foldersBucket = []byte("folders") - dbVersionBucket = []byte("db_version") - dbVersionKey = []byte("version") + usersBucket = []byte("users") + //usersIDIdxBucket = []byte("users_id_idx") + foldersBucket = []byte("folders") + adminsBucket = []byte("admins") + dbVersionBucket = []byte("db_version") + dbVersionKey = []byte("version") ) // BoltProvider auth provider for bolt key/value store @@ -63,10 +63,6 @@ func initializeBoltProvider(basePath string) error { providerLog(logger.LevelWarn, "error creating users bucket: %v", err) return err } - err = dbHandle.Update(func(tx *bolt.Tx) error { - _, e := tx.CreateBucketIfNotExists(usersIDIdxBucket) - return e - }) if err != nil { providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err) return err @@ -76,7 +72,15 @@ func initializeBoltProvider(basePath string) error { return e }) if err != nil { - providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err) + providerLog(logger.LevelWarn, "error creating folders bucket: %v", err) + return err + } + err = dbHandle.Update(func(tx *bolt.Tx) error { + _, e := tx.CreateBucketIfNotExists(adminsBucket) + return e + }) + if err != nil { + providerLog(logger.LevelWarn, "error creating admins bucket: %v", err) return err } err = dbHandle.Update(func(tx *bolt.Tx) error { @@ -87,19 +91,19 @@ func initializeBoltProvider(basePath string) error { providerLog(logger.LevelWarn, "error creating database version bucket: %v", err) return err } - provider = BoltProvider{dbHandle: dbHandle} + provider = &BoltProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating bolt key/value store handler: %v", err) } return err } -func (p BoltProvider) checkAvailability() error { +func (p *BoltProvider) checkAvailability() error { _, err := getBoltDatabaseVersion(p.dbHandle) return err } -func (p BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { +func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { var user User if password == "" { return user, errors.New("Credentials cannot be null or empty") @@ -112,7 +116,17 @@ func (p BoltProvider) validateUserAndPass(username, password, ip, protocol strin return checkUserAndPass(user, password, ip, protocol) } -func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) { +func (p *BoltProvider) validateAdminAndPass(username, password, ip string) (Admin, error) { + admin, err := p.adminExists(username) + if err != nil { + providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err) + return admin, err + } + err = admin.checkUserAndPass(password, ip) + return admin, err +} + +func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) { var user User if len(pubKey) == 0 { return user, "", errors.New("Credentials cannot be null or empty") @@ -125,36 +139,9 @@ func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (Use return checkUserAndPubKey(user, pubKey) } -func (p BoltProvider) getUserByID(ID int64) (User, error) { - var user User - err := p.dbHandle.View(func(tx *bolt.Tx) error { - bucket, idxBucket, err := getBuckets(tx) - if err != nil { - return err - } - userIDAsBytes := itob(ID) - username := idxBucket.Get(userIDAsBytes) - if username == nil { - return &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)} - } - u := bucket.Get(username) - if u == nil { - return &RecordNotFoundError{err: fmt.Sprintf("username %#v and ID: %v does not exist", string(username), ID)} - } - folderBucket, err := getFolderBucket(tx) - if err != nil { - return err - } - user, err = joinUserAndFolders(u, folderBucket) - return err - }) - - return user, err -} - -func (p BoltProvider) updateLastLogin(username string) error { +func (p *BoltProvider) updateLastLogin(username string) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -182,9 +169,9 @@ func (p BoltProvider) updateLastLogin(username string) error { }) } -func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -216,7 +203,7 @@ func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, }) } -func (p BoltProvider) getUsedQuota(username string) (int, int64, error) { +func (p *BoltProvider) getUsedQuota(username string) (int, int64, error) { user, err := p.userExists(username) if err != nil { providerLog(logger.LevelWarn, "unable to get quota for user %v error: %v", username, err) @@ -225,10 +212,173 @@ func (p BoltProvider) getUsedQuota(username string) (int, int64, error) { return user.UsedQuotaFiles, user.UsedQuotaSize, err } -func (p BoltProvider) userExists(username string) (User, error) { +func (p *BoltProvider) adminExists(username string) (Admin, error) { + var admin Admin + + err := p.dbHandle.View(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + a := bucket.Get([]byte(username)) + if a == nil { + return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", username)} + } + return json.Unmarshal(a, &admin) + }) + + return admin, err +} + +func (p *BoltProvider) addAdmin(admin *Admin) error { + err := admin.validate() + if err != nil { + return err + } + return p.dbHandle.Update(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + if a := bucket.Get([]byte(admin.Username)); a != nil { + return fmt.Errorf("admin %v already exists", admin.Username) + } + id, err := bucket.NextSequence() + if err != nil { + return err + } + admin.ID = int64(id) + buf, err := json.Marshal(admin) + if err != nil { + return err + } + return bucket.Put([]byte(admin.Username), buf) + }) +} + +func (p *BoltProvider) updateAdmin(admin *Admin) error { + err := admin.validate() + if err != nil { + return err + } + return p.dbHandle.Update(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + var a []byte + + if a = bucket.Get([]byte(admin.Username)); a == nil { + return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)} + } + var oldAdmin Admin + err = json.Unmarshal(a, &oldAdmin) + if err != nil { + return err + } + + admin.ID = oldAdmin.ID + buf, err := json.Marshal(admin) + if err != nil { + return err + } + return bucket.Put([]byte(admin.Username), buf) + }) +} + +func (p *BoltProvider) deleteAdmin(admin *Admin) error { + return p.dbHandle.Update(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + + if bucket.Get([]byte(admin.Username)) == nil { + return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)} + } + + return bucket.Delete([]byte(admin.Username)) + }) +} + +func (p *BoltProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) { + admins := make([]Admin, 0, limit) + + err := p.dbHandle.View(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + cursor := bucket.Cursor() + itNum := 0 + if order == OrderASC { + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + itNum++ + if itNum <= offset { + continue + } + var admin Admin + err = json.Unmarshal(v, &admin) + if err != nil { + return err + } + admin.HideConfidentialData() + admins = append(admins, admin) + if len(admins) >= limit { + break + } + } + } else { + for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() { + itNum++ + if itNum <= offset { + continue + } + var admin Admin + err = json.Unmarshal(v, &admin) + if err != nil { + return err + } + admin.HideConfidentialData() + admins = append(admins, admin) + if len(admins) >= limit { + break + } + } + } + return err + }) + + return admins, err +} + +func (p *BoltProvider) dumpAdmins() ([]Admin, error) { + admins := make([]Admin, 0, 30) + err := p.dbHandle.View(func(tx *bolt.Tx) error { + bucket, err := getAdminBucket(tx) + if err != nil { + return err + } + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var admin Admin + err = json.Unmarshal(v, &admin) + if err != nil { + return err + } + admins = append(admins, admin) + } + return err + }) + + return admins, err +} + +func (p *BoltProvider) userExists(username string) (User, error) { var user User err := p.dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -246,13 +396,13 @@ func (p BoltProvider) userExists(username string) (User, error) { return user, err } -func (p BoltProvider) addUser(user *User) error { +func (p *BoltProvider) addUser(user *User) error { err := validateUser(user) if err != nil { return err } return p.dbHandle.Update(func(tx *bolt.Tx) error { - bucket, idxBucket, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -282,22 +432,17 @@ func (p BoltProvider) addUser(user *User) error { if err != nil { return err } - err = bucket.Put([]byte(user.Username), buf) - if err != nil { - return err - } - userIDAsBytes := itob(user.ID) - return idxBucket.Put(userIDAsBytes, []byte(user.Username)) + return bucket.Put([]byte(user.Username), buf) }) } -func (p BoltProvider) updateUser(user *User) error { +func (p *BoltProvider) updateUser(user *User) error { err := validateUser(user) if err != nil { return err } return p.dbHandle.Update(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -339,9 +484,9 @@ func (p BoltProvider) updateUser(user *User) error { }) } -func (p BoltProvider) deleteUser(user *User) error { +func (p *BoltProvider) deleteUser(user *User) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { - bucket, idxBucket, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -357,23 +502,18 @@ func (p BoltProvider) deleteUser(user *User) error { } } } - userIDAsBytes := itob(user.ID) - userName := idxBucket.Get(userIDAsBytes) - if userName == nil { - return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)} + exists := bucket.Get([]byte(user.Username)) + if exists == nil { + return &RecordNotFoundError{err: fmt.Sprintf("user %#v does not exist", user.Username)} } - err = bucket.Delete(userName) - if err != nil { - return err - } - return idxBucket.Delete(userIDAsBytes) + return bucket.Delete([]byte(user.Username)) }) } -func (p BoltProvider) dumpUsers() ([]User, error) { +func (p *BoltProvider) dumpUsers() ([]User, error) { users := make([]User, 0, 100) err := p.dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -398,35 +538,14 @@ func (p BoltProvider) dumpUsers() ([]User, error) { return users, err } -func (p BoltProvider) getUserWithUsername(username string) ([]User, error) { - users := []User{} - var user User - user, err := p.userExists(username) - if err == nil { - user.HideConfidentialData() - users = append(users, user) - return users, nil - } - if _, ok := err.(*RecordNotFoundError); ok { - err = nil - } - return users, err -} - -func (p BoltProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { +func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, error) { users := make([]User, 0, limit) var err error if limit <= 0 { return users, err } - if len(username) > 0 { - if offset == 0 { - return p.getUserWithUsername(username) - } - return users, err - } err = p.dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -472,7 +591,7 @@ func (p BoltProvider) getUsers(limit int, offset int, order string, username str return users, err } -func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { +func (p *BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { folders := make([]vfs.BaseVirtualFolder, 0, 50) err := p.dbHandle.View(func(tx *bolt.Tx) error { bucket, err := getFolderBucket(tx) @@ -493,7 +612,7 @@ func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { return folders, err } -func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { +func (p *BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { folders := make([]vfs.BaseVirtualFolder, 0, limit) var err error if limit <= 0 { @@ -554,7 +673,7 @@ func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([ return folders, err } -func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) { +func (p *BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) { var folder vfs.BaseVirtualFolder err := p.dbHandle.View(func(tx *bolt.Tx) error { bucket, err := getFolderBucket(tx) @@ -567,7 +686,7 @@ func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error return folder, err } -func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error { +func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error { err := validateFolder(folder) if err != nil { return err @@ -585,13 +704,13 @@ func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error { }) } -func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { +func (p *BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { bucket, err := getFolderBucket(tx) if err != nil { return err } - usersBucket, _, err := getBuckets(tx) + usersBucket, err := getUsersBucket(tx) if err != nil { return err } @@ -635,7 +754,7 @@ func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { }) } -func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { bucket, err := getFolderBucket(tx) if err != nil { @@ -666,7 +785,7 @@ func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd }) } -func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { +func (p *BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { folder, err := p.getFolderByPath(mappedPath) if err != nil { providerLog(logger.LevelWarn, "unable to get quota for folder %#v error: %v", mappedPath, err) @@ -675,20 +794,20 @@ func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) return folder.UsedQuotaFiles, folder.UsedQuotaSize, err } -func (p BoltProvider) close() error { +func (p *BoltProvider) close() error { return p.dbHandle.Close() } -func (p BoltProvider) reloadConfig() error { +func (p *BoltProvider) reloadConfig() error { return nil } // initializeDatabase does nothing, no initilization is needed for bolt provider -func (p BoltProvider) initializeDatabase() error { +func (p *BoltProvider) initializeDatabase() error { return ErrNoInitRequired } -func (p BoltProvider) migrateDatabase() error { +func (p *BoltProvider) migrateDatabase() error { dbVersion, err := getBoltDatabaseVersion(p.dbHandle) if err != nil { return err @@ -718,7 +837,7 @@ func (p BoltProvider) migrateDatabase() error { } } -func (p BoltProvider) revertDatabase(targetVersion int) error { +func (p *BoltProvider) revertDatabase(targetVersion int) error { dbVersion, err := getBoltDatabaseVersion(p.dbHandle) if err != nil { return err @@ -762,16 +881,12 @@ func updateBoltDatabaseFromV4(dbHandle *bolt.DB) error { return updateDatabaseFrom4To5(dbHandle) } -// itob returns an 8-byte big endian representation of v. -func itob(v int64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, uint64(v)) - return b -} - func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) { var user User err := json.Unmarshal(u, &user) + if err != nil { + return user, err + } if len(user.VirtualFolders) > 0 { var folders []vfs.VirtualFolder for _, folder := range user.VirtualFolders { @@ -872,7 +987,7 @@ func removeUserFromFolderMapping(folder vfs.VirtualFolder, user *User, bucket *b func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error { return dbHandle.Update(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -893,7 +1008,7 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error { return err } return dbHandle.Update(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -908,14 +1023,23 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error { }) } -func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) { +func getAdminBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + var err error + + bucket := tx.Bucket(adminsBucket) + if bucket == nil { + err = errors.New("unable to find admin bucket, bolt database structure not correcly defined") + } + return bucket, err +} + +func getUsersBucket(tx *bolt.Tx) (*bolt.Bucket, error) { var err error bucket := tx.Bucket(usersBucket) - idxBucket := tx.Bucket(usersIDIdxBucket) - if bucket == nil || idxBucket == nil { - err = fmt.Errorf("unable to find required buckets, bolt database structure not correcly defined") + if bucket == nil { + err = errors.New("unable to find required buckets, bolt database structure not correcly defined") } - return bucket, idxBucket, err + return bucket, err } func getFolderBucket(tx *bolt.Tx) (*bolt.Bucket, error) { @@ -954,7 +1078,7 @@ func updateDatabaseFrom2To3(dbHandle *bolt.DB) error { providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3") users := []User{} err := dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -1011,7 +1135,7 @@ func updateDatabaseFrom3To4(dbHandle *bolt.DB) error { foldersToScan := []string{} users := []userCompactVFolders{} err := dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -1075,7 +1199,7 @@ func downgradeBoltDatabaseFrom5To4(dbHandle *bolt.DB) error { providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4") users := []compatUserV4{} err := dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -1116,7 +1240,7 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error { providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5") users := []User{} err := dbHandle.View(func(tx *bolt.Tx) error { - bucket, _, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } @@ -1154,13 +1278,13 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error { func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) { usernames := []string{} err := dbHandle.View(func(tx *bolt.Tx) error { - _, idxBucket, err := getBuckets(tx) + bucket, err := getUsersBucket(tx) if err != nil { return err } - cursor := idxBucket.Cursor() - for k, v := cursor.First(); k != nil; k, v = cursor.Next() { - usernames = append(usernames, string(v)) + cursor := bucket.Cursor() + for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { + usernames = append(usernames, string(k)) } return nil }) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index f53d206b..d5722f62 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -24,6 +24,7 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -62,7 +63,7 @@ const ( MemoryDataProviderName = "memory" // DumpVersion defines the version for the dump. // For restore/load we support the current version and the previous one - DumpVersion = 5 + DumpVersion = 6 argonPwdPrefix = "$argon2id$" bcryptPwdPrefix = "$2a$" @@ -73,7 +74,6 @@ const ( 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" operationAdd = "add" operationUpdate = "update" @@ -123,9 +123,11 @@ var ( sqlTableUsers = "users" sqlTableFolders = "folders" sqlTableFoldersMapping = "folders_mapping" + sqlTableAdmins = "admins" sqlTableSchemaVersion = "schema_version" argon2Params *argon2id.Params lastLoginMinDelay = 10 * time.Minute + usernameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.~]+$") ) type schemaVersion struct { @@ -185,8 +187,6 @@ type Config struct { ConnectionString string `json:"connection_string" mapstructure:"connection_string"` // prefix for SQL tables SQLTablesPrefix string `json:"sql_tables_prefix" mapstructure:"sql_tables_prefix"` - // Set to 0 to disable users management, 1 to enable - ManageUsers int `json:"manage_users" mapstructure:"manage_users"` // Set the preferred way to track users quota between the following choices: // 0, disable quota tracking. REST API to scan user dir and update quota will do nothing // 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions @@ -277,6 +277,7 @@ type Config struct { type BackupData struct { Users []User `json:"users"` Folders []vfs.BaseVirtualFolder `json:"folders"` + Admins []Admin `json:"admins"` Version int `json:"version"` } @@ -334,6 +335,13 @@ func (e *ValidationError) Error() string { return fmt.Sprintf("Validation error: %s", e.err) } +// NewValidationError returns a validation errors +func NewValidationError(error string) *ValidationError { + return &ValidationError{ + err: error, + } +} + // MethodDisabledError raised if a method is disabled in config file. // For example, if user management is disabled, this error is raised // every time a user operation is done using the REST API @@ -370,9 +378,8 @@ type Provider interface { addUser(user *User) error updateUser(user *User) error deleteUser(user *User) error - getUsers(limit int, offset int, order string, username string) ([]User, error) + getUsers(limit int, offset int, order string) ([]User, error) dumpUsers() ([]User, error) - getUserByID(ID int64) (User, error) updateLastLogin(username string) error getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) @@ -381,6 +388,13 @@ type Provider interface { updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error getUsedFolderQuota(mappedPath string) (int, int64, error) dumpFolders() ([]vfs.BaseVirtualFolder, error) + adminExists(username string) (Admin, error) + addAdmin(admin *Admin) error + updateAdmin(admin *Admin) error + deleteAdmin(admin *Admin) error + getAdmins(limit int, offset int, order string) ([]Admin, error) + dumpAdmins() ([]Admin, error) + validateAdminAndPass(username, password, ip string) (Admin, error) checkAvailability() error close() error reloadConfig() error @@ -391,7 +405,7 @@ type Provider interface { // Initialize the data provider. // An error is returned if the configured driver is invalid or if the data provider cannot be initialized -func Initialize(cnf Config, basePath string) error { +func Initialize(cnf Config, basePath string, checkAdmins bool) error { var err error config = cnf @@ -408,6 +422,13 @@ func Initialize(cnf Config, basePath string) error { if err != nil { return err } + argon2Params = &argon2id.Params{ + Memory: cnf.PasswordHashing.Argon2Options.Memory, + Iterations: cnf.PasswordHashing.Argon2Options.Iterations, + Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism, + SaltLength: 16, + KeyLength: 32, + } if cnf.UpdateMode == 0 { err = provider.initializeDatabase() if err != nil && err != ErrNoInitRequired { @@ -423,16 +444,16 @@ func Initialize(cnf Config, basePath string) error { providerLog(logger.LevelWarn, "database migration error: %v", err) return err } + if checkAdmins { + err = checkDefaultAdmin() + if err != nil { + providerLog(logger.LevelWarn, "check default admin error: %v", err) + return err + } + } } else { providerLog(logger.LevelInfo, "database initialization/migration skipped, manual mode is configured") } - argon2Params = &argon2id.Params{ - Memory: cnf.PasswordHashing.Argon2Options.Memory, - Iterations: cnf.PasswordHashing.Argon2Options.Iterations, - Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism, - SaltLength: 16, - KeyLength: 32, - } startAvailabilityTimer() return nil } @@ -476,13 +497,29 @@ func validateSQLTablesPrefix() error { sqlTableUsers = config.SQLTablesPrefix + sqlTableUsers sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping + sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion - providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v schema version %#v", - sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableSchemaVersion) + providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v schema version %#v", + sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableAdmins, sqlTableSchemaVersion) } return nil } +func checkDefaultAdmin() error { + admins, err := provider.getAdmins(1, 0, OrderASC) + if err != nil { + return err + } + if len(admins) > 0 { + return nil + } + logger.Debug(logSender, "", "no admins found, try to create the default one") + // we need to create the default admin + admin := &Admin{} + admin.setDefaults() + return provider.addAdmin(admin) +} + // InitializeDatabase creates the initial database structure func InitializeDatabase(cnf Config, basePath string) error { config = cnf @@ -525,6 +562,11 @@ func RevertDatabase(cnf Config, basePath string, targetVersion int) error { return provider.revertDatabase(targetVersion) } +// CheckAdminAndPass validates the given admin and password connecting from ip +func CheckAdminAndPass(username, password, ip string) (Admin, error) { + return provider.validateAdminAndPass(username, password, ip) +} + // CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error func CheckUserAndPass(username, password, ip, protocol string) (User, error) { if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) { @@ -583,9 +625,6 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard // UpdateLastLogin updates the last login fields for the given SFTP user func UpdateLastLogin(user User) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin) diff := -time.Until(lastLogin) if diff < 0 || diff > lastLoginMinDelay { @@ -606,9 +645,6 @@ func UpdateUserQuota(user User, filesAdd int, sizeAdd int64, reset bool) error { } else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() { return nil } - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } if filesAdd == 0 && sizeAdd == 0 && !reset { return nil } @@ -621,9 +657,6 @@ func UpdateVirtualFolderQuota(vfolder vfs.BaseVirtualFolder, filesAdd int, sizeA if config.TrackQuota == 0 { return &MethodDisabledError{err: trackQuotaDisabledError} } - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } if filesAdd == 0 && sizeAdd == 0 && !reset { return nil } @@ -646,17 +679,37 @@ func GetUsedVirtualFolderQuota(mappedPath string) (int, int64, error) { return provider.getUsedFolderQuota(mappedPath) } -// UserExists checks if the given SFTP username exists, returns an error if no match is found +// AddAdmin adds a new SFTPGo admin +func AddAdmin(admin *Admin) error { + return provider.addAdmin(admin) +} + +// UpdateAdmin updates an existing SFTPGo admin +func UpdateAdmin(admin *Admin) error { + return provider.updateAdmin(admin) +} + +// DeleteAdmin deletes an existing SFTPGo admin +func DeleteAdmin(username string) error { + admin, err := provider.adminExists(username) + if err != nil { + return err + } + return provider.deleteAdmin(&admin) +} + +// AdminExists returns the given admins if it exists +func AdminExists(username string) (Admin, error) { + return provider.adminExists(username) +} + +// UserExists checks if the given SFTPGo username exists, returns an error if no match is found func UserExists(username string) (User, error) { return provider.userExists(username) } // AddUser adds a new SFTPGo user. -// ManageUsers configuration must be set to 1 to enable this method func AddUser(user *User) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } err := provider.addUser(user) if err == nil { go executeAction(operationAdd, *user) @@ -665,11 +718,7 @@ func AddUser(user *User) error { } // UpdateUser updates an existing SFTPGo user. -// ManageUsers configuration must be set to 1 to enable this method func UpdateUser(user *User) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } err := provider.updateUser(user) if err == nil { RemoveCachedWebDAVUser(user.Username) @@ -679,15 +728,15 @@ func UpdateUser(user *User) error { } // DeleteUser deletes an existing SFTPGo user. -// ManageUsers configuration must be set to 1 to enable this method -func DeleteUser(user *User) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} +func DeleteUser(username string) error { + user, err := provider.userExists(username) + if err != nil { + return err } - err := provider.deleteUser(user) + err = provider.deleteUser(&user) if err == nil { RemoveCachedWebDAVUser(user.Username) - go executeAction(operationDelete, *user) + go executeAction(operationDelete, user) } return err } @@ -699,32 +748,28 @@ func ReloadConfig() error { return provider.reloadConfig() } -// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty -func GetUsers(limit, offset int, order string, username string) ([]User, error) { - return provider.getUsers(limit, offset, order, username) +// GetAdmins returns an array of admins respecting limit and offset +func GetAdmins(limit, offset int, order string) ([]Admin, error) { + return provider.getAdmins(limit, offset, order) } -// GetUserByID returns the user with the given database ID if a match is found or an error -func GetUserByID(ID int64) (User, error) { - return provider.getUserByID(ID) +// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty +func GetUsers(limit, offset int, order string) ([]User, error) { + return provider.getUsers(limit, offset, order) } // AddFolder adds a new virtual folder. -// ManageUsers configuration must be set to 1 to enable this method func AddFolder(folder *vfs.BaseVirtualFolder) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} - } return provider.addFolder(folder) } // DeleteFolder deletes an existing folder. -// ManageUsers configuration must be set to 1 to enable this method -func DeleteFolder(folder *vfs.BaseVirtualFolder) error { - if config.ManageUsers == 0 { - return &MethodDisabledError{err: manageUsersDisabledError} +func DeleteFolder(folderPath string) error { + folder, err := provider.getFolderByPath(folderPath) + if err != nil { + return err } - return provider.deleteFolder(folder) + return provider.deleteFolder(&folder) } // GetFolderByPath returns the folder with the specified path if any @@ -740,7 +785,6 @@ func GetFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualF // DumpData returns all users and folders func DumpData() (BackupData, error) { var data BackupData - data.Version = DumpVersion users, err := provider.dumpUsers() if err != nil { return data, err @@ -749,8 +793,14 @@ func DumpData() (BackupData, error) { if err != nil { return data, err } + admins, err := provider.dumpAdmins() + if err != nil { + return data, err + } data.Users = users data.Folders = folders + data.Admins = admins + data.Version = DumpVersion return data, err } @@ -974,7 +1024,7 @@ func validatePermissions(user *User) error { if utils.IsStringInSlice(PermAny, perms) { permissions[cleanedDir] = []string{PermAny} } else { - permissions[cleanedDir] = perms + permissions[cleanedDir] = utils.RemoveDuplicates(perms) } } user.Permissions = permissions @@ -1238,6 +1288,9 @@ func validateBaseParams(user *User) error { if user.Username == "" { return &ValidationError{err: "username is mandatory"} } + if !usernameRegex.MatchString(user.Username) { + return &ValidationError{err: fmt.Sprintf("username %#v is not valid", user.Username)} + } if user.HomeDir == "" { return &ValidationError{err: "home_dir is mandatory"} } @@ -1251,7 +1304,7 @@ func validateBaseParams(user *User) error { } func createUserPasswordHash(user *User) error { - if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) { + if user.Password != "" && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) { pwd, err := argon2id.CreateHash(user.Password, argon2Params) if err != nil { return err diff --git a/dataprovider/memory.go b/dataprovider/memory.go index ed935e5b..197fd442 100644 --- a/dataprovider/memory.go +++ b/dataprovider/memory.go @@ -26,14 +26,16 @@ type memoryProviderHandle struct { isClosed bool // slice with ordered usernames usernames []string - // mapping between ID and username - usersIdx map[int64]string // map for users, username is the key users map[string]User // map for virtual folders, MappedPath is the key vfolders map[string]vfs.BaseVirtualFolder // slice with ordered folders mapped path vfoldersPaths []string + // map for admins, username is the key + admins map[string]Admin + // slice with ordered admins + adminsUsernames []string } // MemoryProvider auth provider for a memory store @@ -50,15 +52,16 @@ func initializeMemoryProvider(basePath string) { configFile = filepath.Join(basePath, configFile) } } - provider = MemoryProvider{ + provider = &MemoryProvider{ dbHandle: &memoryProviderHandle{ - isClosed: false, - usernames: []string{}, - usersIdx: make(map[int64]string), - users: make(map[string]User), - vfolders: make(map[string]vfs.BaseVirtualFolder), - vfoldersPaths: []string{}, - configFile: configFile, + isClosed: false, + usernames: []string{}, + users: make(map[string]User), + vfolders: make(map[string]vfs.BaseVirtualFolder), + vfoldersPaths: []string{}, + admins: make(map[string]Admin), + adminsUsernames: []string{}, + configFile: configFile, }, } if err := provider.reloadConfig(); err != nil { @@ -67,7 +70,7 @@ func initializeMemoryProvider(basePath string) { } } -func (p MemoryProvider) checkAvailability() error { +func (p *MemoryProvider) checkAvailability() error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -76,7 +79,7 @@ func (p MemoryProvider) checkAvailability() error { return nil } -func (p MemoryProvider) close() error { +func (p *MemoryProvider) close() error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -86,7 +89,7 @@ func (p MemoryProvider) close() error { return nil } -func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { +func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { var user User if password == "" { return user, errors.New("Credentials cannot be null or empty") @@ -99,7 +102,7 @@ func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol str return checkUserAndPass(user, password, ip, protocol) } -func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) { +func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) { var user User if len(pubKey) == 0 { return user, "", errors.New("Credentials cannot be null or empty") @@ -112,19 +115,17 @@ func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (U return checkUserAndPubKey(user, pubKey) } -func (p MemoryProvider) getUserByID(ID int64) (User, error) { - p.dbHandle.Lock() - defer p.dbHandle.Unlock() - if p.dbHandle.isClosed { - return User{}, errMemoryProviderClosed +func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) { + admin, err := p.adminExists(username) + if err != nil { + providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err) + return admin, err } - if val, ok := p.dbHandle.usersIdx[ID]; ok { - return p.userExistsInternal(val) - } - return User{}, &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)} + err = admin.checkUserAndPass(password, ip) + return admin, err } -func (p MemoryProvider) updateLastLogin(username string) error { +func (p *MemoryProvider) updateLastLogin(username string) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -139,7 +140,7 @@ func (p MemoryProvider) updateLastLogin(username string) error { return nil } -func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -164,7 +165,7 @@ func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64 return nil } -func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) { +func (p *MemoryProvider) getUsedQuota(username string) (int, int64, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -178,7 +179,7 @@ func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) { return user.UsedQuotaFiles, user.UsedQuotaSize, err } -func (p MemoryProvider) addUser(user *User) error { +func (p *MemoryProvider) addUser(user *User) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -199,13 +200,12 @@ func (p MemoryProvider) addUser(user *User) error { user.LastLogin = 0 user.VirtualFolders = p.joinVirtualFoldersFields(user) p.dbHandle.users[user.Username] = user.getACopy() - p.dbHandle.usersIdx[user.ID] = user.Username p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username) sort.Strings(p.dbHandle.usernames) return nil } -func (p MemoryProvider) updateUser(user *User) error { +func (p *MemoryProvider) updateUser(user *User) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -227,12 +227,13 @@ func (p MemoryProvider) updateUser(user *User) error { user.UsedQuotaSize = u.UsedQuotaSize user.UsedQuotaFiles = u.UsedQuotaFiles user.LastLogin = u.LastLogin + user.ID = u.ID // pre-login and external auth hook will use the passed *user so save a copy p.dbHandle.users[user.Username] = user.getACopy() return nil } -func (p MemoryProvider) deleteUser(user *User) error { +func (p *MemoryProvider) deleteUser(user *User) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -246,7 +247,6 @@ func (p MemoryProvider) deleteUser(user *User) error { p.removeUserFromFolderMapping(oldFolder.MappedPath, u.Username) } delete(p.dbHandle.users, user.Username) - delete(p.dbHandle.usersIdx, user.ID) // this could be more efficient p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users)) for username := range p.dbHandle.users { @@ -256,7 +256,7 @@ func (p MemoryProvider) deleteUser(user *User) error { return nil } -func (p MemoryProvider) dumpUsers() ([]User, error) { +func (p *MemoryProvider) dumpUsers() ([]User, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() users := make([]User, 0, len(p.dbHandle.usernames)) @@ -276,7 +276,7 @@ func (p MemoryProvider) dumpUsers() ([]User, error) { return users, err } -func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { +func (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersPaths)) @@ -289,7 +289,7 @@ func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { return folders, nil } -func (p MemoryProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { +func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User, error) { users := make([]User, 0, limit) var err error p.dbHandle.Lock() @@ -300,16 +300,6 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s if limit <= 0 { return users, err } - if len(username) > 0 { - if offset == 0 { - user, err := p.userExistsInternal(username) - if err == nil { - user.HideConfidentialData() - users = append(users, user) - } - } - return users, err - } itNum := 0 if order == OrderASC { for _, username := range p.dbHandle.usernames { @@ -344,7 +334,7 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s return users, err } -func (p MemoryProvider) userExists(username string) (User, error) { +func (p *MemoryProvider) userExists(username string) (User, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -353,14 +343,152 @@ func (p MemoryProvider) userExists(username string) (User, error) { return p.userExistsInternal(username) } -func (p MemoryProvider) userExistsInternal(username string) (User, error) { +func (p *MemoryProvider) userExistsInternal(username string) (User, error) { if val, ok := p.dbHandle.users[username]; ok { return val.getACopy(), nil } return User{}, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)} } -func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *MemoryProvider) addAdmin(admin *Admin) error { + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + if p.dbHandle.isClosed { + return errMemoryProviderClosed + } + err := admin.validate() + if err != nil { + return err + } + _, err = p.adminExistsInternal(admin.Username) + if err == nil { + return fmt.Errorf("admin %#v already exists", admin.Username) + } + admin.ID = p.getNextAdminID() + p.dbHandle.admins[admin.Username] = admin.getACopy() + p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username) + sort.Strings(p.dbHandle.adminsUsernames) + return nil +} + +func (p *MemoryProvider) updateAdmin(admin *Admin) error { + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + if p.dbHandle.isClosed { + return errMemoryProviderClosed + } + err := admin.validate() + if err != nil { + return err + } + a, err := p.adminExistsInternal(admin.Username) + if err != nil { + return err + } + admin.ID = a.ID + p.dbHandle.admins[admin.Username] = admin.getACopy() + return nil +} + +func (p *MemoryProvider) deleteAdmin(admin *Admin) error { + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + if p.dbHandle.isClosed { + return errMemoryProviderClosed + } + _, err := p.adminExistsInternal(admin.Username) + if err != nil { + return err + } + + delete(p.dbHandle.admins, admin.Username) + // this could be more efficient + p.dbHandle.adminsUsernames = make([]string, 0, len(p.dbHandle.admins)) + for username := range p.dbHandle.admins { + p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username) + } + sort.Strings(p.dbHandle.adminsUsernames) + return nil +} + +func (p *MemoryProvider) adminExists(username string) (Admin, error) { + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + if p.dbHandle.isClosed { + return Admin{}, errMemoryProviderClosed + } + return p.adminExistsInternal(username) +} + +func (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) { + if val, ok := p.dbHandle.admins[username]; ok { + return val.getACopy(), nil + } + return Admin{}, &RecordNotFoundError{err: fmt.Sprintf("admin %#v does not exist", username)} +} + +func (p *MemoryProvider) dumpAdmins() ([]Admin, error) { + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + + admins := make([]Admin, 0, len(p.dbHandle.admins)) + if p.dbHandle.isClosed { + return admins, errMemoryProviderClosed + } + for _, admin := range p.dbHandle.admins { + admins = append(admins, admin) + } + return admins, nil +} + +func (p *MemoryProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) { + admins := make([]Admin, 0, limit) + + p.dbHandle.Lock() + defer p.dbHandle.Unlock() + + if p.dbHandle.isClosed { + return admins, errMemoryProviderClosed + } + if limit <= 0 { + return admins, nil + } + itNum := 0 + if order == OrderASC { + for _, username := range p.dbHandle.adminsUsernames { + itNum++ + if itNum <= offset { + continue + } + a := p.dbHandle.admins[username] + admin := a.getACopy() + admin.HideConfidentialData() + admins = append(admins, admin) + if len(admins) >= limit { + break + } + } + } else { + for i := len(p.dbHandle.adminsUsernames) - 1; i >= 0; i-- { + itNum++ + if itNum <= offset { + continue + } + username := p.dbHandle.adminsUsernames[i] + a := p.dbHandle.admins[username] + admin := a.getACopy() + admin.HideConfidentialData() + admins = append(admins, admin) + if len(admins) >= limit { + break + } + } + } + + return admins, nil +} + +func (p *MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -383,7 +511,7 @@ func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeA return nil } -func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { +func (p *MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -397,7 +525,7 @@ func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error return folder.UsedQuotaFiles, folder.UsedQuotaSize, err } -func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder { +func (p *MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder { var folders []vfs.VirtualFolder for _, folder := range user.VirtualFolders { f, err := p.addOrGetFolderInternal(folder.MappedPath, user.Username, folder.UsedQuotaSize, folder.UsedQuotaFiles, @@ -413,7 +541,7 @@ func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder return folders } -func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) { +func (p *MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) { folder, err := p.folderExistsInternal(mappedPath) if err == nil { var usernames []string @@ -427,7 +555,7 @@ func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) } } -func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) { +func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) { p.dbHandle.vfolders[folder.MappedPath] = folder if !utils.IsStringInSlice(folder.MappedPath, p.dbHandle.vfoldersPaths) { p.dbHandle.vfoldersPaths = append(p.dbHandle.vfoldersPaths, folder.MappedPath) @@ -435,7 +563,7 @@ func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolde } } -func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) { +func (p *MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) { folder, err := p.folderExistsInternal(mappedPath) if _, ok := err.(*RecordNotFoundError); ok { folder := vfs.BaseVirtualFolder{ @@ -456,14 +584,14 @@ func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, used return folder, err } -func (p MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) { +func (p *MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) { if val, ok := p.dbHandle.vfolders[mappedPath]; ok { return val, nil } return vfs.BaseVirtualFolder{}, &RecordNotFoundError{err: fmt.Sprintf("folder %#v does not exist", mappedPath)} } -func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { +func (p *MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { folders := make([]vfs.BaseVirtualFolder, 0, limit) var err error p.dbHandle.Lock() @@ -514,7 +642,7 @@ func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string) return folders, err } -func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { +func (p *MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -523,7 +651,7 @@ func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolde return p.folderExistsInternal(mappedPath) } -func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error { +func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -544,7 +672,7 @@ func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error { return nil } -func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { +func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { @@ -577,17 +705,17 @@ func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { return nil } -func (p MemoryProvider) getNextID() int64 { +func (p *MemoryProvider) getNextID() int64 { nextID := int64(1) - for id := range p.dbHandle.usersIdx { - if id >= nextID { - nextID = id + 1 + for _, v := range p.dbHandle.users { + if v.ID >= nextID { + nextID = v.ID + 1 } } return nextID } -func (p MemoryProvider) getNextFolderID() int64 { +func (p *MemoryProvider) getNextFolderID() int64 { nextID := int64(1) for _, v := range p.dbHandle.vfolders { if v.ID >= nextID { @@ -597,17 +725,28 @@ func (p MemoryProvider) getNextFolderID() int64 { return nextID } -func (p MemoryProvider) clear() { +func (p *MemoryProvider) getNextAdminID() int64 { + nextID := int64(1) + for _, a := range p.dbHandle.admins { + if a.ID >= nextID { + nextID = a.ID + 1 + } + } + return nextID +} + +func (p *MemoryProvider) clear() { p.dbHandle.Lock() defer p.dbHandle.Unlock() p.dbHandle.usernames = []string{} - p.dbHandle.usersIdx = make(map[int64]string) p.dbHandle.users = make(map[string]User) p.dbHandle.vfoldersPaths = []string{} p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder) + p.dbHandle.admins = make(map[string]Admin) + p.dbHandle.adminsUsernames = []string{} } -func (p MemoryProvider) reloadConfig() error { +func (p *MemoryProvider) reloadConfig() error { if p.dbHandle.configFile == "" { providerLog(logger.LevelDebug, "no users configuration file defined") return nil @@ -676,14 +815,14 @@ func (p MemoryProvider) reloadConfig() error { } // initializeDatabase does nothing, no initilization is needed for memory provider -func (p MemoryProvider) initializeDatabase() error { +func (p *MemoryProvider) initializeDatabase() error { return ErrNoInitRequired } -func (p MemoryProvider) migrateDatabase() error { +func (p *MemoryProvider) migrateDatabase() error { return ErrNoInitRequired } -func (p MemoryProvider) revertDatabase(targetVersion int) error { +func (p *MemoryProvider) revertDatabase(targetVersion int) error { return errors.New("memory provider does not store data, revert not possible") } diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index 9ea5f29c..1c7ac532 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -40,6 +40,10 @@ const ( "ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;" mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;" + mysqlV7SQL = "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " + + "`password` varchar(255) NOT NULL, `email` varchar(255) NULL, `status` integer NOT NULL, `permissions` longtext NOT NULL, " + + "`filters` longtext NULL, `additional_info` longtext NULL);" + mysqlV7DownSQL = "DROP TABLE `{{admins}}` CASCADE;" ) // MySQLProvider auth provider for MySQL/MariaDB database @@ -65,7 +69,7 @@ func initializeMySQLProvider() error { dbHandle.SetMaxIdleConns(2) } dbHandle.SetConnMaxLifetime(240 * time.Second) - provider = MySQLProvider{dbHandle: dbHandle} + provider = &MySQLProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v", getMySQLConnectionString(true), err) @@ -87,98 +91,122 @@ func getMySQLConnectionString(redactedPwd bool) string { return connectionString } -func (p MySQLProvider) checkAvailability() error { +func (p *MySQLProvider) checkAvailability() error { return sqlCommonCheckAvailability(p.dbHandle) } -func (p MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { +func (p *MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle) } -func (p MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { +func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) } -func (p MySQLProvider) getUserByID(ID int64) (User, error) { - return sqlCommonGetUserByID(ID, p.dbHandle) -} - -func (p MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p MySQLProvider) getUsedQuota(username string) (int, int64, error) { +func (p *MySQLProvider) getUsedQuota(username string) (int, int64, error) { return sqlCommonGetUsedQuota(username, p.dbHandle) } -func (p MySQLProvider) updateLastLogin(username string) error { +func (p *MySQLProvider) updateLastLogin(username string) error { return sqlCommonUpdateLastLogin(username, p.dbHandle) } -func (p MySQLProvider) userExists(username string) (User, error) { - return sqlCommonCheckUserExists(username, p.dbHandle) +func (p *MySQLProvider) userExists(username string) (User, error) { + return sqlCommonGetUserByUsername(username, p.dbHandle) } -func (p MySQLProvider) addUser(user *User) error { +func (p *MySQLProvider) addUser(user *User) error { return sqlCommonAddUser(user, p.dbHandle) } -func (p MySQLProvider) updateUser(user *User) error { +func (p *MySQLProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p MySQLProvider) deleteUser(user *User) error { +func (p *MySQLProvider) deleteUser(user *User) error { return sqlCommonDeleteUser(user, p.dbHandle) } -func (p MySQLProvider) dumpUsers() ([]User, error) { +func (p *MySQLProvider) dumpUsers() ([]User, error) { return sqlCommonDumpUsers(p.dbHandle) } -func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { - return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) +func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) { + return sqlCommonGetUsers(limit, offset, order, p.dbHandle) } -func (p MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { +func (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { return sqlCommonDumpFolders(p.dbHandle) } -func (p MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { +func (p *MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle) } -func (p MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { +func (p *MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) } -func (p MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { +func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonAddFolder(folder, p.dbHandle) } -func (p MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { +func (p *MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonDeleteFolder(folder, p.dbHandle) } -func (p MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { +func (p *MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle) } -func (p MySQLProvider) close() error { +func (p *MySQLProvider) adminExists(username string) (Admin, error) { + return sqlCommonGetAdminByUsername(username, p.dbHandle) +} + +func (p *MySQLProvider) addAdmin(admin *Admin) error { + return sqlCommonAddAdmin(admin, p.dbHandle) +} + +func (p *MySQLProvider) updateAdmin(admin *Admin) error { + return sqlCommonUpdateAdmin(admin, p.dbHandle) +} + +func (p *MySQLProvider) deleteAdmin(admin *Admin) error { + return sqlCommonDeleteAdmin(admin, p.dbHandle) +} + +func (p *MySQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) { + return sqlCommonGetAdmins(limit, offset, order, p.dbHandle) +} + +func (p *MySQLProvider) dumpAdmins() ([]Admin, error) { + return sqlCommonDumpAdmins(p.dbHandle) +} + +func (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) { + return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle) +} + +func (p *MySQLProvider) close() error { return p.dbHandle.Close() } -func (p MySQLProvider) reloadConfig() error { +func (p *MySQLProvider) reloadConfig() error { return nil } // initializeDatabase creates the initial database structure -func (p MySQLProvider) initializeDatabase() error { +func (p *MySQLProvider) initializeDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) if err == nil && dbVersion.Version > 0 { return ErrNoInitRequired @@ -206,7 +234,7 @@ func (p MySQLProvider) initializeDatabase() error { return tx.Commit() } -func (p MySQLProvider) migrateDatabase() error { +func (p *MySQLProvider) migrateDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -226,6 +254,8 @@ func (p MySQLProvider) migrateDatabase() error { return updateMySQLDatabaseFromV4(p.dbHandle) case 5: return updateMySQLDatabaseFromV5(p.dbHandle) + case 6: + return updateMySQLDatabaseFromV6(p.dbHandle) default: if dbVersion.Version > sqlDatabaseVersion { providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, @@ -238,7 +268,7 @@ func (p MySQLProvider) migrateDatabase() error { } } -func (p MySQLProvider) revertDatabase(targetVersion int) error { +func (p *MySQLProvider) revertDatabase(targetVersion int) error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -247,6 +277,16 @@ func (p MySQLProvider) revertDatabase(targetVersion int) error { return fmt.Errorf("current version match target version, nothing to do") } switch dbVersion.Version { + case 7: + err = downgradeMySQLDatabaseFrom7To6(p.dbHandle) + if err != nil { + return err + } + err = downgradeMySQLDatabaseFrom6To5(p.dbHandle) + if err != nil { + return err + } + return downgradeMySQLDatabaseFrom5To4(p.dbHandle) case 6: err = downgradeMySQLDatabaseFrom6To5(p.dbHandle) if err != nil { @@ -293,7 +333,15 @@ func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error { } func updateMySQLDatabaseFromV5(dbHandle *sql.DB) error { - return updateMySQLDatabaseFrom5To6(dbHandle) + err := updateMySQLDatabaseFrom5To6(dbHandle) + if err != nil { + return err + } + return updateMySQLDatabaseFromV6(dbHandle) +} + +func updateMySQLDatabaseFromV6(dbHandle *sql.DB) error { + return updateMySQLDatabaseFrom6To7(dbHandle) } func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error { @@ -325,6 +373,20 @@ func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error { return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) } +func updateMySQLDatabaseFrom6To7(dbHandle *sql.DB) error { + logger.InfoToConsole("updating database version: 6 -> 7") + providerLog(logger.LevelInfo, "updating database version: 6 -> 7") + sql := strings.Replace(mysqlV7SQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7) +} + +func downgradeMySQLDatabaseFrom7To6(dbHandle *sql.DB) error { + logger.InfoToConsole("downgrading database version: 7 -> 6") + providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6") + sql := strings.Replace(mysqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) +} + func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error { logger.InfoToConsole("downgrading database version: 6 -> 5") providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5") diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index 915f7c46..d628963b 100644 --- a/dataprovider/pgsql.go +++ b/dataprovider/pgsql.go @@ -40,6 +40,11 @@ CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id"); ` pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;` pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;` + pgsqlV7SQL = `CREATE TABLE "{{admins}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE, +"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL, +"filters" text NULL, "additional_info" text NULL); +` + pgsqlV7DownSQL = `DROP TABLE "{{admins}}" CASCADE;` ) // PGSQLProvider auth provider for PostgreSQL database @@ -65,7 +70,7 @@ func initializePGSQLProvider() error { dbHandle.SetMaxIdleConns(2) } dbHandle.SetConnMaxLifetime(240 * time.Second) - provider = PGSQLProvider{dbHandle: dbHandle} + provider = &PGSQLProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v", getPGSQLConnectionString(true), err) @@ -88,98 +93,122 @@ func getPGSQLConnectionString(redactedPwd bool) string { return connectionString } -func (p PGSQLProvider) checkAvailability() error { +func (p *PGSQLProvider) checkAvailability() error { return sqlCommonCheckAvailability(p.dbHandle) } -func (p PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { +func (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle) } -func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { +func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) } -func (p PGSQLProvider) getUserByID(ID int64) (User, error) { - return sqlCommonGetUserByID(ID, p.dbHandle) -} - -func (p PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p PGSQLProvider) getUsedQuota(username string) (int, int64, error) { +func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) { return sqlCommonGetUsedQuota(username, p.dbHandle) } -func (p PGSQLProvider) updateLastLogin(username string) error { +func (p *PGSQLProvider) updateLastLogin(username string) error { return sqlCommonUpdateLastLogin(username, p.dbHandle) } -func (p PGSQLProvider) userExists(username string) (User, error) { - return sqlCommonCheckUserExists(username, p.dbHandle) +func (p *PGSQLProvider) userExists(username string) (User, error) { + return sqlCommonGetUserByUsername(username, p.dbHandle) } -func (p PGSQLProvider) addUser(user *User) error { +func (p *PGSQLProvider) addUser(user *User) error { return sqlCommonAddUser(user, p.dbHandle) } -func (p PGSQLProvider) updateUser(user *User) error { +func (p *PGSQLProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p PGSQLProvider) deleteUser(user *User) error { +func (p *PGSQLProvider) deleteUser(user *User) error { return sqlCommonDeleteUser(user, p.dbHandle) } -func (p PGSQLProvider) dumpUsers() ([]User, error) { +func (p *PGSQLProvider) dumpUsers() ([]User, error) { return sqlCommonDumpUsers(p.dbHandle) } -func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { - return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) +func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) { + return sqlCommonGetUsers(limit, offset, order, p.dbHandle) } -func (p PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { +func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { return sqlCommonDumpFolders(p.dbHandle) } -func (p PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { +func (p *PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle) } -func (p PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { +func (p *PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) } -func (p PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { +func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonAddFolder(folder, p.dbHandle) } -func (p PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { +func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonDeleteFolder(folder, p.dbHandle) } -func (p PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { +func (p *PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle) } -func (p PGSQLProvider) close() error { +func (p *PGSQLProvider) adminExists(username string) (Admin, error) { + return sqlCommonGetAdminByUsername(username, p.dbHandle) +} + +func (p *PGSQLProvider) addAdmin(admin *Admin) error { + return sqlCommonAddAdmin(admin, p.dbHandle) +} + +func (p *PGSQLProvider) updateAdmin(admin *Admin) error { + return sqlCommonUpdateAdmin(admin, p.dbHandle) +} + +func (p *PGSQLProvider) deleteAdmin(admin *Admin) error { + return sqlCommonDeleteAdmin(admin, p.dbHandle) +} + +func (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) { + return sqlCommonGetAdmins(limit, offset, order, p.dbHandle) +} + +func (p *PGSQLProvider) dumpAdmins() ([]Admin, error) { + return sqlCommonDumpAdmins(p.dbHandle) +} + +func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) { + return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle) +} + +func (p *PGSQLProvider) close() error { return p.dbHandle.Close() } -func (p PGSQLProvider) reloadConfig() error { +func (p *PGSQLProvider) reloadConfig() error { return nil } // initializeDatabase creates the initial database structure -func (p PGSQLProvider) initializeDatabase() error { +func (p *PGSQLProvider) initializeDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) if err == nil && dbVersion.Version > 0 { return ErrNoInitRequired @@ -207,7 +236,7 @@ func (p PGSQLProvider) initializeDatabase() error { return tx.Commit() } -func (p PGSQLProvider) migrateDatabase() error { +func (p *PGSQLProvider) migrateDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -227,6 +256,8 @@ func (p PGSQLProvider) migrateDatabase() error { return updatePGSQLDatabaseFromV4(p.dbHandle) case 5: return updatePGSQLDatabaseFromV5(p.dbHandle) + case 6: + return updatePGSQLDatabaseFromV6(p.dbHandle) default: if dbVersion.Version > sqlDatabaseVersion { providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, @@ -239,7 +270,7 @@ func (p PGSQLProvider) migrateDatabase() error { } } -func (p PGSQLProvider) revertDatabase(targetVersion int) error { +func (p *PGSQLProvider) revertDatabase(targetVersion int) error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -248,6 +279,16 @@ func (p PGSQLProvider) revertDatabase(targetVersion int) error { return fmt.Errorf("current version match target version, nothing to do") } switch dbVersion.Version { + case 7: + err = downgradePGSQLDatabaseFrom7To6(p.dbHandle) + if err != nil { + return err + } + err = downgradePGSQLDatabaseFrom6To5(p.dbHandle) + if err != nil { + return err + } + return downgradePGSQLDatabaseFrom5To4(p.dbHandle) case 6: err = downgradePGSQLDatabaseFrom6To5(p.dbHandle) if err != nil { @@ -294,7 +335,15 @@ func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error { } func updatePGSQLDatabaseFromV5(dbHandle *sql.DB) error { - return updatePGSQLDatabaseFrom5To6(dbHandle) + err := updatePGSQLDatabaseFrom5To6(dbHandle) + if err != nil { + return err + } + return updatePGSQLDatabaseFromV6(dbHandle) +} + +func updatePGSQLDatabaseFromV6(dbHandle *sql.DB) error { + return updatePGSQLDatabaseFrom6To7(dbHandle) } func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error { @@ -326,6 +375,20 @@ func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error { return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) } +func updatePGSQLDatabaseFrom6To7(dbHandle *sql.DB) error { + logger.InfoToConsole("updating database version: 6 -> 7") + providerLog(logger.LevelInfo, "updating database version: 6 -> 7") + sql := strings.Replace(pgsqlV7SQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7) +} + +func downgradePGSQLDatabaseFrom7To6(dbHandle *sql.DB) error { + logger.InfoToConsole("downgrading database version: 7 -> 6") + providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6") + sql := strings.Replace(pgsqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) +} + func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error { logger.InfoToConsole("downgrading database version: 6 -> 5") providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5") diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index 4ab12c29..42c8aacc 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -14,7 +14,7 @@ import ( ) const ( - sqlDatabaseVersion = 6 + sqlDatabaseVersion = 7 initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);" defaultSQLQueryTimeout = 10 * time.Second longSQLQueryTimeout = 60 * time.Second @@ -26,7 +26,174 @@ type sqlQuerier interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } -func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) { +type sqlScanner interface { + Scan(dest ...interface{}) error +} + +func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) { + var admin Admin + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getAdminByUsernameQuery() + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return admin, err + } + defer stmt.Close() + row := stmt.QueryRowContext(ctx, username) + + return getAdminFromDbRow(row) +} + +func sqlCommonValidateAdminAndPass(username, password, ip string, dbHandle *sql.DB) (Admin, error) { + admin, err := sqlCommonGetAdminByUsername(username, dbHandle) + if err != nil { + providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err) + return admin, err + } + err = admin.checkUserAndPass(password, ip) + return admin, err +} + +func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error { + err := admin.validate() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getAddAdminQuery() + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return err + } + defer stmt.Close() + + perms, err := json.Marshal(admin.Permissions) + if err != nil { + return err + } + + filters, err := json.Marshal(admin.Filters) + if err != nil { + return err + } + + _, err = stmt.ExecContext(ctx, admin.Username, admin.Password, admin.Status, admin.Email, string(perms), + string(filters), admin.AdditionalInfo) + return err +} + +func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error { + err := admin.validate() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getUpdateAdminQuery() + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return err + } + defer stmt.Close() + + perms, err := json.Marshal(admin.Permissions) + if err != nil { + return err + } + + filters, err := json.Marshal(admin.Filters) + if err != nil { + return err + } + + _, err = stmt.ExecContext(ctx, admin.Password, admin.Status, admin.Email, string(perms), string(filters), + admin.AdditionalInfo, admin.Username) + return err +} + +func sqlCommonDeleteAdmin(admin *Admin, dbHandle *sql.DB) error { + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getDeleteAdminQuery() + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return err + } + defer stmt.Close() + _, err = stmt.ExecContext(ctx, admin.Username) + return err +} + +func sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([]Admin, error) { + admins := make([]Admin, 0, limit) + + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getAdminsQuery(order) + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return nil, err + } + defer stmt.Close() + + rows, err := stmt.QueryContext(ctx, limit, offset) + if err != nil { + return admins, err + } + defer rows.Close() + + for rows.Next() { + a, err := getAdminFromDbRow(rows) + if err != nil { + return admins, err + } + a.HideConfidentialData() + admins = append(admins, a) + } + + return admins, rows.Err() +} + +func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) { + admins := make([]Admin, 0, 30) + + ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) + defer cancel() + q := getDumpAdminsQuery() + stmt, err := dbHandle.PrepareContext(ctx, q) + if err != nil { + providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) + return nil, err + } + defer stmt.Close() + + rows, err := stmt.QueryContext(ctx) + if err != nil { + return admins, err + } + defer rows.Close() + + for rows.Next() { + a, err := getAdminFromDbRow(rows) + if err != nil { + return admins, err + } + admins = append(admins, a) + } + + return admins, rows.Err() +} + +func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, error) { var user User ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() @@ -39,7 +206,7 @@ func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) { defer stmt.Close() row := stmt.QueryRowContext(ctx, username) - user, err = getUserFromDbRow(row, nil) + user, err = getUserFromDbRow(row) if err != nil { return user, err } @@ -51,7 +218,7 @@ func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHan if password == "" { return user, errors.New("Credentials cannot be null or empty") } - user, err := getUserByUsername(username, dbHandle) + user, err := sqlCommonGetUserByUsername(username, dbHandle) if err != nil { providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err) return user, err @@ -64,7 +231,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sq if len(pubKey) == 0 { return user, "", errors.New("Credentials cannot be null or empty") } - user, err := getUserByUsername(username, dbHandle) + user, err := sqlCommonGetUserByUsername(username, dbHandle) if err != nil { providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err) return user, "", err @@ -78,26 +245,6 @@ func sqlCommonCheckAvailability(dbHandle *sql.DB) error { return dbHandle.PingContext(ctx) } -func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) { - var user User - ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) - defer cancel() - q := getUserByIDQuery() - stmt, err := dbHandle.PrepareContext(ctx, q) - if err != nil { - providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) - return user, err - } - defer stmt.Close() - - row := stmt.QueryRowContext(ctx, ID) - user, err = getUserFromDbRow(row, nil) - if err != nil { - return user, err - } - return getUserWithVirtualFolders(user, dbHandle) -} - func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error { ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() @@ -158,25 +305,6 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error { return err } -func sqlCommonCheckUserExists(username string, dbHandle *sql.DB) (User, error) { - var user User - ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) - defer cancel() - q := getUserByUsernameQuery() - stmt, err := dbHandle.PrepareContext(ctx, q) - if err != nil { - providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) - return user, err - } - defer stmt.Close() - row := stmt.QueryRowContext(ctx, username) - user, err = getUserFromDbRow(row, nil) - if err != nil { - return user, err - } - return getUserWithVirtualFolders(user, dbHandle) -} - func sqlCommonAddUser(user *User, dbHandle *sql.DB) error { err := validateUser(user) if err != nil { @@ -317,7 +445,7 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) { defer rows.Close() for rows.Next() { - u, err := getUserFromDbRow(nil, rows) + u, err := getUserFromDbRow(rows) if err != nil { return users, err } @@ -327,30 +455,30 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) { } users = append(users, u) } + err = rows.Err() + if err != nil { + return users, err + } return getUsersWithVirtualFolders(users, dbHandle) } -func sqlCommonGetUsers(limit int, offset int, order string, username string, dbHandle sqlQuerier) ([]User, error) { +func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier) ([]User, error) { users := make([]User, 0, limit) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() - q := getUsersQuery(order, username) + q := getUsersQuery(order) stmt, err := dbHandle.PrepareContext(ctx, q) if err != nil { providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) return nil, err } defer stmt.Close() - var rows *sql.Rows - if len(username) > 0 { - rows, err = stmt.QueryContext(ctx, username, limit, offset) //nolint:rowserrcheck // rows.Err() is checked - } else { - rows, err = stmt.QueryContext(ctx, limit, offset) //nolint:rowserrcheck // rows.Err() is checked - } + + rows, err := stmt.QueryContext(ctx, limit, offset) if err == nil { defer rows.Close() for rows.Next() { - u, err := getUserFromDbRow(nil, rows) + u, err := getUserFromDbRow(rows) if err != nil { return users, err } @@ -384,7 +512,47 @@ func updateUserPermissionsFromDb(user *User, permissions string) error { return err } -func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) { +func getAdminFromDbRow(row sqlScanner) (Admin, error) { + var admin Admin + var email, filters, additionalInfo, permissions sql.NullString + + err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions, + &filters, &additionalInfo) + + if err != nil { + if err == sql.ErrNoRows { + return admin, &RecordNotFoundError{err: err.Error()} + } + return admin, err + } + + if permissions.Valid { + var perms []string + err = json.Unmarshal([]byte(permissions.String), &perms) + if err != nil { + return admin, err + } + admin.Permissions = perms + } + + if email.Valid { + admin.Email = email.String + } + if filters.Valid { + var adminFilters AdminFilters + err = json.Unmarshal([]byte(filters.String), &adminFilters) + if err == nil { + admin.Filters = adminFilters + } + } + if additionalInfo.Valid { + admin.AdditionalInfo = additionalInfo.String + } + + return admin, err +} + +func getUserFromDbRow(row sqlScanner) (User, error) { var user User var permissions sql.NullString var password sql.NullString @@ -392,18 +560,11 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) { var filters sql.NullString var fsConfig sql.NullString var additionalInfo sql.NullString - var err error - if row != nil { - err = row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions, - &user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate, - &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, - &additionalInfo) - } else { - err = rows.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions, - &user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate, - &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, - &additionalInfo) - } + + err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions, + &user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate, + &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, + &additionalInfo) if err != nil { if err == sql.ErrNoRows { return user, &RecordNotFoundError{err: err.Error()} diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index 51de5fb6..2354cbe4 100644 --- a/dataprovider/sqlite.go +++ b/dataprovider/sqlite.go @@ -78,6 +78,10 @@ INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir DROP TABLE "{{users}}"; ALTER TABLE "new__users" RENAME TO "{{users}}"; ` + sqliteV7SQL = `CREATE TABLE "{{admins}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE, +"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL, "filters" text NULL, +"additional_info" text NULL);` + sqliteV7DownSQL = `DROP TABLE "{{admins}}";` ) // SQLiteProvider auth provider for SQLite database @@ -109,7 +113,7 @@ func initializeSQLiteProvider(basePath string) error { if err == nil { providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString) dbHandle.SetMaxOpenConns(1) - provider = SQLiteProvider{dbHandle: dbHandle} + provider = &SQLiteProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v", connectionString, err) @@ -117,98 +121,122 @@ func initializeSQLiteProvider(basePath string) error { return err } -func (p SQLiteProvider) checkAvailability() error { +func (p *SQLiteProvider) checkAvailability() error { return sqlCommonCheckAvailability(p.dbHandle) } -func (p SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { +func (p *SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) { return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle) } -func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { +func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) { return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) } -func (p SQLiteProvider) getUserByID(ID int64) (User, error) { - return sqlCommonGetUserByID(ID, p.dbHandle) -} - -func (p SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p SQLiteProvider) getUsedQuota(username string) (int, int64, error) { +func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, error) { return sqlCommonGetUsedQuota(username, p.dbHandle) } -func (p SQLiteProvider) updateLastLogin(username string) error { +func (p *SQLiteProvider) updateLastLogin(username string) error { return sqlCommonUpdateLastLogin(username, p.dbHandle) } -func (p SQLiteProvider) userExists(username string) (User, error) { - return sqlCommonCheckUserExists(username, p.dbHandle) +func (p *SQLiteProvider) userExists(username string) (User, error) { + return sqlCommonGetUserByUsername(username, p.dbHandle) } -func (p SQLiteProvider) addUser(user *User) error { +func (p *SQLiteProvider) addUser(user *User) error { return sqlCommonAddUser(user, p.dbHandle) } -func (p SQLiteProvider) updateUser(user *User) error { +func (p *SQLiteProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p SQLiteProvider) deleteUser(user *User) error { +func (p *SQLiteProvider) deleteUser(user *User) error { return sqlCommonDeleteUser(user, p.dbHandle) } -func (p SQLiteProvider) dumpUsers() ([]User, error) { +func (p *SQLiteProvider) dumpUsers() ([]User, error) { return sqlCommonDumpUsers(p.dbHandle) } -func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { - return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) +func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) { + return sqlCommonGetUsers(limit, offset, order, p.dbHandle) } -func (p SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { +func (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { return sqlCommonDumpFolders(p.dbHandle) } -func (p SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { +func (p *SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) { return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle) } -func (p SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { +func (p *SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) } -func (p SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error { +func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonAddFolder(folder, p.dbHandle) } -func (p SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { +func (p *SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { return sqlCommonDeleteFolder(folder, p.dbHandle) } -func (p SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { +func (p *SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error { return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle) } -func (p SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { +func (p *SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle) } -func (p SQLiteProvider) close() error { +func (p *SQLiteProvider) adminExists(username string) (Admin, error) { + return sqlCommonGetAdminByUsername(username, p.dbHandle) +} + +func (p *SQLiteProvider) addAdmin(admin *Admin) error { + return sqlCommonAddAdmin(admin, p.dbHandle) +} + +func (p *SQLiteProvider) updateAdmin(admin *Admin) error { + return sqlCommonUpdateAdmin(admin, p.dbHandle) +} + +func (p *SQLiteProvider) deleteAdmin(admin *Admin) error { + return sqlCommonDeleteAdmin(admin, p.dbHandle) +} + +func (p *SQLiteProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) { + return sqlCommonGetAdmins(limit, offset, order, p.dbHandle) +} + +func (p *SQLiteProvider) dumpAdmins() ([]Admin, error) { + return sqlCommonDumpAdmins(p.dbHandle) +} + +func (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Admin, error) { + return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle) +} + +func (p *SQLiteProvider) close() error { return p.dbHandle.Close() } -func (p SQLiteProvider) reloadConfig() error { +func (p *SQLiteProvider) reloadConfig() error { return nil } // initializeDatabase creates the initial database structure -func (p SQLiteProvider) initializeDatabase() error { +func (p *SQLiteProvider) initializeDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) if err == nil && dbVersion.Version > 0 { return ErrNoInitRequired @@ -236,7 +264,7 @@ func (p SQLiteProvider) initializeDatabase() error { return tx.Commit() } -func (p SQLiteProvider) migrateDatabase() error { +func (p *SQLiteProvider) migrateDatabase() error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -256,6 +284,8 @@ func (p SQLiteProvider) migrateDatabase() error { return updateSQLiteDatabaseFromV4(p.dbHandle) case 5: return updateSQLiteDatabaseFromV5(p.dbHandle) + case 6: + return updateSQLiteDatabaseFromV6(p.dbHandle) default: if dbVersion.Version > sqlDatabaseVersion { providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, @@ -268,7 +298,7 @@ func (p SQLiteProvider) migrateDatabase() error { } } -func (p SQLiteProvider) revertDatabase(targetVersion int) error { +func (p *SQLiteProvider) revertDatabase(targetVersion int) error { dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) if err != nil { return err @@ -277,6 +307,16 @@ func (p SQLiteProvider) revertDatabase(targetVersion int) error { return fmt.Errorf("current version match target version, nothing to do") } switch dbVersion.Version { + case 7: + err = downgradeSQLiteDatabaseFrom7To6(p.dbHandle) + if err != nil { + return err + } + err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle) + if err != nil { + return err + } + return downgradeSQLiteDatabaseFrom5To4(p.dbHandle) case 6: err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle) if err != nil { @@ -323,7 +363,15 @@ func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error { } func updateSQLiteDatabaseFromV5(dbHandle *sql.DB) error { - return updateSQLiteDatabaseFrom5To6(dbHandle) + err := updateSQLiteDatabaseFrom5To6(dbHandle) + if err != nil { + return err + } + return updateSQLiteDatabaseFromV6(dbHandle) +} + +func updateSQLiteDatabaseFromV6(dbHandle *sql.DB) error { + return updateSQLiteDatabaseFrom6To7(dbHandle) } func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error { @@ -355,6 +403,20 @@ func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error { return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) } +func updateSQLiteDatabaseFrom6To7(dbHandle *sql.DB) error { + logger.InfoToConsole("updating database version: 6 -> 7") + providerLog(logger.LevelInfo, "updating database version: 6 -> 7") + sql := strings.Replace(sqliteV7SQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7) +} + +func downgradeSQLiteDatabaseFrom7To6(dbHandle *sql.DB) error { + logger.InfoToConsole("downgrading database version: 7 -> 6") + providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6") + sql := strings.Replace(sqliteV7DownSQL, "{{admins}}", sqlTableAdmins, 1) + return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) +} + func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error { logger.InfoToConsole("downgrading database version: 6 -> 5") providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5") diff --git a/dataprovider/sqlqueries.go b/dataprovider/sqlqueries.go index 435e8b64..192f6c25 100644 --- a/dataprovider/sqlqueries.go +++ b/dataprovider/sqlqueries.go @@ -12,6 +12,7 @@ const ( selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," + "used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem,additional_info" selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update" + selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info" ) func getSQLPlaceholders() []string { @@ -26,19 +27,40 @@ func getSQLPlaceholders() []string { return placeholders } +func getAdminByUsernameQuery() string { + return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0]) +} + +func getAdminsQuery(order string) string { + return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectAdminFields, sqlTableAdmins, + order, sqlPlaceholders[0], sqlPlaceholders[1]) +} + +func getDumpAdminsQuery() string { + return fmt.Sprintf(`SELECT %v FROM %v`, selectAdminFields, sqlTableAdmins) +} + +func getAddAdminQuery() string { + return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info) + VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], + sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6]) +} + +func getUpdateAdminQuery() string { + return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v + WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], + sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6]) +} + +func getDeleteAdminQuery() string { + return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0]) +} + func getUserByUsernameQuery() string { return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0]) } -func getUserByIDQuery() string { - return fmt.Sprintf(`SELECT %v FROM %v WHERE id = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0]) -} - -func getUsersQuery(order string, username string) string { - if len(username) > 0 { - return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v ORDER BY username %v LIMIT %v OFFSET %v`, - selectUserFields, sqlTableUsers, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2]) - } +func getUsersQuery(order string) string { return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers, order, sqlPlaceholders[0], sqlPlaceholders[1]) } diff --git a/dataprovider/user.go b/dataprovider/user.go index 2710fecc..c3a4b6c5 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -20,7 +20,7 @@ import ( "github.com/drakkan/sftpgo/vfs" ) -// Available permissions for SFTP users +// Available permissions for SFTPGo users const ( // All permissions are granted PermAny = "*" @@ -802,26 +802,12 @@ func (u *User) GetExpirationDateAsString() string { // GetAllowedIPAsString returns the allowed IP as comma separated string func (u User) GetAllowedIPAsString() string { - result := "" - for _, IPMask := range u.Filters.AllowedIP { - if len(result) > 0 { - result += "," - } - result += IPMask - } - return result + return strings.Join(u.Filters.AllowedIP, ",") } // GetDeniedIPAsString returns the denied IP as comma separated string func (u User) GetDeniedIPAsString() string { - result := "" - for _, IPMask := range u.Filters.DeniedIP { - if len(result) > 0 { - result += "," - } - result += IPMask - } - return result + return strings.Join(u.Filters.DeniedIP, ",") } // SetEmptySecretsIfNil sets the secrets to empty if nil diff --git a/docs/account.md b/docs/account.md index 77abb23c..dfc1657e 100644 --- a/docs/account.md +++ b/docs/account.md @@ -15,5 +15,5 @@ SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha5 If you want to use your existing accounts, you have these options: -- you can import your users inside SFTPGo. Take a look at [sftpgo_api_cli](../examples/rest-api-cli#convert-users-from-other-stores "SFTPGo API CLI example"), it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users +- you can import your users inside SFTPGo. Take a look at [convert users](.../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users - you can use an external authentication program diff --git a/docs/defender.md b/docs/defender.md index 072a26be..eaae913f 100644 --- a/docs/defender.md +++ b/docs/defender.md @@ -38,7 +38,7 @@ The `defender` can also load a permanent block list and/or a safe list of ip add - `safelist_file`, defines the path to a file containing a list of ip addresses and/or networks to never ban. - `blocklist_file`, defines the path to a file containing a list of ip addresses and/or networks to always ban. -These list must be stored as JSON with the following schema: +These list must be stored as JSON conforming to the following schema: - `addresses`, list of strings. Each string must be a valid IPv4/IPv6 address. - `networks`, list of strings. Each string must be a valid IPv4/IPv6 CIDR address. diff --git a/docs/full-configuration.md b/docs/full-configuration.md index ccee0137..b700629f 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -160,7 +160,6 @@ The configuration file contains the following sections: - `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql` - `connectionstring`, string. Provide a custom database connection string. If not empty, this connection string will be used instead of building one using the previous parameters. Leave empty for drivers `bolt` and `memory` - `sql_tables_prefix`, string. Prefix for SQL tables - - `manage_users`, integer. Set to 0 to disable users management, 1 to enable - `track_quota`, integer. Set the preferred mode to track users quota between the following choices: - 0, disable quota tracking. REST API to scan users home directories/virtual folders and update quota will do nothing - 1, quota is updated each time a user uploads or deletes a file, even if the user has no quota restrictions @@ -193,7 +192,6 @@ The 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. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled - `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 passwords 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. - `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 server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. - **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4717b2ca..f4b20b03 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -4,32 +4,40 @@ SFTPGo exposes REST API to manage, backup, and restore users and folders, and to If quota tracking is enabled in the 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 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. +REST API are protected using JSON Web Tokens (JWT) authentication and can be exposed over HTTPS. -For example, you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way: +The default credentials are: -```shell -ProxyPass /api/v1 http://127.0.0.1:8080/api/v1 -ProxyPassReverse /api/v1 http://127.0.0.1:8080/api/v1 +- username: `admin` +- password: `password` + +You can get a JWT token using the `/api/v2/token` endpoint, you need to authenticate using HTTP Basic authentication and the credentials of an active administrator. Here is a sample response: + +```json +{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTA4NzU5NDksImp0aSI6ImMwMjAzbGZjZHJwZDRsMGMxanZnIiwibmJmIjoxNjEwODc1MzE5LCJwZXJtaXNzaW9ucyI6WyIqIl0sInN1YiI6ImlHZ010NlZNU3AzN2tld3hMR3lUV1l2b2p1a2ttSjBodXlJZHBzSWRyOFE9IiwidXNlcm5hbWUiOiJhZG1pbiJ9.dt-UwcWdEMwoGauuiQw8BmgpBAv4YlTaXkyNK-7iRJ4","expires_at":"2021-01-17T09:32:29Z"} ``` -and you can add authentication with something like this: +once the access token has expired, you need to get a new one. -```shell - - AuthType Digest - AuthName "Private" - AuthDigestDomain "/api/v1" - AuthDigestProvider file - AuthUserFile "/etc/httpd/conf/auth_digest" - Require valid-user - -``` +JWT tokens are not stored and we use a randomly generated secret to sign them so if you restart SFTPGo all the previous tokens will be invalidated and you will get a 401 HTTP response code. -and, of course, you can configure the web server to use HTTPS. +You can create other administrator and assign them the following permissions: + +- add users +- edit users +- del users +- view users +- view connections +- close connections +- view server status +- view and start quota scans +- view defender +- manage defender +- manage system +- manage admins + +You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP. The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs"). -A sample CLI client for the REST API can be found inside the source tree [rest-api-cli](../examples/rest-api-cli) directory. - -You can also generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/) +You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/). diff --git a/docs/service.md b/docs/service.md index 123fa8df..c450653d 100644 --- a/docs/service.md +++ b/docs/service.md @@ -61,8 +61,6 @@ sudo systemctl start sftpgo sudo systemctl status sftpgo # automatically start sftpgo on boot sudo systemctl enable sftpgo -# optional, install the REST API CLI. It requires python-requests to run -sudo install -Dm755 examples/rest-api-cli/sftpgo_api_cli /usr/bin/sftpgo_api_cli # optional, create shell completion script, for example for bash sudo sh -c '/usr/bin/sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo' # optional, create man pages @@ -102,8 +100,6 @@ sudo ln -s /usr/local/opt/sftpgo/init/com.github.drakkan.sftpgo.plist /Library/L sudo launchctl load -w /Library/LaunchDaemons/com.github.drakkan.sftpgo.plist # verify that the service is started sudo launchctl list com.github.drakkan.sftpgo -# optional, install the REST API CLI. It requires python-requests to run, this python module is not installed by default -sudo cp examples/rest-api-cli/sftpgo_api_cli /usr/local/opt/sftpgo/bin/ ``` ## Windows diff --git a/docs/web-admin.md b/docs/web-admin.md index 064f59d8..e04bfcec 100644 --- a/docs/web-admin.md +++ b/docs/web-admin.md @@ -1,8 +1,13 @@ # Web Admin -You can easily build your own interface using the exposed REST API. Anyway, SFTPGo also provides a very basic built-in web interface that allows you to manage users and connections. +You can easily build your own interface using the exposed [REST API](./rest-api.md). Anyway, SFTPGo also provides a basic built-in web interface that allows you to manage users, virtual folders, admins and connections. With the default `httpd` configuration, the web admin is available at the following URL: [http://127.0.0.1:8080/web](http://127.0.0.1:8080/web) -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](./rest-api.md). +The default credentials are: + +- username: `admin` +- password: `password` + +The web interface can be exposed over HTTPS. diff --git a/examples/convertusers/README.md b/examples/convertusers/README.md new file mode 100644 index 00000000..2cfb7dae --- /dev/null +++ b/examples/convertusers/README.md @@ -0,0 +1,49 @@ +# Import users from other stores + +`convertusers` is a very simple command line client, written in python, to import users from other stores. It requires `python3` or `python2`. + +Here is the usage: + +```console +usage: convertusers [-h] [--min-uid MIN_UID] [--max-uid MAX_UID] [--usernames USERNAMES [USERNAMES ...]] + [--force-uid FORCE_UID] [--force-gid FORCE_GID] + input_file {unix-passwd,pure-ftpd,proftpd} output_file + +Convert users to a JSON format suitable to use with loadddata + +positional arguments: + input_file + {unix-passwd,pure-ftpd,proftpd} + To import from unix-passwd format you need the permission to read /etc/shadow that is typically + granted to the root user only + output_file + +optional arguments: + -h, --help show this help message and exit + --min-uid MIN_UID if >= 0 only import users with UID greater or equal to this value. Default: -1 + --max-uid MAX_UID if >= 0 only import users with UID lesser or equal to this value. Default: -1 + --usernames USERNAMES [USERNAMES ...] + Only import users with these usernames. Default: [] + --force-uid FORCE_UID + if >= 0 the imported users will have this UID in SFTPGo. Default: -1 + --force-gid FORCE_GID + if >= 0 the imported users will have this GID in SFTPGo. Default: -1 +``` + +Let's see some examples: + +```console +python convertusers "" unix-passwd unix_users.json --min-uid 500 --force-uid 1000 --force-gid 1000 +``` + +```console +python convertusers pureftpd.passwd pure-ftpd pure_users.json --usernames "user1" "user2" +``` + +```console +python convertusers proftpd.passwd proftpd pro_users.json +``` + +The generated json file can be used as input for the `loaddata` REST API. + +Please note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`. diff --git a/examples/convertusers/convertusers b/examples/convertusers/convertusers new file mode 100755 index 00000000..694895a5 --- /dev/null +++ b/examples/convertusers/convertusers @@ -0,0 +1,208 @@ +#!/usr/bin/env python + +import argparse +import json +import sys +import time + +try: + import pwd + import spwd +except ImportError: + pwd = None + + +class ConvertUsers: + + def __init__(self, input_file, users_format, output_file, min_uid, max_uid, usernames, force_uid, force_gid): + self.input_file = input_file + self.users_format = users_format + self.output_file = output_file + self.min_uid = min_uid + self.max_uid = max_uid + self.usernames = usernames + self.force_uid = force_uid + self.force_gid = force_gid + self.SFTPGoUsers = [] + + def buildUserObject(self, username, password, home_dir, uid, gid, max_sessions, quota_size, quota_files, upload_bandwidth, + download_bandwidth, status, expiration_date, allowed_ip=[], denied_ip=[]): + return {'id':0, 'username':username, 'password':password, 'home_dir':home_dir, 'uid':uid, 'gid':gid, + 'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, 'permissions':{'/':"*"}, + 'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth, + 'status':status, 'expiration_date':expiration_date, + 'filters':{'allowed_ip':allowed_ip, 'denied_ip':denied_ip}} + + def addUser(self, user): + user['id'] = len(self.SFTPGoUsers) + 1 + print('') + print('New user imported: {}'.format(user)) + print('') + self.SFTPGoUsers.append(user) + + def saveUsers(self): + if self.SFTPGoUsers: + data = {'users':self.SFTPGoUsers} + jsonData = json.dumps(data) + with open(self.output_file, 'w') as f: + f.write(jsonData) + print() + print('Number of users saved to "{}": {}. You can import them using loaddata'.format(self.output_file, + len(self.SFTPGoUsers))) + print() + sys.exit(0) + else: + print('No user imported') + sys.exit(1) + + def convert(self): + if self.users_format == 'unix-passwd': + self.convertFromUnixPasswd() + elif self.users_format == 'pure-ftpd': + self.convertFromPureFTPD() + else: + self.convertFromProFTPD() + self.saveUsers() + + def isUserValid(self, username, uid): + if self.usernames and not username in self.usernames: + return False + if self.min_uid >= 0 and uid < self.min_uid: + return False + if self.max_uid >= 0 and uid > self.max_uid: + return False + return True + + def convertFromUnixPasswd(self): + days_from_epoch_time = time.time() / 86400 + for user in pwd.getpwall(): + username = user.pw_name + password = user.pw_passwd + uid = user.pw_uid + gid = user.pw_gid + home_dir = user.pw_dir + status = 1 + expiration_date = 0 + if not self.isUserValid(username, uid): + continue + if self.force_uid >= 0: + uid = self.force_uid + if self.force_gid >= 0: + gid = self.force_gid + # FIXME: if the passwords aren't in /etc/shadow they are probably DES encrypted and we don't support them + if password == 'x' or password == '*': + user_info = spwd.getspnam(username) + password = user_info.sp_pwdp + if not password or password == '!!' or password == '!*': + print('cannot import user "{}" without a password'.format(username)) + continue + if user_info.sp_inact > 0: + last_pwd_change_diff = days_from_epoch_time - user_info.sp_lstchg + if last_pwd_change_diff > user_info.sp_inact: + status = 0 + if user_info.sp_expire > 0: + expiration_date = user_info.sp_expire * 86400 + self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, status, + expiration_date)) + + def convertFromProFTPD(self): + with open(self.input_file, 'r') as f: + for line in f: + fields = line.split(':') + if len(fields) > 6: + username = fields[0] + password = fields[1] + uid = int(fields[2]) + gid = int(fields[3]) + home_dir = fields[5] + if not self.isUserValid(username, uid): + continue + if self.force_uid >= 0: + uid = self.force_uid + if self.force_gid >= 0: + gid = self.force_gid + self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, 1, 0)) + + def convertPureFTPDIP(self, fields): + result = [] + if not fields: + return result + for v in fields.split(','): + ip_mask = v.strip() + if not ip_mask: + continue + if ip_mask.count('.') < 3 and ip_mask.count(':') < 3: + print('cannot import pure-ftpd IP: {}'.format(ip_mask)) + continue + if '/' not in ip_mask: + ip_mask += '/32' + result.append(ip_mask) + return result + + def convertFromPureFTPD(self): + with open(self.input_file, 'r') as f: + for line in f: + fields = line.split(':') + if len(fields) > 16: + username = fields[0] + password = fields[1] + uid = int(fields[2]) + gid = int(fields[3]) + home_dir = fields[5] + upload_bandwidth = 0 + if fields[6]: + upload_bandwidth = int(int(fields[6]) / 1024) + download_bandwidth = 0 + if fields[7]: + download_bandwidth = int(int(fields[7]) / 1024) + max_sessions = 0 + if fields[10]: + max_sessions = int(fields[10]) + quota_files = 0 + if fields[11]: + quota_files = int(fields[11]) + quota_size = 0 + if fields[12]: + quota_size = int(fields[12]) + allowed_ip = self.convertPureFTPDIP(fields[15]) + denied_ip = self.convertPureFTPDIP(fields[16]) + if not self.isUserValid(username, uid): + continue + if self.force_uid >= 0: + uid = self.force_uid + if self.force_gid >= 0: + gid = self.force_gid + self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, max_sessions, quota_size, + quota_files, upload_bandwidth, download_bandwidth, 1, 0, allowed_ip, + denied_ip)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description= + 'Convert users to a JSON format suitable to use with loadddata') + supportedUsersFormats = [] + help_text = '' + if pwd is not None: + supportedUsersFormats.append('unix-passwd') + help_text = 'To import from unix-passwd format you need the permission to read /etc/shadow that is typically granted to the root user only' + supportedUsersFormats.append('pure-ftpd') + supportedUsersFormats.append('proftpd') + parser.add_argument('input_file', type=str) + parser.add_argument('users_format', type=str, choices=supportedUsersFormats, help=help_text) + parser.add_argument('output_file', type=str) + parser.add_argument('--min-uid', type=int, default=-1, help='if >= 0 only import users with UID greater or equal ' + + 'to this value. Default: %(default)s') + parser.add_argument('--max-uid', type=int, default=-1, help='if >= 0 only import users with UID lesser or equal ' + + 'to this value. Default: %(default)s') + parser.add_argument('--usernames', type=str, nargs='+', default=[], help='Only import users with these usernames. ' + + 'Default: %(default)s') + parser.add_argument('--force-uid', type=int, default=-1, help='if >= 0 the imported users will have this UID in ' + + 'SFTPGo. Default: %(default)s') + parser.add_argument('--force-gid', type=int, default=-1, help='if >= 0 the imported users will have this GID in ' + + 'SFTPGo. Default: %(default)s') + + args = parser.parse_args() + + convertUsers = ConvertUsers(args.input_file, args.users_format, args.output_file, args.min_uid, args.max_uid, + args.usernames, args.force_uid, args.force_gid) + convertUsers.convert() diff --git a/examples/rest-api-cli/README.md b/examples/rest-api-cli/README.md index a6bc2bd5..46edd959 100644 --- a/examples/rest-api-cli/README.md +++ b/examples/rest-api-cli/README.md @@ -1,5 +1,7 @@ # REST API CLI client +:warning: This sample client is deprecated and it will work only with api V1 (SFTPGo <= 1.2.2). You can easily build your own client from the OpenAPI schema. + `sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python. It has the following requirements: diff --git a/ftpd/cryptfs_test.go b/ftpd/cryptfs_test.go index 6be0e3a1..2df968de 100644 --- a/ftpd/cryptfs_test.go +++ b/ftpd/cryptfs_test.go @@ -14,14 +14,14 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" - "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" ) func TestBasicFTPHandlingCryptFs(t *testing.T) { u := getTestUserWithCryptFs() u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -56,7 +56,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) { assert.Len(t, list, 1) assert.Equal(t, testFileSize, int64(list[0].Size)) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -66,7 +66,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) { assert.Error(t, err) err = client.Delete(testFileName + "1") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize) @@ -108,7 +108,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -117,7 +117,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) { func TestZeroBytesTransfersCryptFs(t *testing.T) { u := getTestUserWithCryptFs() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -146,7 +146,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -154,7 +154,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) { func TestResumeCryptFs(t *testing.T) { u := getTestUserWithCryptFs() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -207,7 +207,7 @@ func TestResumeCryptFs(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) diff --git a/ftpd/ftpd_test.go b/ftpd/ftpd_test.go index 2472ff36..41cd9deb 100644 --- a/ftpd/ftpd_test.go +++ b/ftpd/ftpd_test.go @@ -28,7 +28,7 @@ import ( "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/ftpd" - "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" @@ -132,7 +132,7 @@ func TestMain(m *testing.M) { logger.WarnToConsole("error initializing common: %v", err) os.Exit(1) } - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) if err != nil { logger.ErrorToConsole("error initializing data provider: %v", err) os.Exit(1) @@ -150,7 +150,7 @@ func TestMain(m *testing.M) { httpdConf := config.GetHTTPDConfig() httpdConf.BindPort = 8079 - httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "") + httpdtest.SetBaseURL("http://127.0.0.1:8079") ftpdConf := config.GetFTPDConfig() ftpdConf.Bindings = []ftpd.Binding{ @@ -298,11 +298,11 @@ func TestInitializationFailure(t *testing.T) { func TestBasicFTPHandling(t *testing.T) { u := getTestUser() u.QuotaSize = 6553600 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.QuotaSize = 6553600 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { @@ -332,7 +332,7 @@ func TestBasicFTPHandling(t *testing.T) { localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -342,7 +342,7 @@ func TestBasicFTPHandling(t *testing.T) { assert.Error(t, err) err = client.Delete(testFileName + "1") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize) @@ -385,9 +385,9 @@ func TestBasicFTPHandling(t *testing.T) { assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -396,12 +396,12 @@ func TestBasicFTPHandling(t *testing.T) { func TestLoginInvalidPwd(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.Password = "wrong" _, err = getFTPClient(user, false) assert.Error(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -425,7 +425,7 @@ func TestLoginExternalAuth(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) client, err := getFTPClient(u, true) if assert.NoError(t, err) { @@ -441,22 +441,20 @@ func TestLoginExternalAuth(t *testing.T) { assert.NoError(t, err) } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - if assert.Len(t, users, 1) { - user := users[0] - assert.Equal(t, defaultUsername, user.Username) - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - err = os.RemoveAll(user.GetHomeDir()) - assert.NoError(t, err) - } + assert.Equal(t, defaultUsername, user.Username) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) + err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -475,11 +473,10 @@ func TestPreLoginHook(t *testing.T) { err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm) assert.NoError(t, err) providerConf.PreLoginHook = preLoginPath - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + _, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound) assert.NoError(t, err) - assert.Equal(t, 0, len(users)) client, err := getFTPClient(u, false) if assert.NoError(t, err) { err = checkBasicFTP(client) @@ -488,10 +485,8 @@ func TestPreLoginHook(t *testing.T) { assert.NoError(t, err) } - users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user := users[0] // test login with an existing user client, err = getFTPClient(user, true) @@ -518,7 +513,7 @@ func TestPreLoginHook(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -527,7 +522,7 @@ func TestPreLoginHook(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(preLoginPath) assert.NoError(t, err) @@ -540,7 +535,7 @@ func TestPostConnectHook(t *testing.T) { common.Config.PostConnectHook = postConnectPath u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm) assert.NoError(t, err) @@ -559,7 +554,7 @@ func TestPostConnectHook(t *testing.T) { assert.NoError(t, err) } - common.Config.PostConnectHook = "http://127.0.0.1:8079/api/v1/version" + common.Config.PostConnectHook = "http://127.0.0.1:8079/healthz" client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -577,7 +572,7 @@ func TestPostConnectHook(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -589,7 +584,7 @@ func TestMaxConnections(t *testing.T) { oldValue := common.Config.MaxTotalConnections common.Config.MaxTotalConnections = 1 - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -600,7 +595,7 @@ func TestMaxConnections(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -618,7 +613,7 @@ func TestDefender(t *testing.T) { err := common.Initialize(cfg) assert.NoError(t, err) - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, false) if assert.NoError(t, err) { @@ -640,7 +635,7 @@ func TestDefender(t *testing.T) { assert.Contains(t, err.Error(), "Access denied, banned client IP") } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -652,7 +647,7 @@ func TestDefender(t *testing.T) { func TestMaxSessions(t *testing.T) { u := getTestUser() u.MaxSessions = 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -663,7 +658,7 @@ func TestMaxSessions(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -671,7 +666,7 @@ func TestMaxSessions(t *testing.T) { func TestZeroBytesTransfers(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, useTLS := range []bool{true, false} { client, err := getFTPClient(user, useTLS) @@ -699,7 +694,7 @@ func TestZeroBytesTransfers(t *testing.T) { assert.NoError(t, err) } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -727,7 +722,7 @@ func TestDownloadErrors(t *testing.T) { DeniedPatterns: []string{"*.jpg"}, }, } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -758,7 +753,7 @@ func TestDownloadErrors(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -779,7 +774,7 @@ func TestUploadErrors(t *testing.T) { DeniedExtensions: []string{".zip"}, }, } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -823,7 +818,7 @@ func TestUploadErrors(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -831,9 +826,9 @@ func TestUploadErrors(t *testing.T) { func TestResume(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getFTPClient(user, true) @@ -888,9 +883,9 @@ func TestResume(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -900,12 +895,12 @@ func TestResume(t *testing.T) { func TestDeniedLoginMethod(t *testing.T) { u := getTestUser() u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) _, err = getFTPClient(user, false) assert.Error(t, err) user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndPassword} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -913,7 +908,7 @@ func TestDeniedLoginMethod(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -923,12 +918,12 @@ func TestDeniedLoginMethod(t *testing.T) { func TestDeniedProtocols(t *testing.T) { u := getTestUser() u.Filters.DeniedProtocols = []string{common.ProtocolFTP} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) _, err = getFTPClient(user, false) assert.Error(t, err) user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolWebDAV} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -936,7 +931,7 @@ func TestDeniedProtocols(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -945,11 +940,11 @@ func TestDeniedProtocols(t *testing.T) { func TestQuotaLimits(t *testing.T) { u := getTestUser() u.QuotaFiles = 1 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.QuotaFiles = 1 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testFileSize := int64(65535) @@ -981,7 +976,7 @@ func TestQuotaLimits(t *testing.T) { // test quota size user.QuotaSize = testFileSize - 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, true) if assert.NoError(t, err) { @@ -995,7 +990,7 @@ func TestQuotaLimits(t *testing.T) { // now test quota limits while uploading the current file, we have 1 bytes remaining user.QuotaSize = testFileSize + 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1031,13 +1026,13 @@ func TestQuotaLimits(t *testing.T) { assert.NoError(t, err) user.QuotaFiles = 0 user.QuotaSize = 0 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1047,11 +1042,11 @@ func TestUploadMaxSize(t *testing.T) { testFileSize := int64(65535) u := getTestUser() u.Filters.MaxUploadFileSize = testFileSize + 1 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.Filters.MaxUploadFileSize = testFileSize + 1 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testFilePath := filepath.Join(homeBasePath, testFileName) @@ -1084,13 +1079,13 @@ func TestUploadMaxSize(t *testing.T) { err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) user.Filters.MaxUploadFileSize = 65536000 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1100,7 +1095,7 @@ func TestLoginWithIPilters(t *testing.T) { u := getTestUser() u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"} u.Filters.AllowedIP = []string{"172.19.0.0/16"} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if !assert.Error(t, err) { @@ -1108,7 +1103,7 @@ func TestLoginWithIPilters(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1129,7 +1124,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) - err := dataprovider.Initialize(providerConf, configDir) + err := dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) if _, err = os.Stat(credentialsFile); err == nil { @@ -1137,7 +1132,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, os.Remove(credentialsFile)) } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) @@ -1152,7 +1147,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1160,7 +1155,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) assert.NoError(t, config.LoadConfig(configDir, "")) providerConf = config.GetProviderConf() - assert.NoError(t, dataprovider.Initialize(providerConf, configDir)) + assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true)) } func TestLoginInvalidFs(t *testing.T) { @@ -1168,7 +1163,7 @@ func TestLoginInvalidFs(t *testing.T) { u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) providerConf := config.GetProviderConf() @@ -1186,7 +1181,7 @@ func TestLoginInvalidFs(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1194,7 +1189,7 @@ func TestLoginInvalidFs(t *testing.T) { func TestClientClose(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -1207,7 +1202,7 @@ func TestClientClose(t *testing.T) { 1*time.Second, 50*time.Millisecond) } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1215,9 +1210,9 @@ func TestClientClose(t *testing.T) { func TestRename(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testDir := "adir" @@ -1262,7 +1257,7 @@ func TestRename(t *testing.T) { assert.NoError(t, err) } user.Permissions[path.Join("/", testDir)] = []string{dataprovider.PermListItems} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1277,15 +1272,15 @@ func TestRename(t *testing.T) { if user.Username == defaultUsername { user.Permissions = make(map[string][]string) user.Permissions["/"] = allPerms - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1293,9 +1288,9 @@ func TestRename(t *testing.T) { func TestSymlink(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) @@ -1342,9 +1337,9 @@ func TestSymlink(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1353,9 +1348,9 @@ func TestSymlink(t *testing.T) { func TestStat(t *testing.T) { u := getTestUser() u.Permissions["/subdir"] = []string{dataprovider.PermUpload} - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { @@ -1391,9 +1386,9 @@ func TestStat(t *testing.T) { } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1413,7 +1408,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { }) err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, false) if assert.NoError(t, err) { @@ -1423,7 +1418,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { assert.NoError(t, err) err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0) assert.NoError(t, err) - folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -1432,7 +1427,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { } err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0) assert.NoError(t, err) - folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -1444,9 +1439,9 @@ func TestUploadOverwriteVfolder(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1466,7 +1461,7 @@ func TestAllocateAvailable(t *testing.T) { }) err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, false) if assert.NoError(t, err) { @@ -1488,7 +1483,7 @@ func TestAllocateAvailable(t *testing.T) { assert.NoError(t, err) } user.QuotaSize = 100 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1537,7 +1532,7 @@ func TestAllocateAvailable(t *testing.T) { user.Filters.MaxUploadFileSize = 100 user.QuotaSize = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1560,7 +1555,7 @@ func TestAllocateAvailable(t *testing.T) { } user.QuotaSize = 50 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1572,7 +1567,7 @@ func TestAllocateAvailable(t *testing.T) { user.QuotaSize = 1000 user.Filters.MaxUploadFileSize = 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getFTPClient(user, false) if assert.NoError(t, err) { @@ -1582,9 +1577,9 @@ func TestAllocateAvailable(t *testing.T) { assert.Equal(t, "1", response) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1594,9 +1589,9 @@ func TestAllocateAvailable(t *testing.T) { func TestAvailableUnsupportedFs(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(sftpUser, false) if assert.NoError(t, err) { @@ -1608,9 +1603,9 @@ func TestAvailableUnsupportedFs(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1618,9 +1613,9 @@ func TestAvailableUnsupportedFs(t *testing.T) { func TestChtimes(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { @@ -1651,9 +1646,9 @@ func TestChtimes(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1663,7 +1658,7 @@ func TestChown(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("chown is not supported on Windows") } - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true) if assert.NoError(t, err) { @@ -1686,7 +1681,7 @@ func TestChown(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1697,9 +1692,9 @@ func TestChmod(t *testing.T) { t.Skip("chmod is partially supported on Windows") } u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getFTPClient(user, true) @@ -1733,9 +1728,9 @@ func TestChmod(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1743,9 +1738,9 @@ func TestChmod(t *testing.T) { func TestCombineDisabled(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getFTPClient(user, true) @@ -1763,9 +1758,9 @@ func TestCombineDisabled(t *testing.T) { } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1773,7 +1768,7 @@ func TestCombineDisabled(t *testing.T) { func TestActiveModeDisabled(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClientImplicitTLS(user) if assert.NoError(t, err) { @@ -1809,7 +1804,7 @@ func TestActiveModeDisabled(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1817,7 +1812,7 @@ func TestActiveModeDisabled(t *testing.T) { func TestSITEDisabled(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClientImplicitTLS(user) if assert.NoError(t, err) { @@ -1832,7 +1827,7 @@ func TestSITEDisabled(t *testing.T) { err = client.Quit() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1840,13 +1835,13 @@ func TestSITEDisabled(t *testing.T) { func TestHASH(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) u = getTestUserWithCryptFs() u.Username += "_crypt" - cryptUser, _, err := httpd.AddUser(u, http.StatusOK) + cryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} { client, err := getFTPClientImplicitTLS(user) @@ -1891,13 +1886,13 @@ func TestHASH(t *testing.T) { } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(cryptUser, http.StatusOK) + _, err = httpdtest.RemoveUser(cryptUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(cryptUser.GetHomeDir()) assert.NoError(t, err) @@ -1905,9 +1900,9 @@ func TestHASH(t *testing.T) { func TestCombine(t *testing.T) { u := getTestUser() - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getFTPClientImplicitTLS(user) @@ -1945,9 +1940,9 @@ func TestCombine(t *testing.T) { } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) diff --git a/go.mod b/go.mod index 3cd9b7c8..aab01d84 100644 --- a/go.mod +++ b/go.mod @@ -3,34 +3,39 @@ module github.com/drakkan/sftpgo go 1.15 require ( - cloud.google.com/go v0.74.0 // indirect + cloud.google.com/go v0.75.0 // indirect cloud.google.com/go/storage v1.12.0 github.com/Azure/azure-storage-blob-go v0.12.0 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b - github.com/aws/aws-sdk-go v1.36.20 + github.com/aws/aws-sdk-go v1.36.28 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d github.com/fclairamb/ftpserverlib v0.12.0 github.com/frankban/quicktest v1.11.2 // indirect github.com/go-chi/chi v1.5.1 + github.com/go-chi/jwtauth v1.1.1 github.com/go-chi/render v1.0.1 + github.com/go-ole/go-ole v1.2.5 // indirect github.com/go-sql-driver/mysql v1.5.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.1.4 // indirect + github.com/google/uuid v1.1.5 // indirect github.com/grandcat/zeroconf v1.0.0 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 + github.com/lestrrat-go/jwx v1.0.8 github.com/lib/pq v1.9.0 github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-sqlite3 v1.14.6 github.com/miekg/dns v1.1.35 // indirect github.com/minio/sha256-simd v0.1.1 github.com/minio/sio v0.2.1 + github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/otiai10/copy v1.4.2 github.com/pelletier/go-toml v1.8.1 // indirect github.com/pires/go-proxyproto v0.3.3 github.com/pkg/sftp v1.12.1-0.20201128220914-b5b6f3393fe9 github.com/prometheus/client_golang v1.9.0 + github.com/prometheus/procfs v0.3.0 // indirect github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b github.com/rs/xid v1.2.1 github.com/rs/zerolog v1.20.0 @@ -49,12 +54,16 @@ require ( gocloud.dev v0.21.0 gocloud.dev/secrets/hashivault v0.21.0 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20201224014010-6772e930b67b - golang.org/x/sys v0.0.0-20210104204734-6f8348627aad + golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 // indirect + golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 + golang.org/x/text v0.3.5 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect - golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee // indirect + golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect google.golang.org/api v0.36.0 - google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d // indirect + google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f // indirect + google.golang.org/grpc v1.35.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 63d807b5..9fce44b6 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -106,8 +106,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.36.20 h1:IQr81xegCd40Xq21ZjFToKw9llaCzO1LRE75CgnvJ1Q= -github.com/aws/aws-sdk-go v1.36.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.36.28 h1:JVRN7BZgwQ31SQCBwG5QM445+ynJU0ruKu+miFIijYY= +github.com/aws/aws-sdk-go v1.36.28/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -131,6 +131,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -176,6 +177,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -196,6 +198,8 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= +github.com/go-chi/jwtauth v1.1.1 h1:CtUHwzvXUfZeZSbASLgzaTZQ8mL7p+vitX59NBTL1vY= +github.com/go-chi/jwtauth v1.1.1/go.mod h1:znOWz9e5/GfBOKiZlOUoEfjSjUF+cLZO3GcpkoGXvFI= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -210,6 +214,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -290,7 +295,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -298,8 +303,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= -github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= @@ -410,6 +415,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw= +github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk= +github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k= +github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0= +github.com/lestrrat-go/jwx v1.0.8 h1:Mj/2Ey9rkGx4w5IMQ2Q+9KLZn4cZoMgKrnMxi9eXE3k= +github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8= +github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -450,8 +468,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -540,8 +559,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -682,6 +702,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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= @@ -690,8 +711,9 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 h1:BaN3BAqnopnKjvl+15DYP6LLrbBHfbfmlFYzmFj/Q9Q= +golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -722,6 +744,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -754,8 +777,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad h1:MCsdmFSdEd4UEa5TKS5JztCRHK/WtvNei1edOj5RSRo= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -764,8 +787,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -817,6 +841,7 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -832,8 +857,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -911,9 +936,9 @@ google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d h1:HV9Z9qMhQEsdlvxNFELgQ11RkMzO3CMkjEySjCtuLes= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -936,8 +961,9 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/httpd/api_admin.go b/httpd/api_admin.go new file mode 100644 index 00000000..781d673a --- /dev/null +++ b/httpd/api_admin.go @@ -0,0 +1,211 @@ +package httpd + +import ( + "context" + "errors" + "net/http" + "strconv" + + "github.com/go-chi/jwtauth" + "github.com/go-chi/render" + + "github.com/drakkan/sftpgo/dataprovider" +) + +type adminPwd struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` +} + +func getAdmins(w http.ResponseWriter, r *http.Request) { + limit := 100 + offset := 0 + order := dataprovider.OrderASC + var err error + if _, ok := r.URL.Query()["limit"]; ok { + limit, err = strconv.Atoi(r.URL.Query().Get("limit")) + if err != nil { + err = errors.New("Invalid limit") + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + if limit > 500 { + limit = 500 + } + } + if _, ok := r.URL.Query()["offset"]; ok { + offset, err = strconv.Atoi(r.URL.Query().Get("offset")) + if err != nil { + err = errors.New("Invalid offset") + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + } + if _, ok := r.URL.Query()["order"]; ok { + order = r.URL.Query().Get("order") + if order != dataprovider.OrderASC && order != dataprovider.OrderDESC { + err = errors.New("Invalid order") + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + } + + admins, err := dataprovider.GetAdmins(limit, offset, order) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + render.JSON(w, r, admins) +} + +func getAdminByUsername(w http.ResponseWriter, r *http.Request) { + username := getURLParam(r, "username") + renderAdmin(w, r, username, http.StatusOK) +} + +func renderAdmin(w http.ResponseWriter, r *http.Request, username string, status int) { + admin, err := dataprovider.AdminExists(username) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + admin.HideConfidentialData() + if status != http.StatusOK { + ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated) + render.JSON(w, r.WithContext(ctx), admin) + } else { + render.JSON(w, r, admin) + } +} + +func addAdmin(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + var admin dataprovider.Admin + err := render.DecodeJSON(r.Body, &admin) + if err != nil { + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + err = dataprovider.AddAdmin(&admin) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + renderAdmin(w, r, admin.Username, http.StatusCreated) +} + +func updateAdmin(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + username := getURLParam(r, "username") + admin, err := dataprovider.AdminExists(username) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + + adminID := admin.ID + err = render.DecodeJSON(r.Body, &admin) + if err != nil { + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + + claims, err := getTokenClaims(r) + if err != nil || claims.Username == "" { + sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest) + return + } + if username == claims.Username { + if claims.isCriticalPermRemoved(admin.Permissions) { + sendAPIResponse(w, r, errors.New("You cannot remove these permissions to yourself"), "", http.StatusBadRequest) + return + } + if admin.Status == 0 { + sendAPIResponse(w, r, errors.New("You cannot disable yourself"), "", http.StatusBadRequest) + return + } + } + admin.ID = adminID + admin.Username = username + if err := dataprovider.UpdateAdmin(&admin); err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + sendAPIResponse(w, r, nil, "Update admin", http.StatusOK) +} + +func deleteAdmin(w http.ResponseWriter, r *http.Request) { + username := getURLParam(r, "username") + claims, err := getTokenClaims(r) + if err != nil || claims.Username == "" { + sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest) + return + } + if username == claims.Username { + sendAPIResponse(w, r, errors.New("You cannot delete yourself"), "", http.StatusBadRequest) + return + } + + err = dataprovider.DeleteAdmin(username) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + sendAPIResponse(w, r, err, "Admin deleted", http.StatusOK) +} + +func changeAdminPassword(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + + var pwd adminPwd + err := render.DecodeJSON(r.Body, &pwd) + if err != nil { + sendAPIResponse(w, r, err, "", http.StatusBadRequest) + return + } + err = doChangeAdminPassword(r, pwd.CurrentPassword, pwd.NewPassword, pwd.NewPassword) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + sendAPIResponse(w, r, err, "Password updated", http.StatusOK) +} + +func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { + if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { + return dataprovider.NewValidationError("Please provide the current password and the new one two times") + } + if newPassword != confirmNewPassword { + return dataprovider.NewValidationError("The two password fields do not match") + } + if currentPassword == newPassword { + return dataprovider.NewValidationError("The new password must be different from the current one") + } + claims, err := getTokenClaims(r) + if err != nil { + return err + } + admin, err := dataprovider.AdminExists(claims.Username) + if err != nil { + return err + } + match, err := admin.CheckPassword(currentPassword) + if !match || err != nil { + return dataprovider.NewValidationError("Current password does not match") + } + + admin.Password = newPassword + + return dataprovider.UpdateAdmin(&admin) +} + +func getTokenClaims(r *http.Request) (jwtTokenClaims, error) { + tokenClaims := jwtTokenClaims{} + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + return tokenClaims, err + } + tokenClaims.Decode(claims) + + return tokenClaims, nil +} diff --git a/httpd/api_folder.go b/httpd/api_folder.go index 9798ce4d..6e6f08b6 100644 --- a/httpd/api_folder.go +++ b/httpd/api_folder.go @@ -1,6 +1,7 @@ package httpd import ( + "context" "errors" "net/http" "strconv" @@ -64,16 +65,21 @@ func addFolder(w http.ResponseWriter, r *http.Request) { return } err = dataprovider.AddFolder(&folder) - if err == nil { - folder, err = dataprovider.GetFolderByPath(folder.MappedPath) - if err == nil { - render.JSON(w, r, folder) - } else { - sendAPIResponse(w, r, err, "", getRespStatus(err)) - } - } else { + if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) + return } + renderFolder(w, r, folder.MappedPath) +} + +func renderFolder(w http.ResponseWriter, r *http.Request, mappedPath string) { + folder, err := dataprovider.GetFolderByPath(mappedPath) + if err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated) + render.JSON(w, r.WithContext(ctx), folder) } func deleteFolderByPath(w http.ResponseWriter, r *http.Request) { @@ -87,15 +93,10 @@ func deleteFolderByPath(w http.ResponseWriter, r *http.Request) { return } - folder, err := dataprovider.GetFolderByPath(folderPath) + err := dataprovider.DeleteFolder(folderPath) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } - err = dataprovider.DeleteFolder(&folder) - if err != nil { - sendAPIResponse(w, r, err, "", http.StatusInternalServerError) - } else { - sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK) - } + sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK) } diff --git a/httpd/api_maintenance.go b/httpd/api_maintenance.go index 43a53a9f..650d75f6 100644 --- a/httpd/api_maintenance.go +++ b/httpd/api_maintenance.go @@ -112,7 +112,13 @@ func loadData(w http.ResponseWriter, r *http.Request) { return } - logger.Debug(logSender, "", "backup restored, users: %v", len(dump.Users)) + if err = RestoreAdmins(dump.Admins, inputFile, mode); err != nil { + sendAPIResponse(w, r, err, "", getRespStatus(err)) + return + } + + logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs", + len(dump.Users), len(dump.Folders), len(dump.Admins)) sendAPIResponse(w, r, err, "Data restored", http.StatusOK) } @@ -164,6 +170,33 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, scanQuota return nil } +// RestoreAdmins restores the specified admins +func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error { + for _, admin := range admins { + admin := admin // pin + a, err := dataprovider.AdminExists(admin.Username) + if err == nil { + if mode == 1 { + logger.Debug(logSender, "", "loaddata mode 1, existing admin %#v not updated", a.Username) + continue + } + admin.ID = a.ID + err = dataprovider.UpdateAdmin(&admin) + admin.Password = redactedSecret + logger.Debug(logSender, "", "restoring existing admin: %+v, dump file: %#v, error: %v", admin, inputFile, err) + } else { + err = dataprovider.AddAdmin(&admin) + admin.Password = redactedSecret + logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err) + } + if err != nil { + return err + } + } + + return nil +} + // RestoreUsers restores the specified users func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error { for _, user := range users { @@ -176,14 +209,14 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i } user.ID = u.ID err = dataprovider.UpdateUser(&user) - user.Password = "[redacted]" + user.Password = redactedSecret logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err) if mode == 2 && err == nil { disconnectUser(user.Username) } } else { err = dataprovider.AddUser(&user) - user.Password = "[redacted]" + user.Password = redactedSecret logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err) } if err != nil { diff --git a/httpd/api_user.go b/httpd/api_user.go index 74bd91ec..77460842 100644 --- a/httpd/api_user.go +++ b/httpd/api_user.go @@ -1,12 +1,12 @@ package httpd import ( + "context" "errors" "fmt" "net/http" "strconv" - "github.com/go-chi/chi" "github.com/go-chi/render" "github.com/drakkan/sftpgo/common" @@ -16,11 +16,11 @@ import ( ) func getUsers(w http.ResponseWriter, r *http.Request) { + var err error + limit := 100 offset := 0 order := dataprovider.OrderASC - username := "" - var err error if _, ok := r.URL.Query()["limit"]; ok { limit, err = strconv.Atoi(r.URL.Query().Get("limit")) if err != nil { @@ -48,10 +48,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) { return } } - if _, ok := r.URL.Query()["username"]; ok { - username = r.URL.Query().Get("username") - } - users, err := dataprovider.GetUsers(limit, offset, order, username) + users, err := dataprovider.GetUsers(limit, offset, order) if err == nil { render.JSON(w, r, users) } else { @@ -59,19 +56,23 @@ func getUsers(w http.ResponseWriter, r *http.Request) { } } -func getUserByID(w http.ResponseWriter, r *http.Request) { - userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) +func getUserByUsername(w http.ResponseWriter, r *http.Request) { + username := getURLParam(r, "username") + renderUser(w, r, username, http.StatusOK) +} + +func renderUser(w http.ResponseWriter, r *http.Request, username string, status int) { + user, err := dataprovider.UserExists(username) if err != nil { - err = errors.New("Invalid userID") - sendAPIResponse(w, r, err, "", http.StatusBadRequest) + sendAPIResponse(w, r, err, "", getRespStatus(err)) return } - user, err := dataprovider.GetUserByID(userID) - if err == nil { - user.HideConfidentialData() - render.JSON(w, r, user) + user.HideConfidentialData() + if status != http.StatusOK { + ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated) + render.JSON(w, r.WithContext(ctx), user) } else { - sendAPIResponse(w, r, err, "", getRespStatus(err)) + render.JSON(w, r, user) } } @@ -116,27 +117,18 @@ func addUser(w http.ResponseWriter, r *http.Request) { } } err = dataprovider.AddUser(&user) - if err == nil { - user, err = dataprovider.UserExists(user.Username) - if err == nil { - user.HideConfidentialData() - render.JSON(w, r, user) - } else { - sendAPIResponse(w, r, err, "", getRespStatus(err)) - } - } else { + if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) + return } + renderUser(w, r, user.Username, http.StatusCreated) } func updateUser(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) - if err != nil { - err = errors.New("Invalid userID") - sendAPIResponse(w, r, err, "", http.StatusBadRequest) - return - } + var err error + + username := getURLParam(r, "username") disconnect := 0 if _, ok := r.URL.Query()["disconnect"]; ok { disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect")) @@ -146,11 +138,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } } - user, err := dataprovider.GetUserByID(userID) + user, err := dataprovider.UserExists(username) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } + userID := user.ID currentPermissions := user.Permissions currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey @@ -170,6 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, err, "", http.StatusBadRequest) return } + user.ID = userID + user.Username = username user.SetEmptySecretsIfNil() // we use new Permissions if passed otherwise the old ones if len(user.Permissions) == 0 { @@ -177,40 +172,26 @@ func updateUser(w http.ResponseWriter, r *http.Request) { } updateEncryptedSecrets(&user, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey) - if user.ID != userID { - sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest) - return - } err = dataprovider.UpdateUser(&user) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) - } else { - sendAPIResponse(w, r, err, "User updated", http.StatusOK) - if disconnect == 1 { - disconnectUser(user.Username) - } + return + } + sendAPIResponse(w, r, err, "User updated", http.StatusOK) + if disconnect == 1 { + disconnectUser(user.Username) } } func deleteUser(w http.ResponseWriter, r *http.Request) { - userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) - if err != nil { - err = errors.New("Invalid userID") - sendAPIResponse(w, r, err, "", http.StatusBadRequest) - return - } - user, err := dataprovider.GetUserByID(userID) + username := getURLParam(r, "username") + err := dataprovider.DeleteUser(username) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } - err = dataprovider.DeleteUser(&user) - if err != nil { - sendAPIResponse(w, r, err, "", http.StatusInternalServerError) - } else { - sendAPIResponse(w, r, err, "User deleted", http.StatusOK) - disconnectUser(user.Username) - } + sendAPIResponse(w, r, err, "User deleted", http.StatusOK) + disconnectUser(username) } func disconnectUser(username string) { diff --git a/httpd/api_utils.go b/httpd/api_utils.go index d4260d86..22eec8d5 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -1,67 +1,16 @@ package httpd import ( - "bytes" "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" "net/http" - "net/url" "os" - "path" - "path/filepath" - "strconv" - "strings" "github.com/go-chi/render" "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" - "github.com/drakkan/sftpgo/httpclient" - "github.com/drakkan/sftpgo/kms" - "github.com/drakkan/sftpgo/utils" - "github.com/drakkan/sftpgo/version" - "github.com/drakkan/sftpgo/vfs" ) -var ( - httpBaseURL = "http://127.0.0.1:8080" - authUsername = "" - authPassword = "" -) - -// 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 -} - -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 contentType != "" { - req.Header.Set("Content-Type", "application/json") - } - if authUsername != "" || authPassword != "" { - req.SetBasicAuth(authUsername, authPassword) - } - return httpclient.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 - p := path.Join(paths...) - return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/")) -} - func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) { var errorString string if err != nil { @@ -91,944 +40,15 @@ func getRespStatus(err error) int { return http.StatusInternalServerError } -// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode. -func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) { - var newUser dataprovider.User - var body []byte - userAsJSON, _ := json.Marshal(user) - resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(userPath), bytes.NewBuffer(userAsJSON), - "application/json") - if err != nil { - return newUser, body, err +func handleCloseConnection(w http.ResponseWriter, r *http.Request) { + connectionID := getURLParam(r, "connectionID") + if connectionID == "" { + sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest) + return } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if expectedStatusCode != http.StatusOK { - body, _ = getResponseBody(resp) - return newUser, body, err - } - if err == nil { - err = render.DecodeJSON(resp.Body, &newUser) + if common.Connections.Close(connectionID) { + sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK) } else { - body, _ = getResponseBody(resp) + sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) } - if err == nil { - err = checkUser(&user, &newUser) - } - return newUser, body, err -} - -// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode. -func UpdateUser(user dataprovider.User, expectedStatusCode int, disconnect string) (dataprovider.User, []byte, error) { - var newUser dataprovider.User - var body []byte - url, err := addDisconnectQueryParam(buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), disconnect) - if err != nil { - return user, body, err - } - userAsJSON, _ := json.Marshal(user) - resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "application/json") - if err != nil { - return user, body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - err = checkResponse(resp.StatusCode, expectedStatusCode) - if expectedStatusCode != http.StatusOK { - return newUser, body, err - } - if err == nil { - newUser, body, err = GetUserByID(user.ID, expectedStatusCode) - } - if err == nil { - err = checkUser(&user, &newUser) - } - return newUser, body, err -} - -// 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 - resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), nil, "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// GetUserByID gets a user by database id and checks the received HTTP Status code against expectedStatusCode. -func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, []byte, error) { - var user dataprovider.User - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(userPath, strconv.FormatInt(userID, 10)), nil, "") - if err != nil { - return user, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &user) - } else { - body, _ = getResponseBody(resp) - } - return user, body, err -} - -// GetUsers returns a list of users and checks the received HTTP Status code against expectedStatusCode. -// The number of results can be limited specifying a limit. -// Some results can be skipped specifying an offset. -// The results can be filtered specifying a username, the username filter is an exact match -func GetUsers(limit, offset int64, username string, expectedStatusCode int) ([]dataprovider.User, []byte, error) { - var users []dataprovider.User - var body []byte - url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(userPath), limit, offset) - if err != nil { - return users, body, err - } - if len(username) > 0 { - q := url.Query() - q.Add("username", username) - url.RawQuery = q.Encode() - } - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return users, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &users) - } else { - body, _ = getResponseBody(resp) - } - return users, body, err -} - -// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode. -func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) { - var quotaScans []common.ActiveQuotaScan - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "") - if err != nil { - return quotaScans, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, "aScans) - } else { - body, _ = getResponseBody(resp) - } - return quotaScans, body, err -} - -// StartQuotaScan starts a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode. -func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, error) { - var body []byte - userAsJSON, _ := json.Marshal(user) - resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanPath), bytes.NewBuffer(userAsJSON), "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// UpdateQuotaUsage updates the user used quota limits and checks the received HTTP Status code against expectedStatusCode. -func UpdateQuotaUsage(user dataprovider.User, mode string, expectedStatusCode int) ([]byte, error) { - var body []byte - userAsJSON, _ := json.Marshal(user) - url, err := addModeQueryParam(buildURLRelativeToBase(updateUsedQuotaPath), mode) - if err != nil { - return body, err - } - resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// GetConnections returns status and stats for active SFTP/SCP connections -func GetConnections(expectedStatusCode int) ([]common.ConnectionStatus, []byte, error) { - var connections []common.ConnectionStatus - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(activeConnectionsPath), nil, "") - if err != nil { - return connections, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &connections) - } else { - body, _ = getResponseBody(resp) - } - return connections, body, err -} - -// CloseConnection closes an active connection identified by connectionID -func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) { - var body []byte - resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), nil, "") - if err != nil { - return body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - body, _ = getResponseBody(resp) - return body, err -} - -// AddFolder adds a new folder and checks the received HTTP Status code against expectedStatusCode -func AddFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) (vfs.BaseVirtualFolder, []byte, error) { - var newFolder vfs.BaseVirtualFolder - var body []byte - folderAsJSON, _ := json.Marshal(folder) - resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(folderPath), bytes.NewBuffer(folderAsJSON), - "application/json") - if err != nil { - return newFolder, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if expectedStatusCode != http.StatusOK { - body, _ = getResponseBody(resp) - return newFolder, body, err - } - if err == nil { - err = render.DecodeJSON(resp.Body, &newFolder) - } else { - body, _ = getResponseBody(resp) - } - if err == nil { - err = checkFolder(&folder, &newFolder) - } - return newFolder, body, err -} - -// RemoveFolder removes an existing user and checks the received HTTP Status code against expectedStatusCode. -func RemoveFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) { - var body []byte - baseURL := buildURLRelativeToBase(folderPath) - url, err := url.Parse(baseURL) - if err != nil { - return body, err - } - q := url.Query() - q.Add("folder_path", folder.MappedPath) - url.RawQuery = q.Encode() - resp, err := sendHTTPRequest(http.MethodDelete, url.String(), nil, "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// GetFolders returns a list of folders and checks the received HTTP Status code against expectedStatusCode. -// The number of results can be limited specifying a limit. -// Some results can be skipped specifying an offset. -// The results can be filtered specifying a folder path, the folder path filter is an exact match -func GetFolders(limit int64, offset int64, mappedPath string, expectedStatusCode int) ([]vfs.BaseVirtualFolder, []byte, error) { - var folders []vfs.BaseVirtualFolder - var body []byte - url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(folderPath), limit, offset) - if err != nil { - return folders, body, err - } - if len(mappedPath) > 0 { - q := url.Query() - q.Add("folder_path", mappedPath) - url.RawQuery = q.Encode() - } - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return folders, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &folders) - } else { - body, _ = getResponseBody(resp) - } - return folders, body, err -} - -// GetFoldersQuotaScans gets active quota scans for folders and checks the received HTTP Status code against expectedStatusCode. -func GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQuotaScan, []byte, error) { - var quotaScans []common.ActiveVirtualFolderQuotaScan - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, "") - if err != nil { - return quotaScans, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, "aScans) - } else { - body, _ = getResponseBody(resp) - } - return quotaScans, body, err -} - -// StartFolderQuotaScan start a new quota scan for the given folder and checks the received HTTP Status code against expectedStatusCode. -func StartFolderQuotaScan(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) { - var body []byte - folderAsJSON, _ := json.Marshal(folder) - resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanVFolderPath), bytes.NewBuffer(folderAsJSON), "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// UpdateFolderQuotaUsage updates the folder used quota limits and checks the received HTTP Status code against expectedStatusCode. -func UpdateFolderQuotaUsage(folder vfs.BaseVirtualFolder, mode string, expectedStatusCode int) ([]byte, error) { - var body []byte - folderAsJSON, _ := json.Marshal(folder) - url, err := addModeQueryParam(buildURLRelativeToBase(updateFolderUsedQuotaPath), mode) - if err != nil { - return body, err - } - resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(folderAsJSON), "") - if err != nil { - return body, err - } - defer resp.Body.Close() - body, _ = getResponseBody(resp) - return body, checkResponse(resp.StatusCode, expectedStatusCode) -} - -// GetVersion returns version details -func GetVersion(expectedStatusCode int) (version.Info, []byte, error) { - var appVersion version.Info - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(versionPath), nil, "") - if err != nil { - return appVersion, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &appVersion) - } else { - body, _ = getResponseBody(resp) - } - return appVersion, body, err -} - -// GetStatus returns the server status -func GetStatus(expectedStatusCode int) (ServicesStatus, []byte, error) { - var response ServicesStatus - var body []byte - resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(serverStatusPath), nil, "") - if err != nil { - return response, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && (expectedStatusCode == http.StatusOK) { - err = render.DecodeJSON(resp.Body, &response) - } else { - body, _ = getResponseBody(resp) - } - return response, body, err -} - -// GetBanTime returns the ban time for the given IP address -func GetBanTime(ip string, expectedStatusCode int) (map[string]interface{}, []byte, error) { - var response map[string]interface{} - var body []byte - url, err := url.Parse(buildURLRelativeToBase(defenderBanTime)) - if err != nil { - return response, body, err - } - q := url.Query() - q.Add("ip", ip) - url.RawQuery = q.Encode() - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return response, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &response) - } else { - body, _ = getResponseBody(resp) - } - return response, body, err -} - -// GetScore returns the score for the given IP address -func GetScore(ip string, expectedStatusCode int) (map[string]interface{}, []byte, error) { - var response map[string]interface{} - var body []byte - url, err := url.Parse(buildURLRelativeToBase(defenderScore)) - if err != nil { - return response, body, err - } - q := url.Query() - q.Add("ip", ip) - url.RawQuery = q.Encode() - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return response, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &response) - } else { - body, _ = getResponseBody(resp) - } - return response, body, err -} - -// UnbanIP unbans the given IP address -func UnbanIP(ip string, expectedStatusCode int) error { - postBody := make(map[string]string) - postBody["ip"] = ip - asJSON, _ := json.Marshal(postBody) - resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(defenderUnban), bytes.NewBuffer(asJSON), "") - if err != nil { - return err - } - defer resp.Body.Close() - return checkResponse(resp.StatusCode, expectedStatusCode) -} - -// Dumpdata requests a backup to outputFile. -// outputFile is relative to the configured backups_path -func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]interface{}, []byte, error) { - var response map[string]interface{} - var body []byte - url, err := url.Parse(buildURLRelativeToBase(dumpDataPath)) - if err != nil { - return response, body, err - } - q := url.Query() - q.Add("output_file", outputFile) - if len(indent) > 0 { - q.Add("indent", indent) - } - url.RawQuery = q.Encode() - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return response, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &response) - } else { - body, _ = getResponseBody(resp) - } - return response, body, err -} - -// Loaddata restores a backup. -// New users are added, existing users are updated. Users will be restored one by one and the restore is stopped if a -// user cannot be added/updated, so it could happen a partial restore -func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[string]interface{}, []byte, error) { - var response map[string]interface{} - var body []byte - url, err := url.Parse(buildURLRelativeToBase(loadDataPath)) - if err != nil { - return response, body, err - } - q := url.Query() - q.Add("input_file", inputFile) - if len(scanQuota) > 0 { - q.Add("scan_quota", scanQuota) - } - if len(mode) > 0 { - q.Add("mode", mode) - } - url.RawQuery = q.Encode() - resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "") - if err != nil { - return response, body, err - } - defer resp.Body.Close() - err = checkResponse(resp.StatusCode, expectedStatusCode) - if err == nil && expectedStatusCode == http.StatusOK { - err = render.DecodeJSON(resp.Body, &response) - } else { - body, _ = getResponseBody(resp) - } - return response, body, err -} - -func checkResponse(actual int, expected int) error { - if expected != actual { - return fmt.Errorf("wrong status code: got %v want %v", actual, expected) - } - return nil -} - -func getResponseBody(resp *http.Response) ([]byte, error) { - return ioutil.ReadAll(resp.Body) -} - -func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder) error { - if expected.ID <= 0 { - if actual.ID <= 0 { - return errors.New("actual folder ID must be > 0") - } - } else { - if actual.ID != expected.ID { - return errors.New("folder ID mismatch") - } - } - if expected.MappedPath != actual.MappedPath { - return errors.New("mapped path mismatch") - } - if expected.LastQuotaUpdate != actual.LastQuotaUpdate { - return errors.New("last quota update mismatch") - } - if expected.UsedQuotaSize != actual.UsedQuotaSize { - return errors.New("used quota size mismatch") - } - if expected.UsedQuotaFiles != actual.UsedQuotaFiles { - return errors.New("used quota files mismatch") - } - if len(expected.Users) != len(actual.Users) { - return errors.New("folder users mismatch") - } - for _, u := range actual.Users { - if !utils.IsStringInSlice(u, expected.Users) { - return errors.New("folder users mismatch") - } - } - return nil -} - -func checkUser(expected *dataprovider.User, actual *dataprovider.User) error { - if actual.Password != "" { - return errors.New("User password must not be visible") - } - if expected.ID <= 0 { - if actual.ID <= 0 { - return errors.New("actual user ID must be > 0") - } - } else { - if actual.ID != expected.ID { - return errors.New("user ID mismatch") - } - } - if len(expected.Permissions) != len(actual.Permissions) { - return errors.New("Permissions mismatch") - } - for dir, perms := range expected.Permissions { - if actualPerms, ok := actual.Permissions[dir]; ok { - for _, v := range actualPerms { - if !utils.IsStringInSlice(v, perms) { - return errors.New("Permissions contents mismatch") - } - } - } else { - return errors.New("Permissions directories mismatch") - } - } - if err := compareUserFilters(expected, actual); err != nil { - return err - } - if err := compareUserFsConfig(expected, actual); err != nil { - return err - } - if err := compareUserVirtualFolders(expected, actual); err != nil { - return err - } - return compareEqualsUserFields(expected, actual) -} - -func compareUserVirtualFolders(expected *dataprovider.User, actual *dataprovider.User) error { - if len(actual.VirtualFolders) != len(expected.VirtualFolders) { - return errors.New("Virtual folders mismatch") - } - for _, v := range actual.VirtualFolders { - found := false - for _, v1 := range expected.VirtualFolders { - if path.Clean(v.VirtualPath) == path.Clean(v1.VirtualPath) && - filepath.Clean(v.MappedPath) == filepath.Clean(v1.MappedPath) { - found = true - break - } - } - if !found { - return errors.New("Virtual folders mismatch") - } - } - return nil -} - -func compareUserFsConfig(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.FsConfig.Provider != actual.FsConfig.Provider { - return errors.New("Fs provider mismatch") - } - if err := compareS3Config(expected, actual); err != nil { - return err - } - if err := compareGCSConfig(expected, actual); err != nil { - return err - } - if err := compareAzBlobConfig(expected, actual); err != nil { - return err - } - if err := checkEncryptedSecret(expected.FsConfig.CryptConfig.Passphrase, actual.FsConfig.CryptConfig.Passphrase); err != nil { - return err - } - if err := compareSFTPFsConfig(expected, actual); err != nil { - return err - } - return nil -} - -func compareS3Config(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.FsConfig.S3Config.Bucket != actual.FsConfig.S3Config.Bucket { - return errors.New("S3 bucket mismatch") - } - if expected.FsConfig.S3Config.Region != actual.FsConfig.S3Config.Region { - return errors.New("S3 region mismatch") - } - if expected.FsConfig.S3Config.AccessKey != actual.FsConfig.S3Config.AccessKey { - return errors.New("S3 access key mismatch") - } - if err := checkEncryptedSecret(expected.FsConfig.S3Config.AccessSecret, actual.FsConfig.S3Config.AccessSecret); err != nil { - return fmt.Errorf("S3 access secret mismatch: %v", err) - } - if expected.FsConfig.S3Config.Endpoint != actual.FsConfig.S3Config.Endpoint { - return errors.New("S3 endpoint mismatch") - } - if expected.FsConfig.S3Config.StorageClass != actual.FsConfig.S3Config.StorageClass { - return errors.New("S3 storage class mismatch") - } - if expected.FsConfig.S3Config.UploadPartSize != actual.FsConfig.S3Config.UploadPartSize { - return errors.New("S3 upload part size mismatch") - } - if expected.FsConfig.S3Config.UploadConcurrency != actual.FsConfig.S3Config.UploadConcurrency { - return errors.New("S3 upload concurrency mismatch") - } - if expected.FsConfig.S3Config.KeyPrefix != actual.FsConfig.S3Config.KeyPrefix && - expected.FsConfig.S3Config.KeyPrefix+"/" != actual.FsConfig.S3Config.KeyPrefix { - return errors.New("S3 key prefix mismatch") - } - return nil -} - -func compareGCSConfig(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.FsConfig.GCSConfig.Bucket != actual.FsConfig.GCSConfig.Bucket { - return errors.New("GCS bucket mismatch") - } - if expected.FsConfig.GCSConfig.StorageClass != actual.FsConfig.GCSConfig.StorageClass { - return errors.New("GCS storage class mismatch") - } - if expected.FsConfig.GCSConfig.KeyPrefix != actual.FsConfig.GCSConfig.KeyPrefix && - expected.FsConfig.GCSConfig.KeyPrefix+"/" != actual.FsConfig.GCSConfig.KeyPrefix { - return errors.New("GCS key prefix mismatch") - } - if expected.FsConfig.GCSConfig.AutomaticCredentials != actual.FsConfig.GCSConfig.AutomaticCredentials { - return errors.New("GCS automatic credentials mismatch") - } - return nil -} - -func compareSFTPFsConfig(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.FsConfig.SFTPConfig.Endpoint != actual.FsConfig.SFTPConfig.Endpoint { - return errors.New("SFTPFs endpoint mismatch") - } - if expected.FsConfig.SFTPConfig.Username != actual.FsConfig.SFTPConfig.Username { - return errors.New("SFTPFs username mismatch") - } - if err := checkEncryptedSecret(expected.FsConfig.SFTPConfig.Password, actual.FsConfig.SFTPConfig.Password); err != nil { - return fmt.Errorf("SFTPFs password mismatch: %v", err) - } - if err := checkEncryptedSecret(expected.FsConfig.SFTPConfig.PrivateKey, actual.FsConfig.SFTPConfig.PrivateKey); err != nil { - return fmt.Errorf("SFTPFs private key mismatch: %v", err) - } - if expected.FsConfig.SFTPConfig.Prefix != actual.FsConfig.SFTPConfig.Prefix { - if expected.FsConfig.SFTPConfig.Prefix != "" && actual.FsConfig.SFTPConfig.Prefix != "/" { - return errors.New("SFTPFs prefix mismatch") - } - } - if len(expected.FsConfig.SFTPConfig.Fingerprints) != len(actual.FsConfig.SFTPConfig.Fingerprints) { - return errors.New("SFTPFs fingerprints mismatch") - } - for _, value := range actual.FsConfig.SFTPConfig.Fingerprints { - if !utils.IsStringInSlice(value, expected.FsConfig.SFTPConfig.Fingerprints) { - return errors.New("SFTPFs fingerprints mismatch") - } - } - return nil -} - -func compareAzBlobConfig(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.FsConfig.AzBlobConfig.Container != actual.FsConfig.AzBlobConfig.Container { - return errors.New("Azure Blob container mismatch") - } - if expected.FsConfig.AzBlobConfig.AccountName != actual.FsConfig.AzBlobConfig.AccountName { - return errors.New("Azure Blob account name mismatch") - } - if err := checkEncryptedSecret(expected.FsConfig.AzBlobConfig.AccountKey, actual.FsConfig.AzBlobConfig.AccountKey); err != nil { - return fmt.Errorf("Azure Blob account key mismatch: %v", err) - } - if expected.FsConfig.AzBlobConfig.Endpoint != actual.FsConfig.AzBlobConfig.Endpoint { - return errors.New("Azure Blob endpoint mismatch") - } - if expected.FsConfig.AzBlobConfig.SASURL != actual.FsConfig.AzBlobConfig.SASURL { - return errors.New("Azure Blob SASL URL mismatch") - } - if expected.FsConfig.AzBlobConfig.UploadPartSize != actual.FsConfig.AzBlobConfig.UploadPartSize { - return errors.New("Azure Blob upload part size mismatch") - } - if expected.FsConfig.AzBlobConfig.UploadConcurrency != actual.FsConfig.AzBlobConfig.UploadConcurrency { - return errors.New("Azure Blob upload concurrency mismatch") - } - if expected.FsConfig.AzBlobConfig.KeyPrefix != actual.FsConfig.AzBlobConfig.KeyPrefix && - expected.FsConfig.AzBlobConfig.KeyPrefix+"/" != actual.FsConfig.AzBlobConfig.KeyPrefix { - return errors.New("Azure Blob key prefix mismatch") - } - if expected.FsConfig.AzBlobConfig.UseEmulator != actual.FsConfig.AzBlobConfig.UseEmulator { - return errors.New("Azure Blob use emulator mismatch") - } - if expected.FsConfig.AzBlobConfig.AccessTier != actual.FsConfig.AzBlobConfig.AccessTier { - return errors.New("Azure Blob access tier mismatch") - } - return nil -} - -func areSecretEquals(expected, actual *kms.Secret) bool { - if expected == nil && actual == nil { - return true - } - if expected != nil && expected.IsEmpty() && actual == nil { - return true - } - if actual != nil && actual.IsEmpty() && expected == nil { - return true - } - return false -} - -func checkEncryptedSecret(expected, actual *kms.Secret) error { - if areSecretEquals(expected, actual) { - return nil - } - if expected == nil && actual != nil && !actual.IsEmpty() { - return errors.New("secret mismatch") - } - if actual == nil && expected != nil && !expected.IsEmpty() { - return errors.New("secret mismatch") - } - if expected.IsPlain() && actual.IsEncrypted() { - if actual.GetPayload() == "" { - return errors.New("invalid secret payload") - } - if actual.GetAdditionalData() != "" { - return errors.New("invalid secret additional data") - } - if actual.GetKey() != "" { - return errors.New("invalid secret key") - } - } else { - if expected.GetStatus() != actual.GetStatus() || expected.GetPayload() != actual.GetPayload() { - return errors.New("secret mismatch") - } - } - return nil -} - -func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User) error { - if len(expected.Filters.AllowedIP) != len(actual.Filters.AllowedIP) { - return errors.New("AllowedIP mismatch") - } - if len(expected.Filters.DeniedIP) != len(actual.Filters.DeniedIP) { - return errors.New("DeniedIP mismatch") - } - if len(expected.Filters.DeniedLoginMethods) != len(actual.Filters.DeniedLoginMethods) { - return errors.New("Denied login methods mismatch") - } - if len(expected.Filters.DeniedProtocols) != len(actual.Filters.DeniedProtocols) { - return errors.New("Denied protocols mismatch") - } - if expected.Filters.MaxUploadFileSize != actual.Filters.MaxUploadFileSize { - return errors.New("Max upload file size mismatch") - } - for _, IPMask := range expected.Filters.AllowedIP { - if !utils.IsStringInSlice(IPMask, actual.Filters.AllowedIP) { - return errors.New("AllowedIP contents mismatch") - } - } - for _, IPMask := range expected.Filters.DeniedIP { - if !utils.IsStringInSlice(IPMask, actual.Filters.DeniedIP) { - return errors.New("DeniedIP contents mismatch") - } - } - for _, method := range expected.Filters.DeniedLoginMethods { - if !utils.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) { - return errors.New("Denied login methods contents mismatch") - } - } - for _, protocol := range expected.Filters.DeniedProtocols { - if !utils.IsStringInSlice(protocol, actual.Filters.DeniedProtocols) { - return errors.New("Denied protocols contents mismatch") - } - } - if err := compareUserFileExtensionsFilters(expected, actual); err != nil { - return err - } - return compareUserFilePatternsFilters(expected, actual) -} - -func checkFilterMatch(expected []string, actual []string) bool { - if len(expected) != len(actual) { - return false - } - for _, e := range expected { - if !utils.IsStringInSlice(strings.ToLower(e), actual) { - return false - } - } - return true -} - -func compareUserFilePatternsFilters(expected *dataprovider.User, actual *dataprovider.User) error { - if len(expected.Filters.FilePatterns) != len(actual.Filters.FilePatterns) { - return errors.New("file patterns mismatch") - } - for _, f := range expected.Filters.FilePatterns { - found := false - for _, f1 := range actual.Filters.FilePatterns { - if path.Clean(f.Path) == path.Clean(f1.Path) { - if !checkFilterMatch(f.AllowedPatterns, f1.AllowedPatterns) || - !checkFilterMatch(f.DeniedPatterns, f1.DeniedPatterns) { - return errors.New("file patterns contents mismatch") - } - found = true - } - } - if !found { - return errors.New("file patterns contents mismatch") - } - } - return nil -} - -func compareUserFileExtensionsFilters(expected *dataprovider.User, actual *dataprovider.User) error { - if len(expected.Filters.FileExtensions) != len(actual.Filters.FileExtensions) { - return errors.New("file extensions mismatch") - } - for _, f := range expected.Filters.FileExtensions { - found := false - for _, f1 := range actual.Filters.FileExtensions { - if path.Clean(f.Path) == path.Clean(f1.Path) { - if !checkFilterMatch(f.AllowedExtensions, f1.AllowedExtensions) || - !checkFilterMatch(f.DeniedExtensions, f1.DeniedExtensions) { - return errors.New("file extensions contents mismatch") - } - found = true - } - } - if !found { - return errors.New("file extensions contents mismatch") - } - } - return nil -} - -func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.Username != actual.Username { - return errors.New("Username mismatch") - } - if expected.HomeDir != actual.HomeDir { - return errors.New("HomeDir mismatch") - } - if expected.UID != actual.UID { - return errors.New("UID mismatch") - } - if expected.GID != actual.GID { - return errors.New("GID mismatch") - } - if expected.MaxSessions != actual.MaxSessions { - return errors.New("MaxSessions mismatch") - } - if expected.QuotaSize != actual.QuotaSize { - return errors.New("QuotaSize mismatch") - } - if expected.QuotaFiles != actual.QuotaFiles { - return errors.New("QuotaFiles mismatch") - } - if len(expected.Permissions) != len(actual.Permissions) { - return errors.New("Permissions mismatch") - } - if expected.UploadBandwidth != actual.UploadBandwidth { - return errors.New("UploadBandwidth mismatch") - } - if expected.DownloadBandwidth != actual.DownloadBandwidth { - return errors.New("DownloadBandwidth mismatch") - } - if expected.Status != actual.Status { - return errors.New("Status mismatch") - } - if expected.ExpirationDate != actual.ExpirationDate { - return errors.New("ExpirationDate mismatch") - } - if expected.AdditionalInfo != actual.AdditionalInfo { - return errors.New("AdditionalInfo mismatch") - } - return nil -} - -func addLimitAndOffsetQueryParams(rawurl string, limit, offset int64) (*url.URL, error) { - url, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - q := url.Query() - if limit > 0 { - q.Add("limit", strconv.FormatInt(limit, 10)) - } - if offset > 0 { - q.Add("offset", strconv.FormatInt(offset, 10)) - } - url.RawQuery = q.Encode() - return url, err -} - -func addModeQueryParam(rawurl, mode string) (*url.URL, error) { - url, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - q := url.Query() - if len(mode) > 0 { - q.Add("mode", mode) - } - url.RawQuery = q.Encode() - return url, err -} - -func addDisconnectQueryParam(rawurl, disconnect string) (*url.URL, error) { - url, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - q := url.Query() - if len(disconnect) > 0 { - q.Add("disconnect", disconnect) - } - url.RawQuery = q.Encode() - return url, err } diff --git a/httpd/auth.go b/httpd/auth.go deleted file mode 100644 index 72b28b80..00000000 --- a/httpd/auth.go +++ /dev/null @@ -1,34 +0,0 @@ -package httpd - -import ( - "net/http" - "strings" - - "github.com/drakkan/sftpgo/common" -) - -func checkAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !validateCredentials(r) { - w.Header().Set(common.HTTPAuthenticationHeader, "Basic realm=\"SFTPGo Web\"") - if strings.HasPrefix(r.RequestURI, apiPrefix) { - sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } else { - http.Error(w, http.StatusText(http.StatusUnauthorized), 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 - } - return httpAuth.ValidateCredentials(username, password) -} diff --git a/httpd/auth_utils.go b/httpd/auth_utils.go new file mode 100644 index 00000000..937342b6 --- /dev/null +++ b/httpd/auth_utils.go @@ -0,0 +1,147 @@ +package httpd + +import ( + "net/http" + "time" + + "github.com/go-chi/jwtauth" + "github.com/lestrrat-go/jwx/jwt" + "github.com/rs/xid" + + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/utils" +) + +const ( + claimUsernameKey = "username" + claimPermissionsKey = "permissions" + basicRealm = "Basic realm=\"SFTPGo\"" +) + +var ( + tokenDuration = 10 * time.Minute + tokenRefreshMin = 5 * time.Minute +) + +type jwtTokenClaims struct { + Username string + Permissions []string + Signature string +} + +func (c *jwtTokenClaims) asMap() map[string]interface{} { + claims := make(map[string]interface{}) + + claims[claimUsernameKey] = c.Username + claims[claimPermissionsKey] = c.Permissions + claims[jwt.SubjectKey] = c.Signature + + return claims +} + +func (c *jwtTokenClaims) Decode(token map[string]interface{}) { + username := token[claimUsernameKey] + + switch v := username.(type) { + case string: + c.Username = v + } + + signature := token[jwt.SubjectKey] + + switch v := signature.(type) { + case string: + c.Signature = v + } + + permissions := token[claimPermissionsKey] + switch v := permissions.(type) { + case []interface{}: + for _, elem := range v { + switch elemValue := elem.(type) { + case string: + c.Permissions = append(c.Permissions, elemValue) + } + } + } +} + +func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool { + if utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) { + return false + } + if (utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, c.Permissions) || + utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions)) && + !utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, permissions) && + !utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) { + return true + } + return false +} + +func (c *jwtTokenClaims) hasPerm(perm string) bool { + if utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions) { + return true + } + + return utils.IsStringInSlice(perm, c.Permissions) +} + +func (c *jwtTokenClaims) createTokenResponse(tokenAuth *jwtauth.JWTAuth) (map[string]interface{}, error) { + claims := c.asMap() + now := time.Now().UTC() + + claims[jwt.JwtIDKey] = xid.New().String() + claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second) + claims[jwt.ExpirationKey] = now.Add(tokenDuration) + + token, tokenString, err := tokenAuth.Encode(claims) + if err != nil { + return nil, err + } + + response := make(map[string]interface{}) + response["access_token"] = tokenString + response["expires_at"] = token.Expiration().Format(time.RFC3339) + + return response, nil +} + +func (c *jwtTokenClaims) createAndSetCookie(w http.ResponseWriter, tokenAuth *jwtauth.JWTAuth) error { + resp, err := c.createTokenResponse(tokenAuth) + if err != nil { + return err + } + http.SetCookie(w, &http.Cookie{ + Name: "jwt", + Value: resp["access_token"].(string), + Path: webBasePath, + Expires: time.Now().Add(tokenDuration), + HttpOnly: true, + }) + + return nil +} + +func (c *jwtTokenClaims) removeCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: "jwt", + Value: "", + Path: webBasePath, + MaxAge: -1, + HttpOnly: true, + }) +} + +func getAdminFromToken(r *http.Request) *dataprovider.Admin { + admin := &dataprovider.Admin{} + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + return admin + } + tokenClaims := jwtTokenClaims{} + tokenClaims.Decode(claims) + admin.Username = tokenClaims.Username + admin.Permissions = tokenClaims.Permissions + return admin +} diff --git a/httpd/httpd.go b/httpd/httpd.go index d4be5eab..02dd5f81 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -1,19 +1,16 @@ // Package httpd implements REST API and Web interface for SFTPGo. -// REST API allows to manage users and quota and to get real time reports for the active connections -// with possibility of forcibly closing a connection. // The OpenAPI 3 schema for the exposed API can be found inside the source tree: // https://github.com/drakkan/sftpgo/blob/master/httpd/schema/openapi.yaml // A basic Web interface to manage users and connections is provided too package httpd import ( - "crypto/tls" "fmt" - "log" "net/http" + "net/url" "path/filepath" "runtime" - "time" + "strings" "github.com/go-chi/chi" @@ -28,29 +25,38 @@ import ( const ( logSender = "httpd" - apiPrefix = "/api/v1" - activeConnectionsPath = "/api/v1/connection" - quotaScanPath = "/api/v1/quota_scan" - quotaScanVFolderPath = "/api/v1/folder_quota_scan" - userPath = "/api/v1/user" - versionPath = "/api/v1/version" - folderPath = "/api/v1/folder" - serverStatusPath = "/api/v1/status" - dumpDataPath = "/api/v1/dumpdata" - loadDataPath = "/api/v1/loaddata" - updateUsedQuotaPath = "/api/v1/quota_update" - updateFolderUsedQuotaPath = "/api/v1/folder_quota_update" - defenderBanTime = "/api/v1/defender/ban_time" - defenderUnban = "/api/v1/defender/unban" - defenderScore = "/api/v1/defender/score" - metricsPath = "/metrics" + tokenPath = "/api/v2/token" + activeConnectionsPath = "/api/v2/connections" + quotaScanPath = "/api/v2/quota-scans" + quotaScanVFolderPath = "/api/v2/folder-quota-scans" + userPath = "/api/v2/users" + versionPath = "/api/v2/version" + folderPath = "/api/v2/folders" + serverStatusPath = "/api/v2/status" + dumpDataPath = "/api/v2/dumpdata" + loadDataPath = "/api/v2/loaddata" + updateUsedQuotaPath = "/api/v2/quota-update" + updateFolderUsedQuotaPath = "/api/v2/folder-quota-update" + defenderBanTime = "/api/v2/defender/bantime" + defenderUnban = "/api/v2/defender/unban" + defenderScore = "/api/v2/defender/score" + adminPath = "/api/v2/admins" + adminPwdPath = "/api/v2/changepwd/admin" + healthzPath = "/healthz" webBasePath = "/web" + webLoginPath = "/web/login" + webLogoutPath = "/web/logout" webUsersPath = "/web/users" webUserPath = "/web/user" webConnectionsPath = "/web/connections" webFoldersPath = "/web/folders" webFolderPath = "/web/folder" webStatusPath = "/web/status" + webAdminsPath = "/web/admins" + webAdminPath = "/web/admin" + webScanVFolderPath = "/web/folder-quota-scans" + webQuotaScanPath = "/web/quota-scans" + webChangeAdminPwdPath = "/web/changepwd/admin" webStaticFilesPath = "/static" // MaxRestoreSize defines the max size for the loaddata input file MaxRestoreSize = 10485760 // 10 MB @@ -59,12 +65,18 @@ const ( ) var ( - router *chi.Mux backupsPath string - httpAuth common.HTTPAuthProvider certMgr *common.CertManager ) +// Binding defines the configuration for a network listener +type Binding struct { + // The address to listen on. A blank value means listen on all available network interfaces. + Address string `json:"address" mapstructure:"address"` + // The port used for serving requests + Port int `json:"port" mapstructure:"port"` +} + type defenderStatus struct { IsActive bool `json:"is_active"` } @@ -91,12 +103,6 @@ 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. // Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a @@ -128,7 +134,7 @@ func (c Conf) Initialize(configDir string) error { backupsPath = getConfigPath(c.BackupsPath, configDir) staticFilesPath := getConfigPath(c.StaticFilesPath, configDir) templatesPath := getConfigPath(c.TemplatesPath, configDir) - enableWebAdmin := len(staticFilesPath) > 0 || len(templatesPath) > 0 + enableWebAdmin := staticFilesPath != "" || templatesPath != "" if backupsPath == "" { return fmt.Errorf("Required directory is invalid, backup path %#v", backupsPath) } @@ -136,11 +142,6 @@ func (c Conf) Initialize(configDir string) error { return fmt.Errorf("Required directory is invalid, static file path: %#v template path: %#v", staticFilesPath, templatesPath) } - authUserFile := getConfigPath(c.AuthUserFile, configDir) - httpAuth, err = common.NewBasicAuthProvider(authUserFile) - if err != nil { - return err - } certificateFile := getConfigPath(c.CertificateFile, configDir) certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir) if enableWebAdmin { @@ -148,28 +149,18 @@ func (c Conf) Initialize(configDir string) error { } else { logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it") } - initializeRouter(staticFilesPath, enableWebAdmin) - httpServer := &http.Server{ - Handler: router, - ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, - IdleTimeout: 120 * time.Second, - MaxHeaderBytes: 1 << 16, // 64KB - ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), - } if certificateFile != "" && certificateKeyFile != "" { certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender) if err != nil { return err } - config := &tls.Config{ - GetCertificate: certMgr.GetCertificateFunc(), - MinVersion: tls.VersionTLS12, - } - httpServer.TLSConfig = config - return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender) } - return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender) + server := newHttpdServer(c.BindAddress, c.BindPort, staticFilesPath, enableWebAdmin) + return server.listenAndServe() +} + +func isWebAdminRequest(r *http.Request) bool { + return strings.HasPrefix(r.RequestURI, webBasePath+"/") } // ReloadCertificateMgr reloads the certificate manager @@ -202,3 +193,34 @@ func getServicesStatus() ServicesStatus { } return status } + +func getURLParam(r *http.Request, key string) string { + v := chi.URLParam(r, key) + unescaped, err := url.PathUnescape(v) + if err != nil { + return v + } + return unescaped +} + +func fileServer(r chi.Router, path string, root http.FileSystem) { + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) + path += "/" + } + path += "*" + + r.Get(path, func(w http.ResponseWriter, r *http.Request) { + rctx := chi.RouteContext(r.Context()) + pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") + fs := http.StripPrefix(pathPrefix, http.FileServer(root)) + fs.ServeHTTP(w, r) + }) +} + +// GetHTTPRouter returns an HTTP handler suitable to use for test cases +func GetHTTPRouter() http.Handler { + server := newHttpdServer("", 8080, "../static", true) + server.initializeRouter() + return server.router +} diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 5581848e..e1039c17 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -13,6 +13,7 @@ import ( "net/http/httptest" "net/url" "os" + "path" "path/filepath" "runtime" "strconv" @@ -31,7 +32,9 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpclient" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" @@ -42,24 +45,36 @@ const ( defaultUsername = "test_user" defaultPassword = "test_password" testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1" - userPath = "/api/v1/user" - folderPath = "/api/v1/folder" - activeConnectionsPath = "/api/v1/connection" - serverStatusPath = "/api/v1/status" - quotaScanPath = "/api/v1/quota_scan" - quotaScanVFolderPath = "/api/v1/folder_quota_scan" - updateUsedQuotaPath = "/api/v1/quota_update" - updateFolderUsedQuotaPath = "/api/v1/folder_quota_update" - defenderUnban = "/api/v1/defender/unban" - versionPath = "/api/v1/version" - metricsPath = "/metrics" + defaultTokenAuthUser = "admin" + defaultTokenAuthPass = "password" + altAdminUsername = "newTestAdmin" + altAdminPassword = "password1" + userPath = "/api/v2/users" + adminPath = "/api/v2/admins" + adminPwdPath = "/api/v2/changepwd/admin" + folderPath = "/api/v2/folders" + activeConnectionsPath = "/api/v2/connections" + serverStatusPath = "/api/v2/status" + quotaScanPath = "/api/v2/quota-scans" + quotaScanVFolderPath = "/api/v2/folder-quota-scans" + updateUsedQuotaPath = "/api/v2/quota-update" + updateFolderUsedQuotaPath = "/api/v2/folder-quota-update" + defenderUnban = "/api/v2/defender/unban" + versionPath = "/api/v2/version" + healthzPath = "/healthz" webBasePath = "/web" + webLoginPath = "/web/login" + webLogoutPath = "/web/logout" webUsersPath = "/web/users" webUserPath = "/web/user" webFoldersPath = "/web/folders" webFolderPath = "/web/folder" webConnectionsPath = "/web/connections" webStatusPath = "/web/status" + webAdminsPath = "/web/admins" + webAdminPath = "/web/admin" + webChangeAdminPwdPath = "/web/changepwd/admin" + httpBaseURL = "http://127.0.0.1:8081" configDir = ".." httpsCert = `-----BEGIN CERTIFICATE----- MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw @@ -147,7 +162,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) if err != nil { logger.WarnToConsole("error initializing data provider: %v", err) os.Exit(1) @@ -165,7 +180,7 @@ func TestMain(m *testing.M) { httpdConf := config.GetHTTPDConfig() httpdConf.BindPort = 8081 - httpd.SetBaseURLAndCredentials("http://127.0.0.1:8081", "", "") + httpdtest.SetBaseURL(httpBaseURL) backupsPath = filepath.Join(os.TempDir(), "test_backups") httpdConf.BackupsPath = backupsPath err = os.MkdirAll(backupsPath, os.ModePerm) @@ -225,12 +240,7 @@ func TestInitialization(t *testing.T) { assert.NoError(t, err) invalidFile := "invalid file" httpdConf := config.GetHTTPDConfig() - httpdConf.BackupsPath = "test_backups" - httpdConf.AuthUserFile = invalidFile - err = httpdConf.Initialize(configDir) - assert.Error(t, err) httpdConf.BackupsPath = backupsPath - httpdConf.AuthUserFile = "" httpdConf.CertificateFile = invalidFile httpdConf.CertificateKeyFile = invalidFile err = httpdConf.Initialize(configDir) @@ -256,7 +266,7 @@ func TestInitialization(t *testing.T) { } func TestBasicUserHandling(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.MaxSessions = 10 user.QuotaSize = 4096 @@ -266,32 +276,121 @@ func TestBasicUserHandling(t *testing.T) { user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) user.AdditionalInfo = "some free text" originalUser := user - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Equal(t, originalUser.ID, user.ID) - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) +} + +func TestBasicAdminHandling(t *testing.T) { + // we have one admin by default + admins, _, err := httpdtest.GetAdmins(0, 0, http.StatusOK) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(admins), 1) + admin := getTestAdmin() + // the default admin already exists + _, _, err = httpdtest.AddAdmin(admin, http.StatusInternalServerError) + assert.NoError(t, err) + + admin.Username = altAdminUsername + admin, _, err = httpdtest.AddAdmin(admin, http.StatusCreated) + assert.NoError(t, err) + + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + + admin.AdditionalInfo = "test info" + admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, "test info", admin.AdditionalInfo) + + admins, _, err = httpdtest.GetAdmins(1, 0, http.StatusOK) + assert.NoError(t, err) + assert.Len(t, admins, 1) + assert.NotEqual(t, admin.Username, admins[0].Username) + + admins, _, err = httpdtest.GetAdmins(1, 1, http.StatusOK) + assert.NoError(t, err) + assert.Len(t, admins, 1) + assert.Equal(t, admin.Username, admins[0].Username) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusNotFound) + assert.NoError(t, err) + + admin, _, err = httpdtest.GetAdminByUsername(admin.Username+"123", http.StatusNotFound) + assert.NoError(t, err) + + admin.Username = defaultTokenAuthUser + _, err = httpdtest.RemoveAdmin(admin, http.StatusBadRequest) + assert.NoError(t, err) +} + +func TestChangeAdminPassword(t *testing.T) { + _, err := httpdtest.ChangeAdminPassword("wrong", defaultTokenAuthPass, http.StatusBadRequest) + assert.NoError(t, err) + _, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass, defaultTokenAuthPass, http.StatusBadRequest) + assert.NoError(t, err) + _, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass, defaultTokenAuthPass+"1", http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass+"1", defaultTokenAuthPass, http.StatusUnauthorized) + assert.NoError(t, err) + admin, err := dataprovider.AdminExists(defaultTokenAuthUser) + assert.NoError(t, err) + admin.Password = defaultTokenAuthPass + err = dataprovider.UpdateAdmin(&admin) + assert.NoError(t, err) +} + +func TestAdminAllowList(t *testing.T) { + a := getTestAdmin() + a.Username = altAdminUsername + a.Password = altAdminPassword + + admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated) + assert.NoError(t, err) + + token, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + httpdtest.SetJWTToken(token) + _, _, err = httpdtest.GetStatus(http.StatusOK) + assert.NoError(t, err) + + httpdtest.SetJWTToken("") + + admin.Password = altAdminPassword + admin.Filters.AllowList = []string{"10.6.6.0/32"} + admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) + assert.NoError(t, err) + + _, _, err = httpdtest.GetToken(altAdminUsername, altAdminPassword) + assert.EqualError(t, err, "wrong status code: got 401 want 200") + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) assert.NoError(t, err) } func TestUserStatus(t *testing.T) { u := getTestUser() u.Status = 3 - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Status = 0 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.Status = 2 - _, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "") assert.NoError(t, err) user.Status = 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -299,72 +398,72 @@ func TestAddUserNoCredentials(t *testing.T) { u := getTestUser() u.Password = "" u.PublicKeys = []string{} - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserNoUsername(t *testing.T) { u := getTestUser() u.Username = "" - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserNoHomeDir(t *testing.T) { u := getTestUser() u.HomeDir = "" - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserInvalidHomeDir(t *testing.T) { u := getTestUser() u.HomeDir = "relative_path" //nolint:goconst - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserNoPerms(t *testing.T) { u := getTestUser() u.Permissions = make(map[string][]string) - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Permissions["/"] = []string{} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserInvalidPerms(t *testing.T) { u := getTestUser() u.Permissions["/"] = []string{"invalidPerm"} - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) // permissions for root dir are mandatory u.Permissions["/"] = []string{} u.Permissions["/somedir"] = []string{dataprovider.PermAny} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir/.."] = []string{dataprovider.PermAny} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } func TestAddUserInvalidFilters(t *testing.T) { u := getTestUser() u.Filters.AllowedIP = []string{"192.168.1.0/24", "192.168.2.0"} - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.AllowedIP = []string{} u.Filters.DeniedIP = []string{"192.168.3.0/16", "invalid"} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.DeniedIP = []string{} u.Filters.DeniedLoginMethods = []string{"invalid"} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.DeniedLoginMethods = dataprovider.ValidSSHLoginMethods - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.DeniedLoginMethods = []string{} u.Filters.FileExtensions = []dataprovider.ExtensionsFilter{ @@ -374,7 +473,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedExtensions: []string{}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FileExtensions = []dataprovider.ExtensionsFilter{ { @@ -383,7 +482,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedExtensions: []string{}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FileExtensions = []dataprovider.ExtensionsFilter{ { @@ -397,7 +496,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedExtensions: []string{".jpg"}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FileExtensions = nil u.Filters.FilePatterns = []dataprovider.PatternsFilter{ @@ -407,7 +506,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedPatterns: []string{}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FilePatterns = []dataprovider.PatternsFilter{ { @@ -416,7 +515,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedPatterns: []string{}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FilePatterns = []dataprovider.PatternsFilter{ { @@ -429,7 +528,7 @@ func TestAddUserInvalidFilters(t *testing.T) { DeniedPatterns: []string{"*.jpg"}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.FilePatterns = []dataprovider.PatternsFilter{ { @@ -437,13 +536,13 @@ func TestAddUserInvalidFilters(t *testing.T) { AllowedPatterns: []string{"a\\"}, }, } - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.DeniedProtocols = []string{"invalid"} - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.Filters.DeniedProtocols = dataprovider.ValidProtocols - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } @@ -451,7 +550,7 @@ func TestAddUserInvalidFsConfig(t *testing.T) { u := getTestUser() u.FsConfig.Provider = dataprovider.S3FilesystemProvider u.FsConfig.S3Config.Bucket = "" - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) err = os.RemoveAll(credentialsPath) assert.NoError(t, err) @@ -464,89 +563,89 @@ func TestAddUserInvalidFsConfig(t *testing.T) { u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b" u.FsConfig.S3Config.StorageClass = "Standard" //nolint:goconst u.FsConfig.S3Config.KeyPrefix = "/adir/subdir/" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.S3Config.KeyPrefix = "" u.FsConfig.S3Config.UploadPartSize = 3 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.S3Config.UploadPartSize = 5001 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.S3Config.UploadPartSize = 0 u.FsConfig.S3Config.UploadConcurrency = -1 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u = getTestUser() u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.GCSConfig.Bucket = "abucket" u.FsConfig.GCSConfig.StorageClass = "Standard" u.FsConfig.GCSConfig.KeyPrefix = "/somedir/subdir/" u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusRedacted, "test", "", "") //nolint:goconst - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/" //nolint:goconst u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() u.FsConfig.GCSConfig.AutomaticCredentials = 0 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "invalid", "", "") - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u = getTestUser() u.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider u.FsConfig.AzBlobConfig.SASURL = "http://foo\x7f.com/" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.SASURL = "" u.FsConfig.AzBlobConfig.AccountName = "name" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.Container = "container" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusRedacted, "key", "", "") u.FsConfig.AzBlobConfig.KeyPrefix = "/amedir/subdir/" - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusPlain) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.KeyPrefix = "amedir/subdir/" u.FsConfig.AzBlobConfig.UploadPartSize = -1 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.UploadPartSize = 101 - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u = getTestUser() u.FsConfig.Provider = dataprovider.CryptedFilesystemProvider - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.CryptConfig.Passphrase = kms.NewSecret(kms.SecretStatusRedacted, "akey", "", "") - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u = getTestUser() u.FsConfig.Provider = dataprovider.SFTPFilesystemProvider - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.SFTPConfig.Password = kms.NewSecret(kms.SecretStatusRedacted, "randompkey", "", "") - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.SFTPConfig.Password = kms.NewEmptySecret() u.FsConfig.SFTPConfig.PrivateKey = kms.NewSecret(kms.SecretStatusRedacted, "keyforpkey", "", "") - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } @@ -558,7 +657,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "vdir", }) - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -567,7 +666,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -576,7 +675,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -585,7 +684,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -594,7 +693,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -609,7 +708,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -624,7 +723,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir2", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -639,7 +738,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir2", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -654,7 +753,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir2", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -669,7 +768,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir1/../vdir1", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -684,7 +783,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { }, VirtualPath: "/vdir1/subdir", }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -695,7 +794,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { QuotaSize: -1, QuotaFiles: 1, }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -706,7 +805,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { QuotaSize: 1, QuotaFiles: -1, }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -717,7 +816,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { QuotaSize: -2, QuotaFiles: 0, }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.VirtualFolders = nil u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ @@ -728,7 +827,7 @@ func TestAddUserInvalidVirtualFolders(t *testing.T) { QuotaSize: 0, QuotaFiles: -2, }) - _, _, err = httpd.AddUser(u, http.StatusBadRequest) + _, _, err = httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) } @@ -737,18 +836,18 @@ func TestUserPublicKey(t *testing.T) { invalidPubKey := "invalid" validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1" u.PublicKeys = []string{invalidPubKey} - _, _, err := httpd.AddUser(u, http.StatusBadRequest) + _, _, err := httpdtest.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.PublicKeys = []string{validPubKey} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.PublicKeys = []string{validPubKey, invalidPubKey} - _, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "") assert.NoError(t, err) user.PublicKeys = []string{validPubKey, validPubKey, validPubKey} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -756,7 +855,7 @@ func TestUpdateUser(t *testing.T) { u := getTestUser() u.UsedQuotaFiles = 1 u.UsedQuotaSize = 2 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) @@ -802,16 +901,16 @@ func TestUpdateUser(t *testing.T) { QuotaSize: 123, QuotaFiles: 2, }) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "invalid") + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "invalid") assert.NoError(t, err) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "0") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "0") assert.NoError(t, err) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "1") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "1") assert.NoError(t, err) user.Permissions["/subdir"] = []string{} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Len(t, user.Permissions["/subdir"], 0) assert.Len(t, user.VirtualFolders, 2) @@ -822,7 +921,7 @@ func TestUpdateUser(t *testing.T) { assert.Equal(t, 2, folder.QuotaFiles) } } - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -830,23 +929,23 @@ func TestUpdateUser(t *testing.T) { assert.Contains(t, f.Users, user.Username) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) // removing the user must remove folder mapping - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Len(t, f.Users, 0) - _, err = httpd.RemoveFolder(f, http.StatusOK) + _, err = httpdtest.RemoveFolder(f, http.StatusOK) assert.NoError(t, err) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Len(t, f.Users, 0) - _, err = httpd.RemoveFolder(f, http.StatusOK) + _, err = httpdtest.RemoveFolder(f, http.StatusOK) assert.NoError(t, err) } } @@ -857,39 +956,39 @@ func TestUpdateUserQuotaUsage(t *testing.T) { usedQuotaSize := int64(65535) u.UsedQuotaFiles = usedQuotaFiles u.UsedQuotaSize = usedQuotaSize - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - _, err = httpd.UpdateQuotaUsage(u, "invalid_mode", http.StatusBadRequest) + _, err = httpdtest.UpdateQuotaUsage(u, "invalid_mode", http.StatusBadRequest) assert.NoError(t, err) - _, err = httpd.UpdateQuotaUsage(u, "", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(u, "", http.StatusOK) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, usedQuotaSize, user.UsedQuotaSize) - _, err = httpd.UpdateQuotaUsage(u, "add", http.StatusBadRequest) + _, err = httpdtest.UpdateQuotaUsage(u, "add", http.StatusBadRequest) assert.NoError(t, err, "user has no quota restrictions add mode should fail") - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, usedQuotaSize, user.UsedQuotaSize) user.QuotaFiles = 100 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, err = httpd.UpdateQuotaUsage(u, "add", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(u, "add", http.StatusOK) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2*usedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, 2*usedQuotaSize, user.UsedQuotaSize) u.UsedQuotaFiles = -1 - _, err = httpd.UpdateQuotaUsage(u, "", http.StatusBadRequest) + _, err = httpdtest.UpdateQuotaUsage(u, "", http.StatusBadRequest) assert.NoError(t, err) u.UsedQuotaFiles = usedQuotaFiles u.Username = u.Username + "1" - _, err = httpd.UpdateQuotaUsage(u, "", http.StatusNotFound) + _, err = httpdtest.UpdateQuotaUsage(u, "", http.StatusNotFound) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -907,10 +1006,10 @@ func TestUserFolderMapping(t *testing.T) { QuotaSize: -1, QuotaFiles: -1, }) - user1, _, err := httpd.AddUser(u1, http.StatusOK) + user1, _, err := httpdtest.AddUser(u1, http.StatusCreated) assert.NoError(t, err) // virtual folder must be auto created - folders, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folders, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -937,16 +1036,16 @@ func TestUserFolderMapping(t *testing.T) { QuotaSize: -1, QuotaFiles: -1, }) - user2, _, err := httpd.AddUser(u2, http.StatusOK) + user2, _, err := httpdtest.AddUser(u2, http.StatusCreated) assert.NoError(t, err) - folders, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] assert.Len(t, folder.Users, 1) assert.Contains(t, folder.Users, user2.Username) } - folders, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -966,9 +1065,9 @@ func TestUserFolderMapping(t *testing.T) { QuotaSize: 0, QuotaFiles: 0, }) - user2, _, err = httpd.UpdateUser(user2, http.StatusOK, "") + user2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, "") assert.NoError(t, err) - folders, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -977,7 +1076,7 @@ func TestUserFolderMapping(t *testing.T) { assert.Equal(t, 0, folder.UsedQuotaFiles) assert.Equal(t, int64(0), folder.UsedQuotaSize) } - folders, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -991,9 +1090,9 @@ func TestUserFolderMapping(t *testing.T) { }, VirtualPath: "/vdir1", }) - user2, _, err = httpd.UpdateUser(user2, http.StatusOK, "") + user2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, "") assert.NoError(t, err) - folders, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -1001,31 +1100,31 @@ func TestUserFolderMapping(t *testing.T) { assert.Contains(t, folder.Users, user2.Username) } // removing virtual folders should clear relations on both side - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) - user2, _, err = httpd.GetUserByID(user2.ID, http.StatusOK) + user2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK) assert.NoError(t, err) if assert.Len(t, user2.VirtualFolders, 1) { folder := user2.VirtualFolders[0] assert.Equal(t, mappedPath1, folder.MappedPath) } - user1, _, err = httpd.GetUserByID(user1.ID, http.StatusOK) + user1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK) assert.NoError(t, err) if assert.Len(t, user2.VirtualFolders, 1) { folder := user2.VirtualFolders[0] assert.Equal(t, mappedPath1, folder.MappedPath) } - folders, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] assert.Len(t, folder.Users, 2) } // removing a user should clear virtual folder mapping - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) - folders, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -1033,17 +1132,17 @@ func TestUserFolderMapping(t *testing.T) { assert.Contains(t, folder.Users, user2.Username) } // removing a folder should clear mapping on the user side too - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - user2, _, err = httpd.GetUserByID(user2.ID, http.StatusOK) + user2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK) assert.NoError(t, err) assert.Len(t, user2.VirtualFolders, 0) - _, err = httpd.RemoveUser(user2, http.StatusOK) + _, err = httpdtest.RemoveUser(user2, http.StatusOK) assert.NoError(t, err) } func TestUserS3Config(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.S3FilesystemProvider user.FsConfig.S3Config.Bucket = "test" //nolint:goconst @@ -1052,22 +1151,22 @@ func TestUserS3Config(t *testing.T) { user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret") user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000" user.FsConfig.S3Config.UploadPartSize = 8 - user, body, err := httpd.UpdateUser(user, http.StatusOK, "") + user, body, err := httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(body)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) assert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.GetPayload()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Access-Secret", "", "") user.FsConfig.S3Config.AccessSecret = secret - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain) - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) initialSecretPayload := user.FsConfig.S3Config.AccessSecret.GetPayload() assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) @@ -1081,7 +1180,7 @@ func TestUserS3Config(t *testing.T) { user.FsConfig.S3Config.Endpoint = "http://localhost:9000" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" //nolint:goconst user.FsConfig.S3Config.UploadConcurrency = 5 - user, bb, err := httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) assert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.GetPayload()) @@ -1097,23 +1196,23 @@ func TestUserS3Config(t *testing.T) { user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" user.FsConfig.S3Config.UploadPartSize = 6 user.FsConfig.S3Config.UploadConcurrency = 4 - user, body, err = httpd.UpdateUser(user, http.StatusOK, "") + user, body, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(body)) assert.True(t, user.FsConfig.S3Config.AccessSecret.IsEmpty()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 // shared credential test for add instead of update - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) assert.True(t, user.FsConfig.S3Config.AccessSecret.IsEmpty()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUserGCSConfig(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(credentialsPath) assert.NoError(t, err) @@ -1122,7 +1221,7 @@ func TestUserGCSConfig(t *testing.T) { user.FsConfig.Provider = dataprovider.GCSFilesystemProvider user.FsConfig.GCSConfig.Bucket = "test" user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") //nolint:goconst - user, bb, err := httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) credentialFile := filepath.Join(credentialsPath, fmt.Sprintf("%v_gcs_credentials.json", user.Username)) assert.FileExists(t, credentialFile) @@ -1135,7 +1234,7 @@ func TestUserGCSConfig(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "fake credentials", secret.GetPayload()) user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake encrypted credentials", "", "") - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.FileExists(t, credentialFile) creds, err = ioutil.ReadFile(credentialFile) @@ -1146,15 +1245,15 @@ func TestUserGCSConfig(t *testing.T) { err = secret.Decrypt() assert.NoError(t, err) assert.Equal(t, "fake credentials", secret.GetPayload()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake credentials", "", "") - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain) - user, body, err := httpd.AddUser(user, http.StatusOK) + user, body, err := httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err, string(body)) err = os.RemoveAll(credentialsPath) assert.NoError(t, err) @@ -1162,7 +1261,7 @@ func TestUserGCSConfig(t *testing.T) { assert.NoError(t, err) user.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() user.FsConfig.GCSConfig.AutomaticCredentials = 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.NoFileExists(t, credentialFile) user.FsConfig.GCSConfig = vfs.GCSFsConfig{} @@ -1173,21 +1272,21 @@ func TestUserGCSConfig(t *testing.T) { user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("secret") user.FsConfig.S3Config.Endpoint = "http://localhost:9000" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) user.FsConfig.S3Config = vfs.S3FsConfig{} user.FsConfig.Provider = dataprovider.GCSFilesystemProvider user.FsConfig.GCSConfig.Bucket = "test1" user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUserAzureBlobConfig(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.Container = "test" @@ -1195,7 +1294,7 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") user.FsConfig.AzBlobConfig.Endpoint = "http://127.0.0.1:9000" user.FsConfig.AzBlobConfig.UploadPartSize = 8 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) initialPayload := user.FsConfig.AzBlobConfig.AccountKey.GetPayload() assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) @@ -1205,23 +1304,23 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusSecretBox) user.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData("data") user.FsConfig.AzBlobConfig.AccountKey.SetKey("fake key") - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.GetPayload()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Account-Key", "", "") user.FsConfig.AzBlobConfig.AccountKey = secret - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key-Test") - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) initialPayload = user.FsConfig.AzBlobConfig.AccountKey.GetPayload() assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) @@ -1233,7 +1332,7 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.AzBlobConfig.Endpoint = "http://localhost:9001" user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir" user.FsConfig.AzBlobConfig.UploadConcurrency = 5 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.NotEmpty(t, initialPayload) @@ -1248,27 +1347,27 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() user.FsConfig.AzBlobConfig.UploadPartSize = 6 user.FsConfig.AzBlobConfig.UploadConcurrency = 4 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.True(t, user.FsConfig.AzBlobConfig.AccountKey.IsEmpty()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 // sas test for add instead of update - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) assert.True(t, user.FsConfig.AzBlobConfig.AccountKey.IsEmpty()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUserCryptFs(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.CryptedFilesystemProvider user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypt passphrase") - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) initialPayload := user.FsConfig.CryptConfig.Passphrase.GetPayload() assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus()) @@ -1278,23 +1377,23 @@ func TestUserCryptFs(t *testing.T) { user.FsConfig.CryptConfig.Passphrase.SetStatus(kms.SecretStatusSecretBox) user.FsConfig.CryptConfig.Passphrase.SetAdditionalData("data") user.FsConfig.CryptConfig.Passphrase.SetKey("fake pass key") - user, bb, err := httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus()) assert.Equal(t, initialPayload, user.FsConfig.CryptConfig.Passphrase.GetPayload()) assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData()) assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 secret := kms.NewSecret(kms.SecretStatusSecretBox, "invalid encrypted payload", "", "") user.FsConfig.CryptConfig.Passphrase = secret - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("passphrase test") - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) initialPayload = user.FsConfig.CryptConfig.Passphrase.GetPayload() assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus()) @@ -1303,7 +1402,7 @@ func TestUserCryptFs(t *testing.T) { assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey()) user.FsConfig.Provider = dataprovider.CryptedFilesystemProvider user.FsConfig.CryptConfig.Passphrase.SetKey("pass") - user, bb, err = httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus()) assert.NotEmpty(t, initialPayload) @@ -1311,12 +1410,12 @@ func TestUserCryptFs(t *testing.T) { assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData()) assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUserSFTPFs(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.SFTPFilesystemProvider user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:2022" @@ -1324,7 +1423,7 @@ func TestUserSFTPFs(t *testing.T) { user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp_pwd") user.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey) user.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Equal(t, "/", user.FsConfig.SFTPConfig.Prefix) initialPwdPayload := user.FsConfig.SFTPConfig.Password.GetPayload() @@ -1343,7 +1442,7 @@ func TestUserSFTPFs(t *testing.T) { user.FsConfig.SFTPConfig.PrivateKey.SetStatus(kms.SecretStatusSecretBox) user.FsConfig.SFTPConfig.PrivateKey.SetAdditionalData("adata") user.FsConfig.SFTPConfig.PrivateKey.SetKey("fake key") - user, bb, err := httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.Password.GetStatus()) assert.Equal(t, initialPwdPayload, user.FsConfig.SFTPConfig.Password.GetPayload()) @@ -1354,21 +1453,21 @@ func TestUserSFTPFs(t *testing.T) { assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 secret := kms.NewSecret(kms.SecretStatusSecretBox, "invalid encrypted payload", "", "") user.FsConfig.SFTPConfig.Password = secret - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.SFTPConfig.Password = kms.NewEmptySecret() user.FsConfig.SFTPConfig.PrivateKey = secret - _, _, err = httpd.AddUser(user, http.StatusOK) + _, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.Error(t, err) user.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey) - user, _, err = httpd.AddUser(user, http.StatusOK) + user, _, err = httpdtest.AddUser(user, http.StatusCreated) assert.NoError(t, err) initialPkeyPayload = user.FsConfig.SFTPConfig.PrivateKey.GetPayload() assert.Empty(t, user.FsConfig.SFTPConfig.Password.GetStatus()) @@ -1378,7 +1477,7 @@ func TestUserSFTPFs(t *testing.T) { assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey()) user.FsConfig.Provider = dataprovider.SFTPFilesystemProvider user.FsConfig.SFTPConfig.PrivateKey.SetKey("k") - user, bb, err = httpd.UpdateUser(user, http.StatusOK, "") + user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(bb)) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.PrivateKey.GetStatus()) assert.NotEmpty(t, initialPkeyPayload) @@ -1386,7 +1485,7 @@ func TestUserSFTPFs(t *testing.T) { assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -1397,7 +1496,7 @@ func TestUserHiddenFields(t *testing.T) { assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.PreferDatabaseCredentials = true - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) // sensitive data must be hidden but not deleted from the dataprovider @@ -1409,7 +1508,7 @@ func TestUserHiddenFields(t *testing.T) { u1.FsConfig.S3Config.Region = "us-east-1" u1.FsConfig.S3Config.AccessKey = "S3-Access-Key" u1.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("S3-Access-Secret") - user1, _, err := httpd.AddUser(u1, http.StatusOK) + user1, _, err := httpdtest.AddUser(u1, http.StatusCreated) assert.NoError(t, err) u2 := getTestUser() @@ -1417,7 +1516,7 @@ func TestUserHiddenFields(t *testing.T) { u2.FsConfig.Provider = dataprovider.GCSFilesystemProvider u2.FsConfig.GCSConfig.Bucket = "test" u2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") - user2, _, err := httpd.AddUser(u2, http.StatusOK) + user2, _, err := httpdtest.AddUser(u2, http.StatusCreated) assert.NoError(t, err) u3 := getTestUser() @@ -1426,14 +1525,14 @@ func TestUserHiddenFields(t *testing.T) { u3.FsConfig.AzBlobConfig.Container = "test" u3.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name" u3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") - user3, _, err := httpd.AddUser(u3, http.StatusOK) + user3, _, err := httpdtest.AddUser(u3, http.StatusCreated) assert.NoError(t, err) u4 := getTestUser() u4.Username = usernames[3] u4.FsConfig.Provider = dataprovider.CryptedFilesystemProvider u4.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("test passphrase") - user4, _, err := httpd.AddUser(u4, http.StatusOK) + user4, _, err := httpdtest.AddUser(u4, http.StatusCreated) assert.NoError(t, err) u5 := getTestUser() @@ -1445,21 +1544,18 @@ func TestUserHiddenFields(t *testing.T) { u5.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey) u5.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint} u5.FsConfig.SFTPConfig.Prefix = "/prefix" - user5, _, err := httpd.AddUser(u5, http.StatusOK) + user5, _, err := httpdtest.AddUser(u5, http.StatusCreated) assert.NoError(t, err) - users, _, err := httpd.GetUsers(0, 0, "", http.StatusOK) + users, _, err := httpdtest.GetUsers(0, 0, http.StatusOK) assert.NoError(t, err) assert.GreaterOrEqual(t, len(users), 5) for _, username := range usernames { - users, _, err = httpd.GetUsers(0, 0, username, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(username, http.StatusOK) assert.NoError(t, err) - if assert.Len(t, users, 1) { - user := users[0] - assert.Empty(t, user.Password) - } + assert.Empty(t, user.Password) } - user1, _, err = httpd.GetUserByID(user1.ID, http.StatusOK) + user1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user1.Password) assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) @@ -1467,7 +1563,7 @@ func TestUserHiddenFields(t *testing.T) { assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus()) assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload()) - user2, _, err = httpd.GetUserByID(user2.ID, http.StatusOK) + user2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user2.Password) assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) @@ -1475,7 +1571,7 @@ func TestUserHiddenFields(t *testing.T) { assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus()) assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload()) - user3, _, err = httpd.GetUserByID(user3.ID, http.StatusOK) + user3, _, err = httpdtest.GetUserByUsername(user3.Username, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user3.Password) assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) @@ -1483,7 +1579,7 @@ func TestUserHiddenFields(t *testing.T) { assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload()) - user4, _, err = httpd.GetUserByID(user4.ID, http.StatusOK) + user4, _, err = httpdtest.GetUserByUsername(user4.Username, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user4.Password) assert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey()) @@ -1491,7 +1587,7 @@ func TestUserHiddenFields(t *testing.T) { assert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetStatus()) assert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetPayload()) - user5, _, err = httpd.GetUserByID(user5.ID, http.StatusOK) + user5, _, err = httpdtest.GetUserByUsername(user5.Username, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user5.Password) assert.Empty(t, user5.FsConfig.SFTPConfig.Password.GetKey()) @@ -1505,7 +1601,7 @@ func TestUserHiddenFields(t *testing.T) { assert.Equal(t, "/prefix", user5.FsConfig.SFTPConfig.Prefix) // finally check that we have all the data inside the data provider - user1, err = dataprovider.GetUserByID(user1.ID) + user1, err = dataprovider.UserExists(user1.Username) assert.NoError(t, err) assert.NotEmpty(t, user1.Password) assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) @@ -1519,7 +1615,7 @@ func TestUserHiddenFields(t *testing.T) { assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData()) - user2, err = dataprovider.GetUserByID(user2.ID) + user2, err = dataprovider.UserExists(user2.Username) assert.NoError(t, err) assert.NotEmpty(t, user2.Password) assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) @@ -1533,7 +1629,7 @@ func TestUserHiddenFields(t *testing.T) { assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData()) - user3, err = dataprovider.GetUserByID(user3.ID) + user3, err = dataprovider.UserExists(user3.Username) assert.NoError(t, err) assert.NotEmpty(t, user3.Password) assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) @@ -1547,7 +1643,7 @@ func TestUserHiddenFields(t *testing.T) { assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) - user4, err = dataprovider.GetUserByID(user4.ID) + user4, err = dataprovider.UserExists(user4.Username) assert.NoError(t, err) assert.NotEmpty(t, user4.Password) assert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey()) @@ -1561,7 +1657,7 @@ func TestUserHiddenFields(t *testing.T) { assert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey()) assert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetAdditionalData()) - user5, err = dataprovider.GetUserByID(user5.ID) + user5, err = dataprovider.UserExists(user5.Username) assert.NoError(t, err) assert.NotEmpty(t, user5.Password) assert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetKey()) @@ -1585,15 +1681,15 @@ func TestUserHiddenFields(t *testing.T) { assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey()) assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData()) - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user2, http.StatusOK) + _, err = httpdtest.RemoveUser(user2, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user3, http.StatusOK) + _, err = httpdtest.RemoveUser(user3, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user4, http.StatusOK) + _, err = httpdtest.RemoveUser(user4, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user5, http.StatusOK) + _, err = httpdtest.RemoveUser(user5, http.StatusOK) assert.NoError(t, err) err = dataprovider.Close() @@ -1604,7 +1700,7 @@ func TestUserHiddenFields(t *testing.T) { providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } @@ -1742,115 +1838,115 @@ func TestSecretObjectCompatibility(t *testing.T) { } func TestUpdateUserNoCredentials(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.Password = "" user.PublicKeys = []string{} // password and public key will be omitted from json serialization if empty and so they will remain unchanged // and no validation error will be raised - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUpdateUserEmptyHomeDir(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.HomeDir = "" - _, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUpdateUserInvalidHomeDir(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) user.HomeDir = "relative_path" - _, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "") assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestUpdateNonExistentUser(t *testing.T) { - _, _, err := httpd.UpdateUser(getTestUser(), http.StatusNotFound, "") + _, _, err := httpdtest.UpdateUser(getTestUser(), http.StatusNotFound, "") assert.NoError(t, err) } func TestGetNonExistentUser(t *testing.T) { - _, _, err := httpd.GetUserByID(0, http.StatusNotFound) + _, _, err := httpdtest.GetUserByUsername("na", http.StatusNotFound) assert.NoError(t, err) } func TestDeleteNonExistentUser(t *testing.T) { - _, err := httpd.RemoveUser(getTestUser(), http.StatusNotFound) + _, err := httpdtest.RemoveUser(getTestUser(), http.StatusNotFound) assert.NoError(t, err) } func TestAddDuplicateUser(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) - _, _, err = httpd.AddUser(getTestUser(), http.StatusInternalServerError) + _, _, err = httpdtest.AddUser(getTestUser(), http.StatusInternalServerError) assert.NoError(t, err) - _, _, err = httpd.AddUser(getTestUser(), http.StatusOK) + _, _, err = httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.Error(t, err, "adding a duplicate user must fail") - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestGetUsers(t *testing.T) { - user1, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user1, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) u := getTestUser() u.Username = defaultUsername + "1" - user2, _, err := httpd.AddUser(u, http.StatusOK) + user2, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) - users, _, err := httpd.GetUsers(0, 0, "", http.StatusOK) + users, _, err := httpdtest.GetUsers(0, 0, http.StatusOK) assert.NoError(t, err) assert.GreaterOrEqual(t, len(users), 2) - users, _, err = httpd.GetUsers(1, 0, "", http.StatusOK) + users, _, err = httpdtest.GetUsers(1, 0, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, len(users)) - users, _, err = httpd.GetUsers(1, 1, "", http.StatusOK) + users, _, err = httpdtest.GetUsers(1, 1, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, len(users)) - _, _, err = httpd.GetUsers(1, 1, "", http.StatusInternalServerError) + _, _, err = httpdtest.GetUsers(1, 1, http.StatusInternalServerError) assert.Error(t, err) - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user2, http.StatusOK) + _, err = httpdtest.RemoveUser(user2, http.StatusOK) assert.NoError(t, err) } func TestGetQuotaScans(t *testing.T) { - _, _, err := httpd.GetQuotaScans(http.StatusOK) + _, _, err := httpdtest.GetQuotaScans(http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.GetQuotaScans(http.StatusInternalServerError) + _, _, err = httpdtest.GetQuotaScans(http.StatusInternalServerError) assert.Error(t, err) - _, _, err = httpd.GetFoldersQuotaScans(http.StatusOK) + _, _, err = httpdtest.GetFoldersQuotaScans(http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.GetFoldersQuotaScans(http.StatusInternalServerError) + _, _, err = httpdtest.GetFoldersQuotaScans(http.StatusInternalServerError) assert.Error(t, err) } func TestStartQuotaScan(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartQuotaScan(user, http.StatusAccepted) + _, err = httpdtest.StartQuotaScan(user, http.StatusAccepted) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) folder := vfs.BaseVirtualFolder{ MappedPath: filepath.Join(os.TempDir(), "folder"), } - _, _, err = httpd.AddFolder(folder, http.StatusOK) + _, _, err = httpdtest.AddFolder(folder, http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartFolderQuotaScan(folder, http.StatusAccepted) + _, err = httpdtest.StartFolderQuotaScan(folder, http.StatusAccepted) assert.NoError(t, err) for { - quotaScan, _, err := httpd.GetFoldersQuotaScans(http.StatusOK) + quotaScan, _, err := httpdtest.GetFoldersQuotaScans(http.StatusOK) if !assert.NoError(t, err, "Error getting active scans") { break } @@ -1859,7 +1955,7 @@ func TestStartQuotaScan(t *testing.T) { } time.Sleep(100 * time.Millisecond) } - _, err = httpd.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) } @@ -1871,25 +1967,25 @@ func TestUpdateFolderQuotaUsage(t *testing.T) { usedQuotaSize := int64(65535) f.UsedQuotaFiles = usedQuotaFiles f.UsedQuotaSize = usedQuotaSize - folder, _, err := httpd.AddFolder(f, http.StatusOK) + folder, _, err := httpdtest.AddFolder(f, http.StatusCreated) if assert.NoError(t, err) { assert.Equal(t, usedQuotaFiles, folder.UsedQuotaFiles) assert.Equal(t, usedQuotaSize, folder.UsedQuotaSize) } - _, err = httpd.UpdateFolderQuotaUsage(folder, "invalid mode", http.StatusBadRequest) + _, err = httpdtest.UpdateFolderQuotaUsage(folder, "invalid mode", http.StatusBadRequest) assert.NoError(t, err) - _, err = httpd.UpdateFolderQuotaUsage(f, "reset", http.StatusOK) + _, err = httpdtest.UpdateFolderQuotaUsage(f, "reset", http.StatusOK) assert.NoError(t, err) - folders, _, err := httpd.GetFolders(0, 0, f.MappedPath, http.StatusOK) + folders, _, err := httpdtest.GetFolders(0, 0, f.MappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder = folders[0] assert.Equal(t, usedQuotaFiles, folder.UsedQuotaFiles) assert.Equal(t, usedQuotaSize, folder.UsedQuotaSize) } - _, err = httpd.UpdateFolderQuotaUsage(f, "add", http.StatusOK) + _, err = httpdtest.UpdateFolderQuotaUsage(f, "add", http.StatusOK) assert.NoError(t, err) - folders, _, err = httpd.GetFolders(0, 0, f.MappedPath, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, f.MappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder = folders[0] @@ -1897,39 +1993,39 @@ func TestUpdateFolderQuotaUsage(t *testing.T) { assert.Equal(t, 2*usedQuotaSize, folder.UsedQuotaSize) } f.UsedQuotaSize = -1 - _, err = httpd.UpdateFolderQuotaUsage(f, "", http.StatusBadRequest) + _, err = httpdtest.UpdateFolderQuotaUsage(f, "", http.StatusBadRequest) assert.NoError(t, err) f.UsedQuotaSize = usedQuotaSize f.MappedPath = f.MappedPath + "1" - _, err = httpd.UpdateFolderQuotaUsage(f, "", http.StatusNotFound) + _, err = httpdtest.UpdateFolderQuotaUsage(f, "", http.StatusNotFound) assert.NoError(t, err) - _, err = httpd.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) } func TestGetVersion(t *testing.T) { - _, _, err := httpd.GetVersion(http.StatusOK) + _, _, err := httpdtest.GetVersion(http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.GetVersion(http.StatusInternalServerError) + _, _, err = httpdtest.GetVersion(http.StatusInternalServerError) assert.Error(t, err, "get version request must succeed, we requested to check a wrong status code") } func TestGetStatus(t *testing.T) { - _, _, err := httpd.GetStatus(http.StatusOK) + _, _, err := httpdtest.GetStatus(http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.GetStatus(http.StatusBadRequest) + _, _, err = httpdtest.GetStatus(http.StatusBadRequest) assert.Error(t, err, "get provider status request must succeed, we requested to check a wrong status code") } func TestGetConnections(t *testing.T) { - _, _, err := httpd.GetConnections(http.StatusOK) + _, _, err := httpdtest.GetConnections(http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.GetConnections(http.StatusInternalServerError) + _, _, err = httpdtest.GetConnections(http.StatusInternalServerError) assert.Error(t, err, "get sftp connections request must succeed, we requested to check a wrong status code") } func TestCloseActiveConnection(t *testing.T) { - _, err := httpd.CloseConnection("non_existent_id", http.StatusNotFound) + _, err := httpdtest.CloseConnection("non_existent_id", http.StatusNotFound) assert.NoError(t, err) user := getTestUser() c := common.NewBaseConnection("connID", common.ProtocolSFTP, user, nil) @@ -1937,13 +2033,13 @@ func TestCloseActiveConnection(t *testing.T) { BaseConnection: c, } common.Connections.Add(fakeConn) - _, err = httpd.CloseConnection(c.GetID(), http.StatusOK) + _, err = httpdtest.CloseConnection(c.GetID(), http.StatusOK) assert.NoError(t, err) assert.Len(t, common.Connections.GetStats(), 0) } func TestCloseConnectionAfterUserUpdateDelete(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) c := common.NewBaseConnection("connID", common.ProtocolFTP, user, nil) fakeConn := &fakeConnection{ @@ -1955,17 +2051,17 @@ func TestCloseConnectionAfterUserUpdateDelete(t *testing.T) { BaseConnection: c1, } common.Connections.Add(fakeConn1) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "0") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "0") assert.NoError(t, err) assert.Len(t, common.Connections.GetStats(), 2) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "1") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "1") assert.NoError(t, err) assert.Len(t, common.Connections.GetStats(), 0) common.Connections.Add(fakeConn) common.Connections.Add(fakeConn1) assert.Len(t, common.Connections.GetStats(), 2) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) assert.Len(t, common.Connections.GetStats(), 0) } @@ -1977,16 +2073,16 @@ func TestUserBaseDir(t *testing.T) { assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.UsersBaseDir = homeBasePath - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) u := getTestUser() u.HomeDir = "" - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) if assert.Error(t, err) { assert.EqualError(t, err, "HomeDir mismatch") } assert.Equal(t, filepath.Join(providerConf.UsersBaseDir, u.Username), user.HomeDir) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) @@ -1996,7 +2092,7 @@ func TestUserBaseDir(t *testing.T) { providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } @@ -2007,28 +2103,28 @@ func TestQuotaTrackingDisabled(t *testing.T) { assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.TrackQuota = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) // user quota scan must fail - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartQuotaScan(user, http.StatusForbidden) + _, err = httpdtest.StartQuotaScan(user, http.StatusForbidden) assert.NoError(t, err) - _, err = httpd.UpdateQuotaUsage(user, "", http.StatusForbidden) + _, err = httpdtest.UpdateQuotaUsage(user, "", http.StatusForbidden) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) // folder quota scan must fail folder := vfs.BaseVirtualFolder{ MappedPath: filepath.Clean(os.TempDir()), } - folder, _, err = httpd.AddFolder(folder, http.StatusOK) + folder, resp, err := httpdtest.AddFolder(folder, http.StatusCreated) + assert.NoError(t, err, string(resp)) + _, err = httpdtest.StartFolderQuotaScan(folder, http.StatusForbidden) assert.NoError(t, err) - _, err = httpd.StartFolderQuotaScan(folder, http.StatusForbidden) + _, err = httpdtest.UpdateFolderQuotaUsage(folder, "", http.StatusForbidden) assert.NoError(t, err) - _, err = httpd.UpdateFolderQuotaUsage(folder, "", http.StatusForbidden) - assert.NoError(t, err) - _, err = httpd.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) err = dataprovider.Close() @@ -2039,30 +2135,37 @@ func TestQuotaTrackingDisabled(t *testing.T) { providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } func TestProviderErrors(t *testing.T) { - err := dataprovider.Close() + token, _, err := httpdtest.GetToken(defaultTokenAuthUser, defaultTokenAuthPass) assert.NoError(t, err) - _, _, err = httpd.GetUserByID(0, http.StatusInternalServerError) + testServerToken, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) assert.NoError(t, err) - _, _, err = httpd.GetUsers(1, 0, defaultUsername, http.StatusInternalServerError) + httpdtest.SetJWTToken(token) + err = dataprovider.Close() assert.NoError(t, err) - _, _, err = httpd.UpdateUser(dataprovider.User{}, http.StatusInternalServerError, "") + _, _, err = httpdtest.GetUserByUsername("na", http.StatusInternalServerError) assert.NoError(t, err) - _, err = httpd.RemoveUser(dataprovider.User{}, http.StatusInternalServerError) + _, _, err = httpdtest.GetUsers(1, 0, http.StatusInternalServerError) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: "apath"}, http.StatusInternalServerError) + _, _, err = httpdtest.GetAdmins(1, 0, http.StatusInternalServerError) assert.NoError(t, err) - status, _, err := httpd.GetStatus(http.StatusOK) + _, _, err = httpdtest.UpdateUser(dataprovider.User{Username: "auser"}, http.StatusInternalServerError, "") + assert.NoError(t, err) + _, err = httpdtest.RemoveUser(dataprovider.User{Username: "auser"}, http.StatusInternalServerError) + assert.NoError(t, err) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: "apath"}, http.StatusInternalServerError) + assert.NoError(t, err) + status, _, err := httpdtest.GetStatus(http.StatusOK) if assert.NoError(t, err) { assert.False(t, status.DataProvider.IsActive) } - _, _, err = httpd.Dumpdata("backup.json", "", http.StatusInternalServerError) + _, _, err = httpdtest.Dumpdata("backup.json", "", http.StatusInternalServerError) assert.NoError(t, err) - _, _, err = httpd.GetFolders(0, 0, "", http.StatusInternalServerError) + _, _, err = httpdtest.GetFolders(0, 0, "", http.StatusInternalServerError) assert.NoError(t, err) user := getTestUser() user.ID = 1 @@ -2073,88 +2176,99 @@ func TestProviderErrors(t *testing.T) { backupFilePath := filepath.Join(backupsPath, "backup.json") err = ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) + _, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) assert.NoError(t, err) - backupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{MappedPath: os.TempDir()}) + backupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{MappedPath: filepath.Clean(os.TempDir())}) backupContent, err = json.Marshal(backupData) assert.NoError(t, err) err = ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) + _, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) + assert.NoError(t, err) + backupData.Users = nil + backupData.Folders = nil + backupData.Admins = append(backupData.Admins, getTestAdmin()) + backupContent, err = json.Marshal(backupData) + assert.NoError(t, err) + err = ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm) + assert.NoError(t, err) + _, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) assert.NoError(t, err) err = os.Remove(backupFilePath) assert.NoError(t, err) - req, err := http.NewRequest(http.MethodGet, webUserPath+"?cloneFromId=1234", nil) + req, err := http.NewRequest(http.MethodGet, webUserPath+"?cloneFrom=user", nil) assert.NoError(t, err) + setJWTCookieForReq(req, testServerToken) rr := executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) + httpdtest.SetJWTToken("") } func TestFolders(t *testing.T) { folder := vfs.BaseVirtualFolder{ MappedPath: "relative path", } - _, _, err := httpd.AddFolder(folder, http.StatusBadRequest) + _, _, err := httpdtest.AddFolder(folder, http.StatusBadRequest) assert.NoError(t, err) folder.MappedPath = filepath.Clean(os.TempDir()) - folder1, _, err := httpd.AddFolder(folder, http.StatusOK) - assert.NoError(t, err) + folder1, resp, err := httpdtest.AddFolder(folder, http.StatusCreated) + assert.NoError(t, err, string(resp)) assert.Equal(t, folder.MappedPath, folder1.MappedPath) assert.Equal(t, 0, folder1.UsedQuotaFiles) assert.Equal(t, int64(0), folder1.UsedQuotaSize) assert.Equal(t, int64(0), folder1.LastQuotaUpdate) // adding a duplicate folder must fail - _, _, err = httpd.AddFolder(folder, http.StatusOK) + _, _, err = httpdtest.AddFolder(folder, http.StatusCreated) assert.Error(t, err) folder.MappedPath = filepath.Join(os.TempDir(), "vfolder") folder.UsedQuotaFiles = 1 folder.UsedQuotaSize = 345 folder.LastQuotaUpdate = 10 - folder2, _, err := httpd.AddFolder(folder, http.StatusOK) + folder2, _, err := httpdtest.AddFolder(folder, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, 1, folder2.UsedQuotaFiles) assert.Equal(t, int64(345), folder2.UsedQuotaSize) assert.Equal(t, int64(10), folder2.LastQuotaUpdate) - folders, _, err := httpd.GetFolders(0, 0, "", http.StatusOK) + folders, _, err := httpdtest.GetFolders(0, 0, "", http.StatusOK) assert.NoError(t, err) numResults := len(folders) assert.GreaterOrEqual(t, numResults, 2) - folders, _, err = httpd.GetFolders(0, 1, "", http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 1, "", http.StatusOK) assert.NoError(t, err) assert.Len(t, folders, numResults-1) - folders, _, err = httpd.GetFolders(1, 0, "", http.StatusOK) + folders, _, err = httpdtest.GetFolders(1, 0, "", http.StatusOK) assert.NoError(t, err) assert.Len(t, folders, 1) - folders, _, err = httpd.GetFolders(0, 0, folder1.MappedPath, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, folder1.MappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { f := folders[0] assert.Equal(t, folder1.MappedPath, f.MappedPath) } - folders, _, err = httpd.GetFolders(0, 0, folder2.MappedPath, http.StatusOK) + folders, _, err = httpdtest.GetFolders(0, 0, folder2.MappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { f := folders[0] assert.Equal(t, folder2.MappedPath, f.MappedPath) } - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{}, http.StatusBadRequest) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{}, http.StatusBadRequest) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{ + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{ MappedPath: "invalid", }, http.StatusNotFound) assert.NoError(t, err) - _, err = httpd.RemoveFolder(folder1, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder1, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(folder2, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder2, http.StatusOK) assert.NoError(t, err) } @@ -2164,27 +2278,27 @@ func TestDumpdata(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - _, _, err = httpd.Dumpdata("", "", http.StatusBadRequest) + _, _, err = httpdtest.Dumpdata("", "", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Dumpdata(filepath.Join(backupsPath, "backup.json"), "", http.StatusBadRequest) + _, _, err = httpdtest.Dumpdata(filepath.Join(backupsPath, "backup.json"), "", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Dumpdata("../backup.json", "", http.StatusBadRequest) + _, _, err = httpdtest.Dumpdata("../backup.json", "", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Dumpdata("backup.json", "0", http.StatusOK) + _, _, err = httpdtest.Dumpdata("backup.json", "0", http.StatusOK) assert.NoError(t, err) - _, _, err = httpd.Dumpdata("backup.json", "1", http.StatusOK) + _, _, err = httpdtest.Dumpdata("backup.json", "1", http.StatusOK) assert.NoError(t, err) err = os.Remove(filepath.Join(backupsPath, "backup.json")) assert.NoError(t, err) if runtime.GOOS != "windows" { err = os.Chmod(backupsPath, 0001) assert.NoError(t, err) - _, _, err = httpd.Dumpdata("bck.json", "", http.StatusInternalServerError) + _, _, err = httpdtest.Dumpdata("bck.json", "", http.StatusInternalServerError) assert.NoError(t, err) // subdir cannot be created - _, _, err = httpd.Dumpdata(filepath.Join("subdir", "bck.json"), "", http.StatusInternalServerError) + _, _, err = httpdtest.Dumpdata(filepath.Join("subdir", "bck.json"), "", http.StatusInternalServerError) assert.NoError(t, err) err = os.Chmod(backupsPath, 0755) assert.NoError(t, err) @@ -2197,7 +2311,7 @@ func TestDumpdata(t *testing.T) { providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } @@ -2213,39 +2327,39 @@ func TestDefenderAPI(t *testing.T) { ip := "::1" - response, _, err := httpd.GetBanTime(ip, http.StatusOK) + response, _, err := httpdtest.GetBanTime(ip, http.StatusOK) require.NoError(t, err) banTime, ok := response["date_time"] require.True(t, ok) assert.Nil(t, banTime) - response, _, err = httpd.GetScore(ip, http.StatusOK) + response, _, err = httpdtest.GetScore(ip, http.StatusOK) require.NoError(t, err) score, ok := response["score"] require.True(t, ok) assert.Equal(t, float64(0), score) - err = httpd.UnbanIP(ip, http.StatusNotFound) + err = httpdtest.UnbanIP(ip, http.StatusNotFound) require.NoError(t, err) common.AddDefenderEvent(ip, common.HostEventNoLoginTried) - response, _, err = httpd.GetScore(ip, http.StatusOK) + response, _, err = httpdtest.GetScore(ip, http.StatusOK) require.NoError(t, err) score, ok = response["score"] require.True(t, ok) assert.Equal(t, float64(2), score) common.AddDefenderEvent(ip, common.HostEventNoLoginTried) - response, _, err = httpd.GetBanTime(ip, http.StatusOK) + response, _, err = httpdtest.GetBanTime(ip, http.StatusOK) require.NoError(t, err) banTime, ok = response["date_time"] require.True(t, ok) assert.NotNil(t, banTime) - err = httpd.UnbanIP(ip, http.StatusOK) + err = httpdtest.UnbanIP(ip, http.StatusOK) require.NoError(t, err) - err = httpd.UnbanIP(ip, http.StatusNotFound) + err = httpdtest.UnbanIP(ip, http.StatusNotFound) require.NoError(t, err) err = common.Initialize(oldConfig) @@ -2253,16 +2367,16 @@ func TestDefenderAPI(t *testing.T) { } func TestDefenderAPIErrors(t *testing.T) { - _, _, err := httpd.GetBanTime("", http.StatusBadRequest) + _, _, err := httpdtest.GetBanTime("", http.StatusBadRequest) require.NoError(t, err) - _, _, err = httpd.GetBanTime("invalid", http.StatusBadRequest) + _, _, err = httpdtest.GetBanTime("invalid", http.StatusBadRequest) require.NoError(t, err) - _, _, err = httpd.GetScore("", http.StatusBadRequest) + _, _, err = httpdtest.GetScore("", http.StatusBadRequest) require.NoError(t, err) - err = httpd.UnbanIP("", http.StatusBadRequest) + err = httpdtest.UnbanIP("", http.StatusBadRequest) require.NoError(t, err) } @@ -2271,8 +2385,12 @@ func TestLoaddata(t *testing.T) { user := getTestUser() user.ID = 1 user.Username = "test_user_restore" + admin := getTestAdmin() + admin.ID = 1 + admin.Username = "test_admin_restore" backupData := dataprovider.BackupData{} backupData.Users = append(backupData.Users, user) + backupData.Admins = append(backupData.Admins, admin) backupData.Folders = []vfs.BaseVirtualFolder{ { MappedPath: mappedPath, @@ -2290,36 +2408,39 @@ func TestLoaddata(t *testing.T) { backupFilePath := filepath.Join(backupsPath, "backup.json") err = ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "a", "", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata(backupFilePath, "a", "", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "", "a", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata(backupFilePath, "", "a", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Loaddata("backup.json", "1", "", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata("backup.json", "1", "", http.StatusBadRequest) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath+"a", "1", "", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata(backupFilePath+"a", "1", "", http.StatusBadRequest) assert.NoError(t, err) if runtime.GOOS != "windows" { err = os.Chmod(backupFilePath, 0111) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "1", "", http.StatusInternalServerError) + _, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusInternalServerError) assert.NoError(t, err) err = os.Chmod(backupFilePath, 0644) assert.NoError(t, err) } - // add user and folder from backup - _, _, err = httpd.Loaddata(backupFilePath, "1", "", http.StatusOK) + // add user, folder, admin from backup + _, _, err = httpdtest.Loaddata(backupFilePath, "1", "", http.StatusOK) assert.NoError(t, err) - // update user from backup - _, _, err = httpd.Loaddata(backupFilePath, "2", "", http.StatusOK) + // update from backup + _, _, err = httpdtest.Loaddata(backupFilePath, "2", "", http.StatusOK) assert.NoError(t, err) - users, _, err := httpd.GetUsers(1, 0, user.Username, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) - if assert.Len(t, users, 1) { - user = users[0] - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - } - folders, _, err := httpd.GetFolders(1, 0, mappedPath, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) + + folders, _, err := httpdtest.GetFolders(1, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder := folders[0] @@ -2328,20 +2449,20 @@ func TestLoaddata(t *testing.T) { assert.Equal(t, 456, folder.UsedQuotaFiles) assert.Equal(t, int64(789), folder.LastQuotaUpdate) assert.Len(t, folder.Users, 0) - _, err = httpd.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) } err = os.Remove(backupFilePath) assert.NoError(t, err) err = createTestFile(backupFilePath, 10485761) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) assert.NoError(t, err) err = os.Remove(backupFilePath) assert.NoError(t, err) err = createTestFile(backupFilePath, 65535) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) + _, _, err = httpdtest.Loaddata(backupFilePath, "1", "0", http.StatusBadRequest) assert.NoError(t, err) err = os.Remove(backupFilePath) assert.NoError(t, err) @@ -2351,23 +2472,31 @@ func TestLoaddataMode(t *testing.T) { user := getTestUser() user.ID = 1 user.Username = "test_user_restore" + admin := getTestAdmin() + admin.ID = 1 + admin.Username = "test_admin_restore" backupData := dataprovider.BackupData{} backupData.Users = append(backupData.Users, user) + backupData.Admins = append(backupData.Admins, admin) backupContent, _ := json.Marshal(backupData) backupFilePath := filepath.Join(backupsPath, "backup.json") err := ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm) assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "0", "0", http.StatusOK) + _, _, err = httpdtest.Loaddata(backupFilePath, "0", "0", http.StatusOK) assert.NoError(t, err) - users, _, err := httpd.GetUsers(1, 0, user.Username, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user = users[0] oldUploadBandwidth := user.UploadBandwidth user.UploadBandwidth = oldUploadBandwidth + 128 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - _, _, err = httpd.Loaddata(backupFilePath, "0", "1", http.StatusOK) + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + oldInfo := admin.AdditionalInfo + admin.AdditionalInfo = "newInfo" + admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) + assert.NoError(t, err) + _, _, err = httpdtest.Loaddata(backupFilePath, "0", "1", http.StatusOK) assert.NoError(t, err) c := common.NewBaseConnection("connID", common.ProtocolFTP, user, nil) @@ -2376,21 +2505,23 @@ func TestLoaddataMode(t *testing.T) { } common.Connections.Add(fakeConn) assert.Len(t, common.Connections.GetStats(), 1) - users, _, err = httpd.GetUsers(1, 0, user.Username, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user = users[0] assert.NotEqual(t, oldUploadBandwidth, user.UploadBandwidth) - _, _, err = httpd.Loaddata(backupFilePath, "0", "2", http.StatusOK) + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + assert.NotEqual(t, oldInfo, admin.AdditionalInfo) + + _, _, err = httpdtest.Loaddata(backupFilePath, "0", "2", http.StatusOK) assert.NoError(t, err) // mode 2 will update the user and close the previous connection assert.Len(t, common.Connections.GetStats(), 0) - users, _, err = httpd.GetUsers(1, 0, user.Username, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user = users[0] assert.Equal(t, oldUploadBandwidth, user.UploadBandwidth) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) assert.NoError(t, err) err = os.Remove(backupFilePath) assert.NoError(t, err) @@ -2400,7 +2531,7 @@ func TestHTTPSConnection(t *testing.T) { client := &http.Client{ Timeout: 5 * time.Second, } - resp, err := client.Get("https://localhost:8443" + metricsPath) + resp, err := client.Get("https://localhost:8443" + healthzPath) if assert.Error(t, err) { if !strings.Contains(err.Error(), "certificate is not valid") && !strings.Contains(err.Error(), "certificate signed by unknown authority") { @@ -2414,28 +2545,35 @@ func TestHTTPSConnection(t *testing.T) { // test using mock http server func TestBasicUserHandlingMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) assert.NoError(t, err) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) - req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + req, err = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) user.MaxSessions = 10 user.UploadBandwidth = 128 user.Permissions["/"] = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload} userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, userPath+"/"+user.Username, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) - req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodGet, userPath+"/"+user.Username, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var updatedUser dataprovider.User err = render.DecodeJSON(rr.Body, &updatedUser) @@ -2444,83 +2582,258 @@ func TestBasicUserHandlingMock(t *testing.T) { assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth) assert.Equal(t, 1, len(updatedUser.Permissions["/"])) assert.True(t, utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions["/"])) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) -} - -func TestGetUserByIdInvalidParamsMock(t *testing.T) { - req, _ := http.NewRequest(http.MethodGet, userPath+"/0", nil) - rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"/a", nil) - rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestAddUserNoUsernameMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() user.Username = "" userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestAddUserInvalidHomeDirMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() user.HomeDir = "relative_path" userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestAddUserInvalidPermsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() user.Permissions["/"] = []string{} userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestAddFolderInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer([]byte("invalid json"))) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestUnbanInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, defenderUnban, bytes.NewBuffer([]byte("invalid json"))) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestAddUserInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer([]byte("invalid json"))) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) +} + +func TestAddAdminInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer([]byte("..."))) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) +} + +func TestAddAdminNoPasswordMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + admin := getTestAdmin() + admin.Password = "" + asJSON, err := json.Marshal(admin) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + assert.Contains(t, rr.Body.String(), "please set a password") +} + +func TestChangeAdminPwdInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer([]byte("{"))) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) +} + +func TestLoginInvalidPasswordMock(t *testing.T) { + _, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass+"1") + assert.Error(t, err) + // now a login with no credentials + req, _ := http.NewRequest(http.MethodGet, "/api/v2/token", nil) + rr := executeRequest(req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) +} + +func TestChangeAdminPwdMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + admin.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminDeleteUsers} + asJSON, err := json.Marshal(admin) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + + altToken, err := getJWTTokenFromTestServer(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + user := getTestUser() + userAsJSON := getUserAsJSON(t, user) + req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + + pwd := make(map[string]string) + pwd["current_password"] = altAdminPassword + pwd["new_password"] = defaultTokenAuthPass + asJSON, err = json.Marshal(pwd) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + _, err = getJWTTokenFromTestServer(altAdminUsername, altAdminPassword) + assert.Error(t, err) + + altToken, err = getJWTTokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) // current password does not match + + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) +} + +func TestUpdateAdminMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Permissions = []string{dataprovider.PermAdminManageAdmins} + asJSON, err := json.Marshal(admin) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, "abc"), bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer([]byte("no json"))) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + admin.Permissions = nil + asJSON, err = json.Marshal(admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + admin = getTestAdmin() + admin.Status = 0 + asJSON, err = json.Marshal(admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + admin.Status = 1 + admin.Permissions = []string{dataprovider.PermAdminAddUsers} + asJSON, err = json.Marshal(admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + + altToken, err := getJWTTokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + admin.Permissions = []string{dataprovider.PermAdminManageAdmins, dataprovider.PermAdminCloseConnections} + asJSON, err = json.Marshal(admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer(asJSON)) + setBearerForReq(req, altToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) } func TestUpdateUserMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) // permissions should not change if empty or nil permissions := user.Permissions user.Permissions = make(map[string][]string) userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, userPath+"/"+user.Username, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, userPath+"/"+user.Username, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var updatedUser dataprovider.User err = render.DecodeJSON(rr.Body, &updatedUser) assert.NoError(t, err) @@ -2533,12 +2846,15 @@ func TestUpdateUserMock(t *testing.T) { assert.Fail(t, "Permissions directories mismatch") } } - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestUpdateUserQuotaUsageMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) var user dataprovider.User u := getTestUser() usedQuotaFiles := 1 @@ -2547,82 +2863,98 @@ func TestUpdateUserQuotaUsageMock(t *testing.T) { u.UsedQuotaSize = usedQuotaSize userAsJSON := getUserAsJSON(t, u) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) assert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, usedQuotaSize, user.UsedQuotaSize) req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer([]byte("string"))) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) assert.True(t, common.QuotaScans.AddUserQuotaScan(user.Username)) req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusConflict, rr.Code) + checkResponseCode(t, http.StatusConflict, rr) assert.True(t, common.QuotaScans.RemoveUserQuotaScan(user.Username)) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestUserPermissionsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() user.Permissions = make(map[string][]string) user.Permissions["/somedir"] = []string{dataprovider.PermAny} userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) user.Permissions = make(map[string][]string) user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions[".."] = []string{dataprovider.PermAny} userAsJSON = getUserAsJSON(t, user) req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) user.Permissions = make(map[string][]string) user.Permissions["/"] = []string{dataprovider.PermAny} userAsJSON = getUserAsJSON(t, user) req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.Permissions["/somedir"] = []string{"invalid"} userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) delete(user.Permissions, "/somedir") user.Permissions["/somedir/.."] = []string{dataprovider.PermAny} userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) delete(user.Permissions, "/somedir/..") user.Permissions["not_abs_path"] = []string{dataprovider.PermAny} userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) delete(user.Permissions, "not_abs_path") user.Permissions["/somedir/../otherdir/"] = []string{dataprovider.PermListItems} userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var updatedUser dataprovider.User err = render.DecodeJSON(rr.Body, &updatedUser) assert.NoError(t, err) @@ -2632,111 +2964,195 @@ func TestUserPermissionsMock(t *testing.T) { } else { assert.Fail(t, "expected dir not found in permissions") } - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestUpdateUserInvalidJsonMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer([]byte("Invalid json"))) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer([]byte("Invalid json"))) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusBadRequest, rr) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestUpdateUserInvalidParamsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.HomeDir = "" userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) userID := user.ID user.ID = 0 userAsJSON = getUserAsJSON(t, user) - req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(userID, 10), bytes.NewBuffer(userAsJSON)) + req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) user.ID = userID req, _ = http.NewRequest(http.MethodPut, userPath+"/0", bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - req, _ = http.NewRequest(http.MethodPut, userPath+"/a", bytes.NewBuffer(userAsJSON)) + checkResponseCode(t, http.StatusNotFound, rr) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) +} + +func TestGetAdminsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + admin := getTestAdmin() + admin.Username = altAdminUsername + asJSON, err := json.Marshal(admin) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON)) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=510&offset=0&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) + var admins []dataprovider.Admin + err = render.DecodeJSON(rr.Body, &admins) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(admins), 1) + firtAdmin := admins[0].Username + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=510&offset=0&order=DESC", nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + admins = nil + err = render.DecodeJSON(rr.Body, &admins) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(admins), 1) + assert.NotEqual(t, firtAdmin, admins[0].Username) + + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=510&offset=1&order=ASC", nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + admins = nil + err = render.DecodeJSON(rr.Body, &admins) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(admins), 1) + assert.NotEqual(t, firtAdmin, admins[0].Username) + + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=a&offset=0&order=ASC", nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=1&offset=a&order=ASC", nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + req, _ = http.NewRequest(http.MethodGet, adminPath+"?limit=1&offset=0&order=ASCa", nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + + req, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, admin.Username), nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) } func TestGetUsersMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=510&offset=0&order=ASC&username="+defaultUsername, nil) + req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=510&offset=0&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var users []dataprovider.User err = render.DecodeJSON(rr.Body, &users) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) + assert.GreaterOrEqual(t, len(users), 1) req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=a&offset=0&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=a&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASCa", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestDeleteUserInvalidParamsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodDelete, userPath+"/0", nil) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/a", nil) - rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) } func TestGetQuotaScansMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, err := http.NewRequest("GET", quotaScanPath, nil) assert.NoError(t, err) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestStartQuotaScanMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) _, err = os.Stat(user.HomeDir) if err == nil { @@ -2747,20 +3163,23 @@ func TestStartQuotaScanMock(t *testing.T) { userAsJSON = getUserAsJSON(t, user) common.QuotaScans.AddUserQuotaScan(user.Username) req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusConflict, rr.Code) + checkResponseCode(t, http.StatusConflict, rr) assert.True(t, common.QuotaScans.RemoveUserQuotaScan(user.Username)) userAsJSON = getUserAsJSON(t, user) req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusAccepted, rr.Code) + checkResponseCode(t, http.StatusAccepted, rr) for { var scans []common.ActiveQuotaScan req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &scans) if !assert.NoError(t, err, "Error getting active scans") { break @@ -2776,14 +3195,16 @@ func TestStartQuotaScanMock(t *testing.T) { assert.NoError(t, err) } req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusAccepted, rr.Code) + checkResponseCode(t, http.StatusAccepted, rr) for { var scans []common.ActiveQuotaScan req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &scans) if !assert.NoError(t, err) { assert.Fail(t, err.Error(), "Error getting active scans") @@ -2795,14 +3216,17 @@ func TestStartQuotaScanMock(t *testing.T) { time.Sleep(100 * time.Millisecond) } - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestUpdateFolderQuotaUsageMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) mappedPath := filepath.Join(os.TempDir(), "vfolder") f := vfs.BaseVirtualFolder{ MappedPath: mappedPath, @@ -2815,13 +3239,15 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) { folderAsJSON, err := json.Marshal(f) assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) err = render.DecodeJSON(rr.Body, &folder) assert.NoError(t, err) req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var folders []vfs.BaseVirtualFolder url, err := url.Parse(folderPath) @@ -2830,8 +3256,9 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodGet, url.String(), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &folders) assert.NoError(t, err) if assert.Len(t, folders, 1) { @@ -2841,13 +3268,15 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) { } req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer([]byte("string"))) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) assert.True(t, common.QuotaScans.AddVFolderQuotaScan(mappedPath)) req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusConflict, rr.Code) + checkResponseCode(t, http.StatusConflict, rr) assert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(mappedPath)) url, err = url.Parse(folderPath) @@ -2856,11 +3285,14 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodDelete, url.String(), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestStartFolderQuotaScanMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) mappedPath := filepath.Join(os.TempDir(), "vfolder") folder := vfs.BaseVirtualFolder{ MappedPath: mappedPath, @@ -2868,8 +3300,9 @@ func TestStartFolderQuotaScanMock(t *testing.T) { folderAsJSON, err := json.Marshal(folder) assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) _, err = os.Stat(mappedPath) if err == nil { err = os.Remove(mappedPath) @@ -2878,8 +3311,9 @@ func TestStartFolderQuotaScanMock(t *testing.T) { // simulate a duplicate quota scan common.QuotaScans.AddVFolderQuotaScan(mappedPath) req, _ = http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusConflict, rr.Code) + checkResponseCode(t, http.StatusConflict, rr) assert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(mappedPath)) // and now a real quota scan _, err = os.Stat(mappedPath) @@ -2888,13 +3322,15 @@ func TestStartFolderQuotaScanMock(t *testing.T) { assert.NoError(t, err) } req, _ = http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusAccepted, rr.Code) + checkResponseCode(t, http.StatusAccepted, rr) var scans []common.ActiveVirtualFolderQuotaScan for { req, _ = http.NewRequest(http.MethodGet, quotaScanVFolderPath, nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &scans) if !assert.NoError(t, err, "Error getting active folders scans") { break @@ -2911,8 +3347,9 @@ func TestStartFolderQuotaScanMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodDelete, url.String(), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = os.RemoveAll(folderPath) assert.NoError(t, err) err = os.RemoveAll(mappedPath) @@ -2920,37 +3357,51 @@ func TestStartFolderQuotaScanMock(t *testing.T) { } func TestStartQuotaScanNonExistentUserMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) } func TestStartQuotaScanBadUserMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer([]byte("invalid json"))) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestStartQuotaScanBadFolderMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer([]byte("invalid json"))) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) } func TestStartQuotaScanNonExistentFolderMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) folder := vfs.BaseVirtualFolder{ MappedPath: os.TempDir(), } folderAsJSON, err := json.Marshal(folder) assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) } func TestGetFoldersMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) mappedPath := filepath.Join(os.TempDir(), "vfolder") folder := vfs.BaseVirtualFolder{ MappedPath: mappedPath, @@ -2958,8 +3409,9 @@ func TestGetFoldersMock(t *testing.T) { folderAsJSON, err := json.Marshal(folder) assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON)) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) err = render.DecodeJSON(rr.Body, &folder) assert.NoError(t, err) @@ -2970,20 +3422,24 @@ func TestGetFoldersMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodGet, url.String(), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &folders) assert.NoError(t, err) assert.Len(t, folders, 1) req, _ = http.NewRequest(http.MethodGet, folderPath+"?limit=a&offset=0&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) req, _ = http.NewRequest(http.MethodGet, folderPath+"?limit=1&offset=a&order=ASC", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) req, _ = http.NewRequest(http.MethodGet, folderPath+"?limit=1&offset=0&order=ASCa", nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusBadRequest, rr) url, err = url.Parse(folderPath) assert.NoError(t, err) @@ -2991,135 +3447,520 @@ func TestGetFoldersMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodDelete, url.String(), nil) + setBearerForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestGetVersionMock(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, versionPath, nil) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusUnauthorized, rr) + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodGet, versionPath, nil) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, versionPath, nil) + setBearerForReq(req, "abcde") + rr = executeRequest(req) + checkResponseCode(t, http.StatusUnauthorized, rr) } func TestGetConnectionsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestGetStatusMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestDeleteActiveConnectionMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+"/connectionID", nil) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) } func TestNotFoundMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodGet, "/non/existing/path", nil) + setBearerForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) } func TestMethodNotAllowedMock(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, activeConnectionsPath, nil) rr := executeRequest(req) - checkResponseCode(t, http.StatusMethodNotAllowed, rr.Code) -} - -func TestMetricsMock(t *testing.T) { - req, _ := http.NewRequest(http.MethodGet, metricsPath, nil) - rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusMethodNotAllowed, rr) } func TestHealthCheck(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/healthz", nil) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) assert.Equal(t, "ok", rr.Body.String()) } func TestGetWebRootMock(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/", nil) rr := executeRequest(req) - checkResponseCode(t, http.StatusMovedPermanently, rr.Code) + checkResponseCode(t, http.StatusMovedPermanently, rr) req, _ = http.NewRequest(http.MethodGet, webBasePath, nil) rr = executeRequest(req) - checkResponseCode(t, http.StatusMovedPermanently, rr.Code) + checkResponseCode(t, http.StatusMovedPermanently, rr) +} + +func TestWebNotFoundURI(t *testing.T) { + urlString := httpBaseURL + webBasePath + "/a" + req, err := http.NewRequest(http.MethodGet, urlString, nil) + assert.NoError(t, err) + resp, err := httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, urlString, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, "invalid token") + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + +func TestWebLoginMock(t *testing.T) { + form := getAdminLoginForm(defaultTokenAuthUser, defaultTokenAuthPass) + req, _ := http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr := executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + cookie := rr.Header().Get("Set-Cookie") + assert.True(t, strings.HasPrefix(cookie, "jwt=")) + token := cookie[4:] + + req, _ = http.NewRequest(http.MethodGet, serverStatusPath, nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, _ = http.NewRequest(http.MethodGet, webStatusPath+"notfound", nil) + req.RequestURI = webStatusPath + "notfound" + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + + req, _ = http.NewRequest(http.MethodGet, webLogoutPath, nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + cookie = rr.Header().Get("Cookie") + assert.Empty(t, cookie) + // now try using wrong credentials + form = getAdminLoginForm(defaultTokenAuthUser, "wrong pwd") + req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + // try from an ip not allowed + a := getTestAdmin() + a.Username = altAdminUsername + a.Password = altAdminPassword + a.Filters.AllowList = []string{"10.0.0.0/8"} + + _, _, err := httpdtest.AddAdmin(a, http.StatusCreated) + assert.NoError(t, err) + + form = getAdminLoginForm(altAdminUsername, altAdminPassword) + req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.RemoteAddr = "127.1.1.1:1234" + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "login from IP 127.1.1.1 not allowed") + + req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.RemoteAddr = "10.9.9.9:1234" + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + + req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.RemoteAddr = "127.0.1.1:4567" + req.Header.Set("X-Forwarded-For", "10.9.9.9") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "Login from IP 127.0.1.1:4567 is not allowed") + + req, _ = http.NewRequest(http.MethodGet, webLoginPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + _, err = httpdtest.RemoveAdmin(a, http.StatusOK) + assert.NoError(t, err) +} + +func TestAdminNoToken(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, webChangeAdminPwdPath, nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + assert.Equal(t, webLoginPath, rr.Header().Get("Location")) + + req, _ = http.NewRequest(http.MethodGet, webUserPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + assert.Equal(t, webLoginPath, rr.Header().Get("Location")) + + req, _ = http.NewRequest(http.MethodGet, userPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusUnauthorized, rr) + + req, _ = http.NewRequest(http.MethodGet, activeConnectionsPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusUnauthorized, rr) +} + +func TestWebAdminPwdChange(t *testing.T) { + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated) + assert.NoError(t, err) + + token, err := getJWTTokenFromTestServer(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodGet, webChangeAdminPwdPath, nil) + setJWTCookieForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + form := make(url.Values) + form.Set("current_password", altAdminPassword) + form.Set("new_password1", altAdminPassword) + form.Set("new_password2", altAdminPassword) + req, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("new_password1", altAdminPassword+"1") + form.Set("new_password2", altAdminPassword+"1") + req, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + assert.Equal(t, webLoginPath, rr.Header().Get("Location")) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) } func TestBasicWebUsersMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user1 := getTestUser() user1.Username += "1" user1AsJSON := getUserAsJSON(t, user1) req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(user1AsJSON)) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) err = render.DecodeJSON(rr.Body, &user1) assert.NoError(t, err) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=a", nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=1", nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, _ = http.NewRequest(http.MethodGet, webUserPath, nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodGet, webUserPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(webUserPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - req, _ = http.NewRequest(http.MethodGet, webUserPath+"/a", nil) - rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) form := make(url.Values) form.Set("username", user.Username) b, contentType, _ := getMultipartFormData(form, "", "") req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) b, contentType, _ = getMultipartFormData(form, "", "") req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/a", &b) + checkResponseCode(t, http.StatusNotFound, rr) + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/aaa", &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + checkResponseCode(t, http.StatusNotFound, rr) + req, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user1.ID, 10), nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user1.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) +} + +func TestWebAdminBasicMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + form := make(url.Values) + form.Set("username", admin.Username) + form.Set("password", "") + form.Set("status", "a") // invalid status + form.Set("permissions", "*") + req, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("status", "1") + req, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("password", admin.Password) + req, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + _, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK) + assert.NoError(t, err) + + req, _ = http.NewRequest(http.MethodGet, webAdminsPath+"?qlimit=a", nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, webAdminsPath+"?qlimit=1", nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, _ = http.NewRequest(http.MethodGet, webAdminPath, nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("password", "") + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + form.Set("email", "not-an-email") + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("email", "") + form.Set("status", "b") + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + form.Set("email", "admin@example.com") + form.Set("status", "0") + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername+"1"), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + + req, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, altAdminUsername), nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, altAdminUsername+"1"), nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + + req, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, altAdminUsername), nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusNotFound) + assert.NoError(t, err) + + req, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, defaultTokenAuthUser), nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + assert.Contains(t, rr.Body.String(), "You cannot delete yourself") +} + +func TestWebAdminPermissions(t *testing.T) { + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + admin.Permissions = []string{dataprovider.PermAdminAddUsers} + _, _, err := httpdtest.AddAdmin(admin, http.StatusCreated) + assert.NoError(t, err) + + token, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, httpBaseURL+webUserPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err := httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+path.Join(webUserPath, "auser"), nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+webFolderPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+webStatusPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+webConnectionsPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+webAdminPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + + req, err = http.NewRequest(http.MethodGet, httpBaseURL+path.Join(webAdminPath, "a"), nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) +} + +func TestAdminUpdateSelfMock(t *testing.T) { + admin, _, err := httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK) + assert.NoError(t, err) + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + form := make(url.Values) + form.Set("username", admin.Username) + form.Set("password", admin.Password) + form.Set("status", "0") + form.Set("permissions", dataprovider.PermAdminAddUsers) + form.Set("permissions", dataprovider.PermAdminCloseConnections) + req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "You cannot remove these permissions to yourself") + + form.Set("permissions", dataprovider.PermAdminAny) + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "You cannot disable yourself") } func TestWebUserAddMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() user.UploadBandwidth = 32 user.DownloadBandwidth = 64 @@ -3143,118 +3984,131 @@ func TestWebUserAddMock(t *testing.T) { b, contentType, _ := getMultipartFormData(form, "", "") // test invalid url escape req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("public_keys", testPubKey) form.Set("uid", strconv.FormatInt(int64(user.UID), 10)) form.Set("gid", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid gid req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("gid", "0") form.Set("max_sessions", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid max sessions req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("max_sessions", "0") form.Set("quota_size", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid quota size req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("quota_size", "0") form.Set("quota_files", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid quota files req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("quota_files", "0") form.Set("upload_bandwidth", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid upload bandwidth req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("upload_bandwidth", strconv.FormatInt(user.UploadBandwidth, 10)) form.Set("download_bandwidth", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid download bandwidth req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("download_bandwidth", strconv.FormatInt(user.DownloadBandwidth, 10)) form.Set("status", "a") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid status req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("status", strconv.Itoa(user.Status)) form.Set("expiration_date", "123") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid expiration date req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("expiration_date", "") form.Set("allowed_ip", "invalid,ip") b, contentType, _ = getMultipartFormData(form, "", "") // test invalid allowed_ip req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("allowed_ip", "") form.Set("denied_ip", "192.168.1.2") // it should be 192.168.1.2/32 b, contentType, _ = getMultipartFormData(form, "", "") // test invalid denied_ip req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("denied_ip", "") // test invalid max file upload size form.Set("max_upload_file_size", "a") b, contentType, _ = getMultipartFormData(form, "", "") req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("max_upload_file_size", "1000") b, contentType, _ = getMultipartFormData(form, "", "") req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) + checkResponseCode(t, http.StatusSeeOther, rr) // the user already exists, was created with the above request b, contentType, _ = getMultipartFormData(form, "", "") req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusOK, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err := render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + newUser := dataprovider.User{} + err = render.DecodeJSON(rr.Body, &newUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - newUser := users[0] assert.Equal(t, user.UID, newUser.UID) assert.Equal(t, user.UploadBandwidth, newUser.UploadBandwidth) assert.Equal(t, user.DownloadBandwidth, newUser.DownloadBandwidth) @@ -3313,26 +4167,31 @@ func TestWebUserAddMock(t *testing.T) { assert.True(t, utils.IsStringInSlice("*.rar", filter.DeniedPatterns)) } } - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(newUser.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) url, err := url.Parse(folderPath) assert.NoError(t, err) q := url.Query() q.Add("folder_path", mappedDir) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodDelete, url.String(), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestWebUserUpdateMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.MaxSessions = 1 user.QuotaFiles = 2 @@ -3362,18 +4221,18 @@ func TestWebUserUpdateMock(t *testing.T) { form.Set("disconnect", "1") form.Set("additional_info", user.AdditionalInfo) b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, user.HomeDir, updateUser.HomeDir) assert.Equal(t, user.MaxSessions, updateUser.MaxSessions) assert.Equal(t, user.QuotaFiles, updateUser.QuotaFiles) @@ -3394,30 +4253,30 @@ func TestWebUserUpdateMock(t *testing.T) { assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, updateUser.Filters.DeniedLoginMethods)) assert.True(t, utils.IsStringInSlice(common.ProtocolFTP, updateUser.Filters.DeniedProtocols)) assert.True(t, utils.IsStringInSlice(".zip", updateUser.Filters.FileExtensions[0].DeniedExtensions)) - req, err = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestRenderWebCloneUserMock(t *testing.T) { - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) - req, err := http.NewRequest(http.MethodGet, webUserPath+"?cloneFromId=a", nil) + req, err := http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFrom=%v", user.Username), nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr.Code) + checkResponseCode(t, http.StatusOK, rr) - req, err = http.NewRequest(http.MethodGet, webUserPath+"?cloneFromId=1234", nil) + req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFrom=%v", altAdminPassword), nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr.Code) - - req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFromId=%v", user.ID), nil) - assert.NoError(t, err) - rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusNotFound, rr) if config.GetProviderConf().Driver != "memory" { user.FsConfig = dataprovider.Filesystem{ @@ -3433,23 +4292,27 @@ func TestRenderWebCloneUserMock(t *testing.T) { err = dataprovider.UpdateUser(&user) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFromId=%v", user.ID), nil) + req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFrom=%v", user.Username), nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestWebUserS3Mock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.S3FilesystemProvider user.FsConfig.S3Config.Bucket = "test" @@ -3491,33 +4354,35 @@ func TestWebUserS3Mock(t *testing.T) { // test invalid s3_upload_part_size form.Set("s3_upload_part_size", "a") b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // test invalid s3_concurrency form.Set("s3_upload_part_size", strconv.FormatInt(user.FsConfig.S3Config.UploadPartSize, 10)) form.Set("s3_upload_concurrency", "a") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // now add the user form.Set("s3_upload_concurrency", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency)) b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate) assert.Equal(t, updateUser.FsConfig.S3Config.Bucket, user.FsConfig.S3Config.Bucket) assert.Equal(t, updateUser.FsConfig.S3Config.Region, user.FsConfig.S3Config.Region) @@ -3535,18 +4400,19 @@ func TestWebUserS3Mock(t *testing.T) { // now check that a redacted password is not saved form.Set("s3_access_secret", "[**redacted**] ") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - users = nil - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + + var lastUpdatedUser dataprovider.User + err = render.DecodeJSON(rr.Body, &lastUpdatedUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - lastUpdatedUser := users[0] assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetStatus()) assert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload(), lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetPayload()) assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetKey()) @@ -3555,31 +4421,36 @@ func TestWebUserS3Mock(t *testing.T) { form.Set("s3_access_key", "") form.Set("s3_access_secret", "") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - users = nil - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var userGet dataprovider.User + err = render.DecodeJSON(rr.Body, &userGet) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - assert.True(t, users[0].FsConfig.S3Config.AccessSecret.IsEmpty()) + assert.True(t, userGet.FsConfig.S3Config.AccessSecret.IsEmpty()) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestWebUserGCSMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) credentialsFilePath := filepath.Join(os.TempDir(), "gcs.json") @@ -3612,30 +4483,32 @@ func TestWebUserGCSMock(t *testing.T) { form.Set("allowed_extensions", "/dir1::.jpg,.png") form.Set("max_upload_file_size", "0") b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) b, contentType, _ = getMultipartFormData(form, "gcs_credential_file", credentialsFilePath) - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = createTestFile(credentialsFilePath, 4096) assert.NoError(t, err) b, contentType, _ = getMultipartFormData(form, "gcs_credential_file", credentialsFilePath) - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate) assert.Equal(t, user.FsConfig.Provider, updateUser.FsConfig.Provider) assert.Equal(t, user.FsConfig.GCSConfig.Bucket, updateUser.FsConfig.GCSConfig.Bucket) @@ -3644,31 +4517,36 @@ func TestWebUserGCSMock(t *testing.T) { assert.Equal(t, "/dir1", updateUser.Filters.FileExtensions[0].Path) form.Set("gcs_auto_credentials", "on") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + updateUser = dataprovider.User{} + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser = users[0] assert.Equal(t, 1, updateUser.FsConfig.GCSConfig.AutomaticCredentials) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = os.Remove(credentialsFilePath) assert.NoError(t, err) } func TestWebUserAzureBlobMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.Container = "container" @@ -3709,33 +4587,35 @@ func TestWebUserAzureBlobMock(t *testing.T) { // test invalid az_upload_part_size form.Set("az_upload_part_size", "a") b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // test invalid az_upload_concurrency form.Set("az_upload_part_size", strconv.FormatInt(user.FsConfig.AzBlobConfig.UploadPartSize, 10)) form.Set("az_upload_concurrency", "a") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // now add the user form.Set("az_upload_concurrency", strconv.Itoa(user.FsConfig.AzBlobConfig.UploadConcurrency)) b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate) assert.Equal(t, updateUser.FsConfig.AzBlobConfig.Container, user.FsConfig.AzBlobConfig.Container) assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountName, user.FsConfig.AzBlobConfig.AccountName) @@ -3752,34 +4632,38 @@ func TestWebUserAzureBlobMock(t *testing.T) { // now check that a redacted password is not saved form.Set("az_account_key", "[**redacted**] ") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - users = nil - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var lastUpdatedUser dataprovider.User + err = render.DecodeJSON(rr.Body, &lastUpdatedUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - lastUpdatedUser := users[0] assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload(), lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()) assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetKey()) assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestWebUserCryptMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.CryptedFilesystemProvider user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypted passphrase") @@ -3806,24 +4690,25 @@ func TestWebUserCryptMock(t *testing.T) { form.Set("max_upload_file_size", "0") // passphrase cannot be empty b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("crypt_passphrase", user.FsConfig.CryptConfig.Passphrase.GetPayload()) b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate) assert.Equal(t, 2, len(updateUser.Filters.FileExtensions)) assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.CryptConfig.Passphrase.GetStatus()) @@ -3833,34 +4718,38 @@ func TestWebUserCryptMock(t *testing.T) { // now check that a redacted password is not saved form.Set("crypt_passphrase", "[**redacted**] ") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - users = nil - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var lastUpdatedUser dataprovider.User + err = render.DecodeJSON(rr.Body, &lastUpdatedUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - lastUpdatedUser := users[0] assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetStatus()) assert.Equal(t, updateUser.FsConfig.CryptConfig.Passphrase.GetPayload(), lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetPayload()) assert.Empty(t, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetKey()) assert.Empty(t, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetAdditionalData()) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestWebUserSFTPFsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) user := getTestUser() userAsJSON := getUserAsJSON(t, user) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - err := render.DecodeJSON(rr.Body, &user) + checkResponseCode(t, http.StatusCreated, rr) + err = render.DecodeJSON(rr.Body, &user) assert.NoError(t, err) user.FsConfig.Provider = dataprovider.SFTPFilesystemProvider user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1" @@ -3892,10 +4781,11 @@ func TestWebUserSFTPFsMock(t *testing.T) { form.Set("max_upload_file_size", "0") // empty sftpconfig b, contentType, _ := getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) form.Set("sftp_endpoint", user.FsConfig.SFTPConfig.Endpoint) form.Set("sftp_username", user.FsConfig.SFTPConfig.Username) form.Set("sftp_password", user.FsConfig.SFTPConfig.Password.GetPayload()) @@ -3903,18 +4793,18 @@ func TestWebUserSFTPFsMock(t *testing.T) { form.Set("sftp_fingerprints", user.FsConfig.SFTPConfig.Fingerprints[0]) form.Set("sftp_prefix", user.FsConfig.SFTPConfig.Prefix) b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - var users []dataprovider.User - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var updateUser dataprovider.User + err = render.DecodeJSON(rr.Body, &updateUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - updateUser := users[0] assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate) assert.Equal(t, 2, len(updateUser.Filters.FileExtensions)) assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.SFTPConfig.Password.GetStatus()) @@ -3934,18 +4824,18 @@ func TestWebUserSFTPFsMock(t *testing.T) { form.Set("sftp_password", "[**redacted**] ") form.Set("sftp_private_key", "[**redacted**]") b, contentType, _ = getMultipartFormData(form, "", "") - req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) + req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", contentType) rr = executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) - req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + checkResponseCode(t, http.StatusSeeOther, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) - users = nil - err = render.DecodeJSON(rr.Body, &users) + checkResponseCode(t, http.StatusOK, rr) + var lastUpdatedUser dataprovider.User + err = render.DecodeJSON(rr.Body, &lastUpdatedUser) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - lastUpdatedUser := users[0] assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.SFTPConfig.Password.GetStatus()) assert.Equal(t, updateUser.FsConfig.SFTPConfig.Password.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.Password.GetPayload()) assert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.Password.GetKey()) @@ -3954,38 +4844,45 @@ func TestWebUserSFTPFsMock(t *testing.T) { assert.Equal(t, updateUser.FsConfig.SFTPConfig.PrivateKey.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetPayload()) assert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetKey()) assert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData()) - req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestAddWebFoldersMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) mappedPath := filepath.Clean(os.TempDir()) form := make(url.Values) form.Set("mapped_path", mappedPath) req, err := http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode())) assert.NoError(t, err) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := executeRequest(req) - checkResponseCode(t, http.StatusSeeOther, rr.Code) + checkResponseCode(t, http.StatusSeeOther, rr) // adding the same folder will fail since the path must be unique req, err = http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode())) assert.NoError(t, err) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // invalid form req, err = http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode())) assert.NoError(t, err) + setJWTCookieForReq(req, token) req.Header.Set("Content-Type", "text/plain; boundary=") rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) // now render the add folder page req, err = http.NewRequest(http.MethodGet, webFolderPath, nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) var folders []vfs.BaseVirtualFolder url, err := url.Parse(folderPath) @@ -3994,8 +4891,9 @@ func TestAddWebFoldersMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodGet, url.String(), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) err = render.DecodeJSON(rr.Body, &folders) assert.NoError(t, err) if assert.Len(t, folders, 1) { @@ -4009,11 +4907,14 @@ func TestAddWebFoldersMock(t *testing.T) { q.Add("folder_path", mappedPath) url.RawQuery = q.Encode() req, _ = http.NewRequest(http.MethodDelete, url.String(), nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestWebFoldersMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) mappedPath1 := filepath.Join(os.TempDir(), "vfolder1") mappedPath2 := filepath.Join(os.TempDir(), "vfolder2") folders := []vfs.BaseVirtualFolder{ @@ -4028,22 +4929,26 @@ func TestWebFoldersMock(t *testing.T) { folderAsJSON, err := json.Marshal(folder) assert.NoError(t, err) req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON)) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusCreated, rr) } req, err := http.NewRequest(http.MethodGet, webFoldersPath, nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, err = http.NewRequest(http.MethodGet, webFoldersPath+"?qlimit=a", nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) req, err = http.NewRequest(http.MethodGet, webFoldersPath+"?qlimit=1", nil) assert.NoError(t, err) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) for _, folder := range folders { url, err := url.Parse(folderPath) @@ -4052,53 +4957,81 @@ func TestWebFoldersMock(t *testing.T) { q.Add("folder_path", folder.MappedPath) url.RawQuery = q.Encode() req, _ := http.NewRequest(http.MethodDelete, url.String(), nil) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } } func TestProviderClosedMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) dataprovider.Close() req, _ := http.NewRequest(http.MethodGet, webFoldersPath, nil) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) + checkResponseCode(t, http.StatusInternalServerError, rr) form := make(url.Values) form.Set("username", "test") req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode())) + setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr.Code) - err := config.LoadConfig(configDir, "") + checkResponseCode(t, http.StatusInternalServerError, rr) + req, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, defaultTokenAuthUser), nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr) + + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), strings.NewReader(form.Encode())) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr) + + req, _ = http.NewRequest(http.MethodGet, webAdminsPath, nil) + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr) + + err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.CredentialsPath = credentialsPath err = os.RemoveAll(credentialsPath) assert.NoError(t, err) - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } func TestGetWebConnectionsMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodGet, webConnectionsPath, nil) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestGetWebStatusMock(t *testing.T) { + token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) req, _ := http.NewRequest(http.MethodGet, webStatusPath, nil) + setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func TestStaticFilesMock(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil) rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr.Code) + checkResponseCode(t, http.StatusOK, rr) } func waitTCPListening(address string) { @@ -4115,6 +5048,16 @@ func waitTCPListening(address string) { } } +func getTestAdmin() dataprovider.Admin { + return dataprovider.Admin{ + Username: defaultTokenAuthUser, + Password: defaultTokenAuthPass, + Status: 1, + Permissions: []string{dataprovider.PermAdminAny}, + Email: "admin@example.com", + } +} + func getTestUser() dataprovider.User { user := dataprovider.User{ Username: defaultUsername, @@ -4133,14 +5076,44 @@ func getUserAsJSON(t *testing.T, user dataprovider.User) []byte { return json } +func getAdminLoginForm(username, password string) url.Values { + form := make(url.Values) + form.Set("username", username) + form.Set("password", password) + return form +} + +func setBearerForReq(req *http.Request, jwtToken string) { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", jwtToken)) +} + +func setJWTCookieForReq(req *http.Request, jwtToken string) { + req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", jwtToken)) +} + +func getJWTTokenFromTestServer(username, password string) (string, error) { + req, _ := http.NewRequest(http.MethodGet, "/api/v2/token", nil) + req.SetBasicAuth(username, password) + rr := executeRequest(req) + if rr.Code != http.StatusOK { + return "", fmt.Errorf("unexpected status code %v", rr) + } + responseHolder := make(map[string]interface{}) + err := render.DecodeJSON(rr.Body, &responseHolder) + if err != nil { + return "", err + } + return responseHolder["access_token"].(string), nil +} + func executeRequest(req *http.Request) *httptest.ResponseRecorder { rr := httptest.NewRecorder() testServer.Config.Handler.ServeHTTP(rr, req) return rr } -func checkResponseCode(t *testing.T, expected, actual int) { - assert.Equal(t, expected, actual) +func checkResponseCode(t *testing.T, expected int, rr *httptest.ResponseRecorder) { + assert.Equal(t, expected, rr.Code, rr.Body.String()) } func createTestFile(path string, size int64) error { diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 0370a5d9..e2221739 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -1,33 +1,31 @@ package httpd import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" "html/template" - "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" - "path/filepath" + "path" "runtime" "strings" "testing" + "time" "github.com/go-chi/chi" + "github.com/go-chi/jwtauth" + "github.com/lestrrat-go/jwx/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" - "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/utils" - "github.com/drakkan/sftpgo/vfs" -) - -const ( - invalidURL = "http://foo\x7f.com/" - inactiveURL = "http://127.0.0.1:12345" ) func TestShouldBind(t *testing.T) { @@ -55,455 +53,6 @@ func TestGetRespStatus(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, respStatus) } -func TestCheckResponse(t *testing.T) { - err := checkResponse(http.StatusOK, http.StatusCreated) - assert.Error(t, err) - err = checkResponse(http.StatusBadRequest, http.StatusBadRequest) - assert.NoError(t, err) -} - -func TestCheckFolder(t *testing.T) { - expected := &vfs.BaseVirtualFolder{} - actual := &vfs.BaseVirtualFolder{} - err := checkFolder(expected, actual) - assert.Error(t, err) - expected.ID = 1 - actual.ID = 2 - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.ID = 2 - actual.ID = 2 - expected.MappedPath = "path" - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.MappedPath = "" - expected.LastQuotaUpdate = 1 - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.LastQuotaUpdate = 0 - expected.UsedQuotaFiles = 1 - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.UsedQuotaFiles = 0 - expected.UsedQuotaSize = 1 - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.UsedQuotaSize = 0 - expected.Users = append(expected.Users, "user1") - err = checkFolder(expected, actual) - assert.Error(t, err) - actual.Users = append(actual.Users, "user2") - err = checkFolder(expected, actual) - assert.Error(t, err) - expected.Users = nil - actual.Users = nil -} - -func TestCheckUser(t *testing.T) { - expected := &dataprovider.User{} - actual := &dataprovider.User{} - actual.Password = "password" - err := checkUser(expected, actual) - assert.Error(t, err) - actual.Password = "" - err = checkUser(expected, actual) - assert.Error(t, err) - expected.ID = 1 - actual.ID = 2 - err = checkUser(expected, actual) - assert.Error(t, err) - expected.ID = 2 - actual.ID = 2 - expected.Permissions = make(map[string][]string) - expected.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload} - actual.Permissions = make(map[string][]string) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Permissions["/"] = append(expected.Permissions["/"], dataprovider.PermRename) - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Permissions = make(map[string][]string) - expected.Permissions["/somedir"] = []string{dataprovider.PermAny} - actual.Permissions = make(map[string][]string) - actual.Permissions["/otherdir"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Permissions = make(map[string][]string) - actual.Permissions = make(map[string][]string) - actual.FsConfig.Provider = dataprovider.S3FilesystemProvider - err = checkUser(expected, actual) - assert.Error(t, err) - actual.FsConfig.Provider = dataprovider.LocalFilesystemProvider - expected.VirtualFolders = append(expected.VirtualFolders, vfs.VirtualFolder{ - BaseVirtualFolder: vfs.BaseVirtualFolder{ - MappedPath: os.TempDir(), - }, - VirtualPath: "/vdir", - }) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.VirtualFolders = append(actual.VirtualFolders, vfs.VirtualFolder{ - BaseVirtualFolder: vfs.BaseVirtualFolder{ - MappedPath: os.TempDir(), - }, - VirtualPath: "/vdir1", - }) - err = checkUser(expected, actual) - assert.Error(t, err) -} - -func TestCompareUserFilters(t *testing.T) { - expected := &dataprovider.User{} - actual := &dataprovider.User{} - actual.ID = 1 - expected.ID = 1 - expected.Filters.AllowedIP = []string{} - actual.Filters.AllowedIP = []string{"192.168.1.2/32"} - err := checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.AllowedIP = []string{"192.168.1.3/32"} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.AllowedIP = []string{} - actual.Filters.AllowedIP = []string{} - expected.Filters.DeniedIP = []string{} - actual.Filters.DeniedIP = []string{"192.168.1.2/32"} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedIP = []string{"192.168.1.3/32"} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedIP = []string{} - actual.Filters.DeniedIP = []string{} - expected.Filters.DeniedLoginMethods = []string{} - actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedLoginMethods = []string{} - actual.Filters.DeniedLoginMethods = []string{} - actual.Filters.DeniedProtocols = []string{common.ProtocolFTP} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedProtocols = []string{common.ProtocolWebDAV} - err = checkUser(expected, actual) - assert.Error(t, err) - expected.Filters.DeniedProtocols = []string{} - actual.Filters.DeniedProtocols = []string{} - expected.Filters.MaxUploadFileSize = 0 - actual.Filters.MaxUploadFileSize = 100 - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.MaxUploadFileSize = 0 - expected.Filters.FileExtensions = append(expected.Filters.FileExtensions, dataprovider.ExtensionsFilter{ - Path: "/", - AllowedExtensions: []string{".jpg", ".png"}, - DeniedExtensions: []string{".zip", ".rar"}, - }) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FileExtensions = append(actual.Filters.FileExtensions, dataprovider.ExtensionsFilter{ - Path: "/sub", - AllowedExtensions: []string{".jpg", ".png"}, - DeniedExtensions: []string{".zip", ".rar"}, - }) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{ - Path: "/", - AllowedExtensions: []string{".jpg"}, - DeniedExtensions: []string{".zip", ".rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{ - Path: "/", - AllowedExtensions: []string{".tiff", ".png"}, - DeniedExtensions: []string{".zip", ".rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{ - Path: "/", - AllowedExtensions: []string{".jpg", ".png"}, - DeniedExtensions: []string{".tar.gz", ".rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FileExtensions = nil - actual.Filters.FilePatterns = nil - expected.Filters.FileExtensions = nil - expected.Filters.FilePatterns = nil - expected.Filters.FilePatterns = append(expected.Filters.FilePatterns, dataprovider.PatternsFilter{ - Path: "/", - AllowedPatterns: []string{"*.jpg", "*.png"}, - DeniedPatterns: []string{"*.zip", "*.rar"}, - }) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FilePatterns = append(actual.Filters.FilePatterns, dataprovider.PatternsFilter{ - Path: "/sub", - AllowedPatterns: []string{"*.jpg", "*.png"}, - DeniedPatterns: []string{"*.zip", "*.rar"}, - }) - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{ - Path: "/", - AllowedPatterns: []string{"*.jpg"}, - DeniedPatterns: []string{"*.zip", "*.rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{ - Path: "/", - AllowedPatterns: []string{"*.tiff", "*.png"}, - DeniedPatterns: []string{"*.zip", "*.rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) - actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{ - Path: "/", - AllowedPatterns: []string{"*.jpg", "*.png"}, - DeniedPatterns: []string{"*.tar.gz", "*.rar"}, - } - err = checkUser(expected, actual) - assert.Error(t, err) -} - -func TestCompareUserFields(t *testing.T) { - expected := &dataprovider.User{} - actual := &dataprovider.User{} - expected.Permissions = make(map[string][]string) - actual.Permissions = make(map[string][]string) - expected.Username = "test" - err := compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.Username = "" - expected.HomeDir = "homedir" - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.HomeDir = "" - expected.UID = 1 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.UID = 0 - expected.GID = 1 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.GID = 0 - expected.MaxSessions = 2 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.MaxSessions = 0 - expected.QuotaSize = 4096 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.QuotaSize = 0 - expected.QuotaFiles = 2 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.QuotaFiles = 0 - expected.Permissions["/"] = []string{dataprovider.PermCreateDirs} - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.Permissions = nil - expected.UploadBandwidth = 64 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.UploadBandwidth = 0 - expected.DownloadBandwidth = 128 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.DownloadBandwidth = 0 - expected.Status = 1 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.Status = 0 - expected.ExpirationDate = 123 - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) - expected.ExpirationDate = 0 - expected.AdditionalInfo = "info" - err = compareEqualsUserFields(expected, actual) - assert.Error(t, err) -} - -func TestCompareUserFsConfig(t *testing.T) { - secretString := "access secret" - expected := &dataprovider.User{} - actual := &dataprovider.User{} - expected.SetEmptySecretsIfNil() - actual.SetEmptySecretsIfNil() - expected.FsConfig.Provider = dataprovider.S3FilesystemProvider - err := compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.Provider = dataprovider.LocalFilesystemProvider - expected.FsConfig.S3Config.Bucket = "bucket" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.Bucket = "" - expected.FsConfig.S3Config.Region = "region" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.Region = "" - expected.FsConfig.S3Config.AccessKey = "access key" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.AccessKey = "" - actual.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - secret, err := utils.EncryptData(secretString) - assert.NoError(t, err) - actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() - kmsSecret, err := kms.GetSecretFromCompatString(secret) - assert.NoError(t, err) - expected.FsConfig.S3Config.AccessSecret = kmsSecret - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) - actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) - actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, "", "", "") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "", "data") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "key", "") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret = nil - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() - actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() - expected.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.Endpoint = "" - expected.FsConfig.S3Config.StorageClass = "Standard" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.StorageClass = "" - expected.FsConfig.S3Config.KeyPrefix = "somedir/subdir" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.KeyPrefix = "" - expected.FsConfig.S3Config.UploadPartSize = 10 - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.UploadPartSize = 0 - expected.FsConfig.S3Config.UploadConcurrency = 3 - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.S3Config.UploadConcurrency = 0 - expected.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("payload") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret() - expected.FsConfig.SFTPConfig.Endpoint = "endpoint" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.SFTPConfig.Endpoint = "" - expected.FsConfig.SFTPConfig.Username = "user" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.SFTPConfig.Username = "" - expected.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftppwd") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.SFTPConfig.Password = kms.NewEmptySecret() - expected.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret("fake key") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret() - expected.FsConfig.SFTPConfig.Prefix = "/home" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.SFTPConfig.Prefix = "" - expected.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:..."} - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - actual.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:different"} - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) -} - -func TestCompareUserGCSConfig(t *testing.T) { - expected := &dataprovider.User{} - actual := &dataprovider.User{} - expected.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir" - err := compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.GCSConfig.KeyPrefix = "" - expected.FsConfig.GCSConfig.Bucket = "bucket" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.GCSConfig.Bucket = "" - expected.FsConfig.GCSConfig.StorageClass = "Standard" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.GCSConfig.StorageClass = "" - expected.FsConfig.GCSConfig.AutomaticCredentials = 1 - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.GCSConfig.AutomaticCredentials = 0 -} - -func TestCompareUserAzureConfig(t *testing.T) { - expected := &dataprovider.User{} - actual := &dataprovider.User{} - expected.FsConfig.AzBlobConfig.Container = "a" - err := compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.Container = "" - expected.FsConfig.AzBlobConfig.AccountName = "aname" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.AccountName = "" - expected.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusAWS, "payload", "", "") - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() - expected.FsConfig.AzBlobConfig.Endpoint = "endpt" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.Endpoint = "" - expected.FsConfig.AzBlobConfig.SASURL = "url" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.SASURL = "" - expected.FsConfig.AzBlobConfig.UploadPartSize = 1 - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.UploadPartSize = 0 - expected.FsConfig.AzBlobConfig.UploadConcurrency = 1 - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.UploadConcurrency = 0 - expected.FsConfig.AzBlobConfig.KeyPrefix = "prefix/" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.KeyPrefix = "" - expected.FsConfig.AzBlobConfig.UseEmulator = true - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.UseEmulator = false - expected.FsConfig.AzBlobConfig.AccessTier = "Hot" - err = compareUserFsConfig(expected, actual) - assert.Error(t, err) - expected.FsConfig.AzBlobConfig.AccessTier = "" -} - func TestGCSWebInvalidFormFile(t *testing.T) { form := make(url.Values) form.Set("username", "test_username") @@ -516,170 +65,313 @@ func TestGCSWebInvalidFormFile(t *testing.T) { assert.EqualError(t, err, http.ErrNotMultipart.Error()) } -func TestApiCallsWithBadURL(t *testing.T) { - oldBaseURL := httpBaseURL - oldAuthUsername := authUsername - oldAuthPassword := authPassword - SetBaseURLAndCredentials(invalidURL, oldAuthUsername, oldAuthPassword) - folder := vfs.BaseVirtualFolder{ - MappedPath: os.TempDir(), +func TestInvalidToken(t *testing.T) { + admin := dataprovider.Admin{ + Username: "admin", } - u := dataprovider.User{} - _, _, err := UpdateUser(u, http.StatusBadRequest, "") - assert.Error(t, err) - _, err = RemoveUser(u, http.StatusNotFound) - assert.Error(t, err) - _, err = RemoveFolder(folder, http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetUsers(1, 0, "", http.StatusBadRequest) - assert.Error(t, err) - _, _, err = GetFolders(1, 0, "", http.StatusBadRequest) - assert.Error(t, err) - _, err = UpdateQuotaUsage(u, "", http.StatusNotFound) - assert.Error(t, err) - _, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound) - assert.Error(t, err) - _, err = CloseConnection("non_existent_id", http.StatusNotFound) - assert.Error(t, err) - _, _, err = Dumpdata("backup.json", "", http.StatusBadRequest) - assert.Error(t, err) - _, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusBadRequest) - assert.Error(t, err) - _, _, err = GetBanTime("", http.StatusBadRequest) - assert.Error(t, err) - _, _, err = GetScore("", http.StatusBadRequest) - assert.Error(t, err) - SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword) + errFake := errors.New("fake error") + asJSON, err := json.Marshal(admin) + assert.NoError(t, err) + req, _ := http.NewRequest(http.MethodPut, path.Join(adminPath, admin.Username), bytes.NewBuffer(asJSON)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("username", admin.Username) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake)) + rr := httptest.NewRecorder() + updateAdmin(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + rr = httptest.NewRecorder() + deleteAdmin(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + + adminPwd := adminPwd{ + CurrentPassword: "old", + NewPassword: "new", + } + asJSON, err = json.Marshal(adminPwd) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, "", bytes.NewBuffer(asJSON)) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake)) + rr = httptest.NewRecorder() + changeAdminPassword(rr, req) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + adm := getAdminFromToken(req) + assert.Empty(t, adm.Username) } -func TestApiCallToNotListeningServer(t *testing.T) { - oldBaseURL := httpBaseURL - oldAuthUsername := authUsername - oldAuthPassword := authPassword - SetBaseURLAndCredentials(inactiveURL, oldAuthUsername, oldAuthPassword) - u := dataprovider.User{} - _, _, err := AddUser(u, http.StatusBadRequest) - assert.Error(t, err) - _, _, err = UpdateUser(u, http.StatusNotFound, "") - assert.Error(t, err) - _, err = RemoveUser(u, http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetUserByID(-1, http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetUsers(100, 0, "", http.StatusOK) - assert.Error(t, err) - _, err = UpdateQuotaUsage(u, "", http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetQuotaScans(http.StatusOK) - assert.Error(t, err) - _, err = StartQuotaScan(u, http.StatusNotFound) - assert.Error(t, err) - folder := vfs.BaseVirtualFolder{ - MappedPath: os.TempDir(), - } - _, err = StartFolderQuotaScan(folder, http.StatusNotFound) - assert.Error(t, err) - _, _, err = AddFolder(folder, http.StatusOK) - assert.Error(t, err) - _, err = RemoveFolder(folder, http.StatusOK) - assert.Error(t, err) - _, _, err = GetFolders(0, 0, "", http.StatusOK) - assert.Error(t, err) - _, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetFoldersQuotaScans(http.StatusOK) - assert.Error(t, err) - _, _, err = GetConnections(http.StatusOK) - assert.Error(t, err) - _, err = CloseConnection("non_existent_id", http.StatusNotFound) - assert.Error(t, err) - _, _, err = GetVersion(http.StatusOK) - assert.Error(t, err) - _, _, err = GetStatus(http.StatusOK) - assert.Error(t, err) - _, _, err = Dumpdata("backup.json", "0", http.StatusOK) - assert.Error(t, err) - _, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusOK) - assert.Error(t, err) - _, _, err = GetBanTime("", http.StatusBadRequest) - assert.Error(t, err) - _, _, err = GetScore("", http.StatusBadRequest) - assert.Error(t, err) - err = UnbanIP("", http.StatusBadRequest) - assert.Error(t, err) +func TestUpdateWebAdminInvalidClaims(t *testing.T) { + server := httpdServer{} + server.initializeRouter() - SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword) + rr := httptest.NewRecorder() + admin := dataprovider.Admin{ + Username: "", + Password: "password", + } + c := jwtTokenClaims{ + Username: admin.Username, + Permissions: admin.Permissions, + Signature: admin.GetSignature(), + } + token, err := c.createTokenResponse(server.tokenAuth) + assert.NoError(t, err) + + form := make(url.Values) + form.Set("status", "1") + req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode()))) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("username", "admin") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) + handleWebUpdateAdminPost(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Contains(t, rr.Body.String(), "Invalid token claims") } -func TestBasicAuth(t *testing.T) { - oldAuthUsername := authUsername - oldAuthPassword := authPassword - authUserFile := filepath.Join(os.TempDir(), "http_users.txt") - authUserData := []byte("test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\n") - err := ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - httpAuth, _ = common.NewBasicAuthProvider(authUserFile) - _, _, err = GetVersion(http.StatusUnauthorized) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test1", "password1") - _, _, err = GetVersion(http.StatusOK) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test1", "wrong_password") - resp, _ := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(metricsPath), nil, "") - defer resp.Body.Close() - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - authUserData = append(authUserData, []byte("test2:$1$OtSSTL8b$bmaCqEksI1e7rnZSjsIDR1\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test2", "password2") - _, _, err = GetVersion(http.StatusOK) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password") - _, _, err = GetVersion(http.StatusOK) - assert.Error(t, err) - authUserData = append(authUserData, []byte("test2:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test2", "password2") - _, _, err = GetVersion(http.StatusOK) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password") - _, _, err = GetVersion(http.StatusOK) - assert.Error(t, err) - authUserData = append(authUserData, []byte("test3:$apr1$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test3", "wrong_password") - _, _, err = GetVersion(http.StatusUnauthorized) - assert.NoError(t, err) - authUserData = append(authUserData, []byte("test4:$invalid$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test3", "password2") - _, _, err = GetVersion(http.StatusUnauthorized) - assert.NoError(t, err) - if runtime.GOOS != osWindows { - authUserData = append(authUserData, []byte("test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) - assert.NoError(t, err) - err = os.Chmod(authUserFile, 0001) - assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test5", "password2") - _, _, err = GetVersion(http.StatusUnauthorized) - assert.NoError(t, err) - err = os.Chmod(authUserFile, os.ModePerm) - assert.NoError(t, err) +func TestCreateTokenError(t *testing.T) { + server := httpdServer{ + tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil), } - authUserData = append(authUserData, []byte("\"foo\"bar\"\r\n")...) - err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) + rr := httptest.NewRecorder() + admin := dataprovider.Admin{ + Username: "admin", + Password: "password", + } + req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) + + server.checkAddrAndSendToken(rr, req, admin) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + rr = httptest.NewRecorder() + form := make(url.Values) + form.Set("username", admin.Username) + form.Set("password", admin.Password) + req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode()))) + req.RemoteAddr = "127.0.0.1:1234" + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + server.handleWebLoginPost(rr, req) + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + // req with no content type + req, _ = http.NewRequest(http.MethodPost, webLoginPath, nil) + rr = httptest.NewRecorder() + server.handleWebLoginPost(rr, req) + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + // req with no POST body + req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%AO%GG", nil) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = httptest.NewRecorder() + server.handleWebLoginPost(rr, req) + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A1%G2", nil) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = httptest.NewRecorder() + handleWebAdminChangePwdPost(rr, req) + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + + req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + _, err := getAdminFromPostFields(req) + assert.Error(t, err) +} + +func TestJWTTokenValidation(t *testing.T) { + tokenAuth := jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil) + claims := make(map[string]interface{}) + claims["username"] = "admin" + claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour) + token, _, err := tokenAuth.Encode(claims) assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, "test2", "password2") - _, _, err = GetVersion(http.StatusUnauthorized) + + r := GetHTTPRouter() + fn := jwtAuthenticator(r) + rr := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, userPath, nil) + ctx := jwtauth.NewContext(req.Context(), token, nil) + fn.ServeHTTP(rr, req.WithContext(ctx)) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + + fn = jwtAuthenticatorWeb(r) + rr = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodGet, webUserPath, nil) + ctx = jwtauth.NewContext(req.Context(), token, nil) + + fn.ServeHTTP(rr, req.WithContext(ctx)) + assert.Equal(t, http.StatusFound, rr.Code) + + errTest := errors.New("test error") + permFn := checkPerm(dataprovider.PermAdminAny) + fn = permFn(r) + rr = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodGet, userPath, nil) + ctx = jwtauth.NewContext(req.Context(), token, errTest) + fn.ServeHTTP(rr, req.WithContext(ctx)) + assert.Equal(t, http.StatusBadRequest, rr.Code) + + permFn = checkPerm(dataprovider.PermAdminAny) + fn = permFn(r) + rr = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodGet, webUserPath, nil) + req.RequestURI = webUserPath + ctx = jwtauth.NewContext(req.Context(), token, errTest) + fn.ServeHTTP(rr, req.WithContext(ctx)) + assert.Equal(t, http.StatusBadRequest, rr.Code) +} + +func TestAdminAllowListConnAddr(t *testing.T) { + server := httpdServer{} + admin := dataprovider.Admin{ + Filters: dataprovider.AdminFilters{ + AllowList: []string{"192.168.1.0/24"}, + }, + } + rr := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) + ctx := context.WithValue(req.Context(), connAddrKey, "127.0.0.1:4567") + req.RemoteAddr = "192.168.1.16:1234" + server.checkAddrAndSendToken(rr, req.WithContext(ctx), admin) + assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String()) +} + +func TestUpdateContextFromCookie(t *testing.T) { + server := httpdServer{ + tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil), + } + req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) + claims := make(map[string]interface{}) + claims["a"] = "b" + token, _, err := server.tokenAuth.Encode(claims) assert.NoError(t, err) - err = os.Remove(authUserFile) + + ctx := jwtauth.NewContext(req.Context(), token, nil) + server.updateContextFromCookie(req.WithContext(ctx)) +} + +func TestCookieExpiration(t *testing.T) { + server := httpdServer{ + tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil), + } + err := errors.New("test error") + rr := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) + ctx := jwtauth.NewContext(req.Context(), nil, err) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie := rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + claims := make(map[string]interface{}) + claims["a"] = "b" + token, _, err := server.tokenAuth.Encode(claims) assert.NoError(t, err) - SetBaseURLAndCredentials(httpBaseURL, oldAuthUsername, oldAuthPassword) - httpAuth, _ = common.NewBasicAuthProvider("") + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + admin := dataprovider.Admin{ + Username: "newtestadmin", + Password: "password", + Permissions: []string{dataprovider.PermAdminAny}, + } + claims = make(map[string]interface{}) + claims[claimUsernameKey] = admin.Username + claims[claimPermissionsKey] = admin.Permissions + claims[jwt.SubjectKey] = admin.GetSignature() + claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute) + token, _, err = server.tokenAuth.Encode(claims) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + admin.Status = 0 + err = dataprovider.AddAdmin(&admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + admin.Status = 1 + admin.Filters.AllowList = []string{"172.16.1.0/24"} + err = dataprovider.UpdateAdmin(&admin) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + admin, err = dataprovider.AdminExists(admin.Username) + assert.NoError(t, err) + claims = make(map[string]interface{}) + claims[claimUsernameKey] = admin.Username + claims[claimPermissionsKey] = admin.Permissions + claims[jwt.SubjectKey] = admin.GetSignature() + claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute) + token, _, err = server.tokenAuth.Encode(claims) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + req.RemoteAddr = "192.168.8.1:1234" + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + req.RemoteAddr = "172.16.1.2:1234" + ctx = jwtauth.NewContext(req.Context(), token, nil) + ctx = context.WithValue(ctx, connAddrKey, "10.9.9.9") + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.Empty(t, cookie) + + req, _ = http.NewRequest(http.MethodGet, tokenPath, nil) + req.RemoteAddr = "172.16.1.12:4567" + ctx = jwtauth.NewContext(req.Context(), token, nil) + server.checkCookieExpiration(rr, req.WithContext(ctx)) + cookie = rr.Header().Get("Set-Cookie") + assert.True(t, strings.HasPrefix(cookie, "jwt=")) + + err = dataprovider.DeleteAdmin(admin.Username) + assert.NoError(t, err) +} + +func TestGetURLParam(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, adminPwdPath, nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("val", "testuser%C3%A0") + rctx.URLParams.Add("inval", "testuser%C3%AO%GG") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + escaped := getURLParam(req, "val") + assert.Equal(t, "testuserà", escaped) + escaped = getURLParam(req, "inval") + assert.Equal(t, "testuser%C3%AO%GG", escaped) +} + +func TestChangePwdValidationErrors(t *testing.T) { + err := doChangeAdminPassword(nil, "", "", "") + require.Error(t, err) + err = doChangeAdminPassword(nil, "a", "b", "c") + require.Error(t, err) + err = doChangeAdminPassword(nil, "a", "a", "a") + require.Error(t, err) + + req, _ := http.NewRequest(http.MethodPut, adminPwdPath, nil) + err = doChangeAdminPassword(req, "currentpwd", "newpwd", "newpwd") + assert.Error(t, err) +} + +func TestRenderUnexistingFolder(t *testing.T) { + rr := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodPost, folderPath, nil) + renderFolder(rr, req, "path not mapped") + assert.Equal(t, http.StatusNotFound, rr.Code) } func TestCloseConnectionHandler(t *testing.T) { diff --git a/httpd/middleware.go b/httpd/middleware.go new file mode 100644 index 00000000..e3d0216e --- /dev/null +++ b/httpd/middleware.go @@ -0,0 +1,95 @@ +package httpd + +import ( + "context" + "net/http" + + "github.com/go-chi/jwtauth" + "github.com/lestrrat-go/jwx/jwt" + + "github.com/drakkan/sftpgo/logger" +) + +type ctxKeyConnAddr int + +const connAddrKey ctxKeyConnAddr = 0 + +func saveConnectionAddress(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), connAddrKey, r.RemoteAddr) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func jwtAuthenticator(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token, _, err := jwtauth.FromContext(r.Context()) + + if err != nil { + logger.Debug(logSender, "", "error getting jwt token: %v", err) + sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + err = jwt.Validate(token) + if token == nil || err != nil { + logger.Debug(logSender, "", "error validating jwt token: %v", err) + sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + // Token is authenticated, pass it through + next.ServeHTTP(w, r) + }) +} + +func jwtAuthenticatorWeb(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token, _, err := jwtauth.FromContext(r.Context()) + + if err != nil { + logger.Debug(logSender, "", "error getting web jwt token: %v", err) + http.Redirect(w, r, webLoginPath, http.StatusFound) + return + } + + err = jwt.Validate(token) + if token == nil || err != nil { + logger.Debug(logSender, "", "error validating web jwt token: %v", err) + http.Redirect(w, r, webLoginPath, http.StatusFound) + return + } + + // Token is authenticated, pass it through + next.ServeHTTP(w, r) + }) +} + +func checkPerm(perm string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + if isWebAdminRequest(r) { + renderBadRequestPage(w, r, err) + } else { + sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + return + } + tokenClaims := jwtTokenClaims{} + tokenClaims.Decode(claims) + + if !tokenClaims.hasPerm(perm) { + if isWebAdminRequest(r) { + renderForbiddenPage(w, r, "You don't have permission for this action") + } else { + sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden) + } + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/httpd/router.go b/httpd/router.go deleted file mode 100644 index ff17255a..00000000 --- a/httpd/router.go +++ /dev/null @@ -1,138 +0,0 @@ -package httpd - -import ( - "net/http" - "strings" - - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/go-chi/render" - - "github.com/drakkan/sftpgo/common" - "github.com/drakkan/sftpgo/logger" - "github.com/drakkan/sftpgo/metrics" - "github.com/drakkan/sftpgo/version" -) - -// GetHTTPRouter returns the configured HTTP handler -func GetHTTPRouter() http.Handler { - return router -} - -func initializeRouter(staticFilesPath string, enableWebAdmin bool) { - router = chi.NewRouter() - - router.Use(middleware.GetHead) - - router.Group(func(r chi.Router) { - r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { - render.PlainText(w, r, "ok") - }) - }) - - router.Group(func(router chi.Router) { - router.Use(middleware.RequestID) - router.Use(middleware.RealIP) - router.Use(logger.NewStructuredLogger(logger.GetLogger())) - router.Use(middleware.Recoverer) - - router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) - })) - - router.Get("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently) - }) - - router.Group(func(router chi.Router) { - router.Use(checkAuth) - - router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently) - }) - - metrics.AddMetricsEndpoint(metricsPath, router) - - router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, version.Get()) - }) - - router.Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, getServicesStatus()) - }) - - router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, common.Connections.GetStats()) - }) - - router.Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection) - router.Get(quotaScanPath, getQuotaScans) - router.Post(quotaScanPath, startQuotaScan) - router.Get(quotaScanVFolderPath, getVFolderQuotaScans) - router.Post(quotaScanVFolderPath, startVFolderQuotaScan) - router.Get(userPath, getUsers) - router.Post(userPath, addUser) - router.Get(userPath+"/{userID}", getUserByID) - router.Put(userPath+"/{userID}", updateUser) - router.Delete(userPath+"/{userID}", deleteUser) - router.Get(folderPath, getFolders) - router.Post(folderPath, addFolder) - router.Delete(folderPath, deleteFolderByPath) - router.Get(dumpDataPath, dumpData) - router.Get(loadDataPath, loadData) - router.Put(updateUsedQuotaPath, updateUserQuotaUsage) - router.Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage) - router.Get(defenderBanTime, getBanTime) - router.Get(defenderScore, getScore) - router.Post(defenderUnban, unban) - if enableWebAdmin { - router.Get(webUsersPath, handleGetWebUsers) - router.Get(webUserPath, handleWebAddUserGet) - router.Get(webUserPath+"/{userID}", handleWebUpdateUserGet) - router.Post(webUserPath, handleWebAddUserPost) - router.Post(webUserPath+"/{userID}", handleWebUpdateUserPost) - router.Get(webConnectionsPath, handleWebGetConnections) - router.Get(webFoldersPath, handleWebGetFolders) - router.Get(webFolderPath, handleWebAddFolderGet) - router.Post(webFolderPath, handleWebAddFolderPost) - router.Get(webStatusPath, handleWebGetStatus) - } - }) - - if enableWebAdmin { - router.Group(func(router chi.Router) { - compressor := middleware.NewCompressor(5) - router.Use(compressor.Handler) - fileServer(router, webStaticFilesPath, http.Dir(staticFilesPath)) - }) - } - }) -} - -func handleCloseConnection(w http.ResponseWriter, r *http.Request) { - connectionID := chi.URLParam(r, "connectionID") - if connectionID == "" { - sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest) - return - } - if common.Connections.Close(connectionID) { - sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK) - } else { - sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) - } -} - -func fileServer(r chi.Router, path string, root http.FileSystem) { - if path != "/" && path[len(path)-1] != '/' { - r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) - path += "/" - } - path += "*" - - r.Get(path, func(w http.ResponseWriter, r *http.Request) { - rctx := chi.RouteContext(r.Context()) - pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") - fs := http.StripPrefix(pathPrefix, http.FileServer(root)) - fs.ServeHTTP(w, r) - }) -} diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 7ddd8cf6..73f8ddbd 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -2,12 +2,12 @@ openapi: 3.0.3 info: title: SFTPGo description: SFTPGo REST API - version: 2.3.0 + version: 2.4.0 servers: - - url: /api/v1 + - url: /api/v2 security: - - BasicAuth: [] + - BearerAuth: [] paths: /healthz: get: @@ -26,6 +26,29 @@ paths: schema: type: string example: ok + /token: + get: + security: + - BasicAuth: [] + tags: + - token + summary: Get an access token + operationId: get_token + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/Token' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' /version: get: tags: @@ -47,7 +70,34 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /connection: + /changepwd/admin: + put: + tags: + - admins + summary: Change the password for the logged in admin + operationId: change_admin_password + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PwdChange' + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/ApiResponse' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + /connections: get: tags: - connections @@ -70,7 +120,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /connection/{connectionID}: + /connections/{connectionID}: delete: tags: - connections @@ -102,7 +152,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /defender/ban_time: + /defender/bantime: get: tags: - defender @@ -197,7 +247,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /quota_scan: + /quota-scans: get: tags: - quota @@ -255,7 +305,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /quota_update: + /quota-update: put: tags: - quota @@ -305,7 +355,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /folder_quota_update: + /folder-quota-update: put: tags: - quota @@ -355,7 +405,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /folder_quota_scan: + /folder-quota-scans: get: tags: - quota @@ -413,7 +463,7 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /folder: + /folders: get: tags: - folders @@ -484,7 +534,7 @@ paths: schema: $ref : '#/components/schemas/BaseVirtualFolder' responses: - 200: + 201: description: successful operation content: application/json: @@ -533,7 +583,193 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /user: + /admins: + get: + tags: + - admins + summary: Returns an array with one or more admins + description: For security reasons hashed passwords are omitted in the response + operationId: get_admins + parameters: + - in: query + name: offset + schema: + type: integer + minimum: 0 + default: 0 + required: false + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 500 + default: 100 + required: false + description: The maximum number of items to return. Max value is 500, default is 100 + - in: query + name: order + required: false + description: Ordering admins by username. Default ASC + schema: + type: string + enum: + - ASC + - DESC + example: ASC + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref : '#/components/schemas/Admin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + post: + tags: + - admins + summary: Adds a new admin + operationId: add_admin + requestBody: + required: true + content: + application/json: + schema: + $ref : '#/components/schemas/Admin' + responses: + 201: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/Admin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + /admins/{username}: + get: + tags: + - admins + summary: Find admin by username + description: For security reasons the hashed password is omitted in the response + operationId: get_admin_by_username + parameters: + - name: username + in: path + description: username of the admin to retrieve + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/Admin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + put: + tags: + - admins + summary: Update an existing admin + operationId: update_admin + parameters: + - name: username + in: path + description: username of the admin to update + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref : '#/components/schemas/Admin' + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/ApiResponse' + example: + message: "User updated" + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + delete: + tags: + - admins + summary: Delete an existing admin + operationId: delete_admin + parameters: + - name: username + in: path + description: username of the admin to delete + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref : '#/components/schemas/ApiResponse' + example: + message: "User deleted" + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + /users: get: tags: - users @@ -567,12 +803,6 @@ paths: - ASC - DESC example: ASC - - in: query - name: username - required: false - description: Filter by username, extact match case sensitive - schema: - type: string responses: 200: description: successful operation @@ -604,7 +834,7 @@ paths: schema: $ref : '#/components/schemas/User' responses: - 200: + 201: description: successful operation content: application/json: @@ -620,21 +850,20 @@ paths: $ref: '#/components/responses/InternalServerError' default: $ref: '#/components/responses/DefaultResponse' - /user/{userID}: + /users/{username}: get: tags: - users - summary: Find user by ID + summary: Find user by username description: For security reasons the hashed password is omitted in the response - operationId: get_user_by_id + operationId: get_user_by_username parameters: - - name: userID + - name: username in: path - description: ID of the user to retrieve + description: username of the user to retrieve required: true schema: - type: integer - format: int32 + type: string responses: 200: description: successful operation @@ -660,13 +889,12 @@ paths: summary: Update an existing user operationId: update_user parameters: - - name: userID + - name: username in: path - description: ID of the user to update + description: username of the user to update required: true schema: - type: integer - format: int32 + type: string - in: query name: disconnect schema: @@ -711,13 +939,12 @@ paths: summary: Delete an existing user operationId: delete_user parameters: - - name: userID + - name: username in: path - description: ID of the user to delete + description: username of the user to delete required: true schema: - type: integer - format: int32 + type: string responses: 200: description: successful operation @@ -949,6 +1176,22 @@ components: minItems: 1 minProperties: 1 description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required + AdminPermissions: + type: string + enum: + - '*' + - 'add_users' + - 'edit_users' + - 'del_users' + - 'view_users' + - 'view_conns' + - 'close_conns' + - 'view_status' + - 'manage_admins' + - 'quota_scans' + - 'manage_system' + - 'manage_defender' + - 'view_defender' LoginMethods: type: string enum: @@ -975,14 +1218,12 @@ components: type: array items: type: string - nullable: true description: list of, case insensitive, allowed shell like file patterns. example: [ "*.jpg", "a*b?.png" ] denied_patterns: type: array items: type: string - nullable: true description: list of, case insensitive, denied shell like file patterns. Denied patterns are evaluated before the allowed ones example: [ "*.zip" ] ExtensionsFilter: @@ -995,14 +1236,12 @@ components: type: array items: type: string - nullable: true description: list of, case insensitive, allowed files extension. Shell like expansion is not supported so you have to specify `.jpg` and not `*.jpg` example: [ ".jpg", ".png" ] denied_extensions: type: array items: type: string - nullable: true description: list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones example: [ ".zip" ] UserFilters: @@ -1012,44 +1251,37 @@ components: type: array items: type: string - nullable: true description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32" example: [ "192.0.2.0/24", "2001:db8::/32" ] denied_ip: type: array items: type: string - nullable: true description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones example: [ "172.16.0.0/16" ] denied_login_methods: type: array items: $ref: '#/components/schemas/LoginMethods' - nullable: true description: if null or empty any available login method is allowed denied_protocols: type: array items: $ref: '#/components/schemas/SupportedProtocols' - nullable: true description: if null or empty any available protocol is allowed file_patterns: type: array items: $ref: '#/components/schemas/PatternsFilter' - nullable: true description: filters based on shell like file patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed file_extensions: type: array items: $ref: '#/components/schemas/ExtensionsFilter' - nullable: true description: filters based on shell like patterns. Deprecated, use file_patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed max_upload_file_size: type: integer format: int64 - nullable: true description: maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync` description: Additional restrictions Secret: @@ -1106,7 +1338,6 @@ components: required: - bucket - region - nullable: true description: S3 Compatible Object Storage configuration details GCSConfig: type: object @@ -1118,7 +1349,6 @@ components: $ref: '#/components/schemas/Secret' automatic_credentials: type: integer - nullable: true enum: - 0 - 1 @@ -1134,7 +1364,6 @@ components: example: folder/subfolder/ required: - bucket - nullable: true description: Google Cloud Storage configuration details. The "credentials" field must be populated only when adding/updating a user. It will be always omitted, since there are sensitive data, when you search/get users AzureBlobFsConfig: type: object @@ -1171,7 +1400,6 @@ components: example: folder/subfolder/ use_emulator: type: boolean - nullable: true description: Azure Blob Storage configuration details CryptFsConfig: type: object @@ -1253,7 +1481,6 @@ components: description: Last quota update as unix timestamp in milliseconds users: type: array - nullable: true items: type: string description: list of usernames associated with this virtual folder @@ -1303,13 +1530,12 @@ components: description: expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration password: type: string - nullable: true + format: password description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users public_keys: type: array items: type: string - nullable: true description: a password or at least one public key/SSH user certificate are mandatory. home_dir: type: string @@ -1318,7 +1544,6 @@ components: type: array items: $ref: '#/components/schemas/VirtualFolder' - nullable: true description: mapping between virtual SFTPGo paths and filesystem paths outside the user home directory. Supported for local filesystem only. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself uid: type: integer @@ -1379,6 +1604,50 @@ components: additional_info: type: string description: Free form text field for external systems + AdminFilters: + type: object + properties: + allow_list: + type: array + items: + type: string + description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32" + example: [ "192.0.2.0/24", "2001:db8::/32" ] + Admin: + type: object + properties: + id: + type: integer + format: int32 + minimum: 1 + status: + type: integer + enum: + - 0 + - 1 + description: > + status: + * `0` user is disabled, login is not allowed + * `1` user is enabled + username: + type: string + description: username is unique + password: + type: string + format: password + description: Admin password. For security reasons this field is omitted when you search/get admins + email: + type: string + format: email + permissions: + type: array + items: + $ref: '#/components/schemas/AdminPermissions' + filters: + $ref: '#/components/schemas/AdminFilters' + additional_info: + type: string + description: Free form text field Transfer: type: object properties: @@ -1409,7 +1678,6 @@ components: description: unique connection identifier client_version: type: string - nullable: true description: client version remote_address: type: string @@ -1420,7 +1688,6 @@ components: description: connection time as unix timestamp in milliseconds command: type: string - nullable: true description: SSH/FTP command or WebDAV method last_activity: type: integer @@ -1436,7 +1703,6 @@ components: - DAV active_transfers: type: array - nullable: true items: $ref : '#/components/schemas/Transfer' QuotaScan: @@ -1602,6 +1868,13 @@ components: score: type: integer description: if 0 the host is not listed + PwdChange: + type: object + properties: + current_password: + type: string + new_password: + type: string ApiResponse: type: object properties: @@ -1610,7 +1883,6 @@ components: description: message, can be empty error: type: string - nullable: true description: error description if any VersionInfo: type: object @@ -1626,7 +1898,19 @@ components: items: type: string description: Features for the current build. Available features are "portable", "bolt", "mysql", "sqlite", "pgsql", "s3", "gcs", "metrics". If a feature is available it has a "+" prefix, otherwise a "-" prefix + Token: + type: object + properties: + access_token: + type: string + expires_at: + type: string + format: date-time securitySchemes: BasicAuth: type: http scheme: basic + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/httpd/server.go b/httpd/server.go new file mode 100644 index 00000000..1e61539e --- /dev/null +++ b/httpd/server.go @@ -0,0 +1,346 @@ +package httpd + +import ( + "crypto/tls" + "fmt" + "log" + "net/http" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/jwtauth" + "github.com/go-chi/render" + + "github.com/drakkan/sftpgo/common" + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/logger" + "github.com/drakkan/sftpgo/utils" + "github.com/drakkan/sftpgo/version" +) + +type httpdServer struct { + binding Binding + staticFilesPath string + enableWebAdmin bool + router *chi.Mux + tokenAuth *jwtauth.JWTAuth +} + +func newHttpdServer(bindAddress string, bindPort int, staticFilesPath string, enableWebAdmin bool) *httpdServer { + return &httpdServer{ + binding: Binding{ + Address: bindAddress, + Port: bindPort, + }, + staticFilesPath: staticFilesPath, + enableWebAdmin: enableWebAdmin, + } +} + +func (s *httpdServer) listenAndServe() error { + s.initializeRouter() + httpServer := &http.Server{ + Handler: s.router, + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 16, // 64KB + ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), + } + if certMgr != nil { + config := &tls.Config{ + GetCertificate: certMgr.GetCertificateFunc(), + MinVersion: tls.VersionTLS12, + } + httpServer.TLSConfig = config + return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender) + } + return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender) +} + +func (s *httpdServer) refreshCookie(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.checkCookieExpiration(w, r) + next.ServeHTTP(w, r) + }) +} + +func (s *httpdServer) handleWebLoginPost(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + if err := r.ParseForm(); err != nil { + renderLoginPage(w, err.Error()) + return + } + username := r.Form.Get("username") + password := r.Form.Get("password") + if username == "" || password == "" { + renderLoginPage(w, "Invalid credentials") + return + } + admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr)) + if err != nil { + renderLoginPage(w, err.Error()) + return + } + if connAddr, ok := r.Context().Value(connAddrKey).(string); ok { + if connAddr != r.RemoteAddr { + if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) { + renderLoginPage(w, fmt.Sprintf("Login from IP %v is not allowed", connAddr)) + return + } + } + } + c := jwtTokenClaims{ + Username: admin.Username, + Permissions: admin.Permissions, + Signature: admin.GetSignature(), + } + + err = c.createAndSetCookie(w, s.tokenAuth) + if err != nil { + renderLoginPage(w, err.Error()) + return + } + + http.Redirect(w, r, webUsersPath, http.StatusFound) +} + +func (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) + sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr)) + if err != nil { + w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) + sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + s.checkAddrAndSendToken(w, r, admin) +} + +func (s *httpdServer) checkAddrAndSendToken(w http.ResponseWriter, r *http.Request, admin dataprovider.Admin) { + if connAddr, ok := r.Context().Value(connAddrKey).(string); ok { + if connAddr != r.RemoteAddr { + if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) { + sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + } + } + + c := jwtTokenClaims{ + Username: admin.Username, + Permissions: admin.Permissions, + Signature: admin.GetSignature(), + } + + resp, err := c.createTokenResponse(s.tokenAuth) + + if err != nil { + sendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + render.JSON(w, r, resp) +} + +func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Request) { + token, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + return + } + tokenClaims := jwtTokenClaims{} + tokenClaims.Decode(claims) + if tokenClaims.Username == "" || tokenClaims.Signature == "" { + return + } + if time.Until(token.Expiration()) > tokenRefreshMin { + return + } + admin, err := dataprovider.AdminExists(tokenClaims.Username) + if err != nil { + return + } + if admin.Status != 1 { + logger.Debug(logSender, "", "admin %#v is disabled, unable to refresh cookie", admin.Username) + return + } + if admin.GetSignature() != tokenClaims.Signature { + logger.Debug(logSender, "", "signature mismatch for admin %#v, unable to refresh cookie", admin.Username) + return + } + if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(r.RemoteAddr)) { + logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie", admin.Username, r.RemoteAddr) + return + } + if connAddr, ok := r.Context().Value(connAddrKey).(string); ok { + if connAddr != r.RemoteAddr { + if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) { + logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie", + admin.Username, connAddr) + return + } + } + } + logger.Debug(logSender, "", "cookie refreshed for admin %#v", admin.Username) + tokenClaims.createAndSetCookie(w, s.tokenAuth) //nolint:errcheck +} + +func (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request { + token, _, err := jwtauth.FromContext(r.Context()) + if token == nil || err != nil { + _, err = r.Cookie("jwt") + if err != nil { + return r + } + token, err = jwtauth.VerifyRequest(s.tokenAuth, r, jwtauth.TokenFromCookie) + ctx := jwtauth.NewContext(r.Context(), token, err) + return r.WithContext(ctx) + } + return r +} + +func (s *httpdServer) initializeRouter() { + s.tokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil) + s.router = chi.NewRouter() + + s.router.Use(saveConnectionAddress) + s.router.Use(middleware.GetHead) + + s.router.Group(func(r chi.Router) { + r.Get(healthzPath, func(w http.ResponseWriter, r *http.Request) { + render.PlainText(w, r, "ok") + }) + }) + + s.router.Group(func(router chi.Router) { + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(logger.NewStructuredLogger(logger.GetLogger())) + router.Use(middleware.Recoverer) + + router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.enableWebAdmin && isWebAdminRequest(r) { + r = s.updateContextFromCookie(r) + renderNotFoundPage(w, r, nil) + return + } + sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) + })) + + router.Get(tokenPath, s.getToken) + + router.Group(func(router chi.Router) { + router.Use(jwtauth.Verifier(s.tokenAuth)) + router.Use(jwtAuthenticator) + + router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, version.Get()) + }) + + router.Put(adminPwdPath, changeAdminPassword) + + router.With(checkPerm(dataprovider.PermAdminViewServerStatus)). + Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, getServicesStatus()) + }) + + router.With(checkPerm(dataprovider.PermAdminViewConnections)). + Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, common.Connections.GetStats()) + }) + + router.With(checkPerm(dataprovider.PermAdminCloseConnections)). + Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getQuotaScans) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startQuotaScan) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getVFolderQuotaScans) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startVFolderQuotaScan) + router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers) + router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser) + router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) + router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser) + router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser) + router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders) + router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder) + router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath, deleteFolderByPath) + router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData) + router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData) + router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsage) + router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage) + router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime) + router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderScore, getScore) + router.With(checkPerm(dataprovider.PermAdminManageDefender)).Post(defenderUnban, unban) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin) + }) + + if s.enableWebAdmin { + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently) + }) + + router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently) + }) + + router.Get(webLoginPath, handleWebLogin) + router.Post(webLoginPath, s.handleWebLoginPost) + + router.Group(func(router chi.Router) { + router.Use(jwtauth.Verifier(s.tokenAuth)) + router.Use(jwtAuthenticatorWeb) + + router.Get(webLogoutPath, handleWebLogout) + router.With(s.refreshCookie).Get(webChangeAdminPwdPath, handleWebAdminChangePwd) + router.Post(webChangeAdminPwdPath, handleWebAdminChangePwdPost) + router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). + Get(webUsersPath, handleGetWebUsers) + router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie). + Get(webUserPath, handleWebAddUserGet) + router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie). + Get(webUserPath+"/{username}", handleWebUpdateUserGet) + router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, handleWebAddUserPost) + router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}", handleWebUpdateUserPost) + router.With(checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). + Get(webConnectionsPath, handleWebGetConnections) + router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). + Get(webFoldersPath, handleWebGetFolders) + router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie). + Get(webFolderPath, handleWebAddFolderGet) + router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webFolderPath, handleWebAddFolderPost) + router.With(checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie). + Get(webStatusPath, handleWebGetStatus) + router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminsPath, handleGetWebAdmins) + router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminPath, handleWebAddAdminGet) + router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminPath+"/{username}", handleWebUpdateAdminGet) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", handleWebUpdateAdminPost) + router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(webAdminPath+"/{username}", deleteAdmin) + router.With(checkPerm(dataprovider.PermAdminCloseConnections)). + Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection) + router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webFolderPath, deleteFolderByPath) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webScanVFolderPath, startVFolderQuotaScan) + router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webUserPath+"/{username}", deleteUser) + router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webQuotaScanPath, startQuotaScan) + }) + + router.Group(func(router chi.Router) { + compressor := middleware.NewCompressor(5) + router.Use(compressor.Handler) + fileServer(router, webStaticFilesPath, http.Dir(s.staticFilesPath)) + }) + } + }) +} diff --git a/httpd/web.go b/httpd/web.go index 82b9adfd..48bb7526 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -6,14 +6,13 @@ import ( "html/template" "io/ioutil" "net/http" + "net/url" "path" "path/filepath" "strconv" "strings" "time" - "github.com/go-chi/chi" - "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/kms" @@ -26,16 +25,23 @@ const ( templateBase = "base.html" templateUsers = "users.html" templateUser = "user.html" + templateAdmins = "admins.html" + templateAdmin = "admin.html" templateConnections = "connections.html" templateFolders = "folders.html" templateFolder = "folder.html" templateMessage = "message.html" templateStatus = "status.html" + templateLogin = "login.html" + templateChangePwd = "changepwd.html" pageUsersTitle = "Users" + pageAdminsTitle = "Admins" pageConnectionsTitle = "Connections" pageStatusTitle = "Status" pageFoldersTitle = "Folders" + pageChangePwdTitle = "Change password" page400Title = "Bad request" + page403Title = "Forbidden" page404Title = "Not found" page404Body = "The page you are looking for does not exist." page500Title = "Internal Server Error" @@ -50,24 +56,27 @@ var ( ) type basePage struct { - Title string - CurrentURL string - UsersURL string - UserURL string - APIUserURL string - APIConnectionsURL string - APIQuotaScanURL string - ConnectionsURL string - FoldersURL string - FolderURL string - APIFoldersURL string - APIFolderQuotaScanURL string - StatusURL string - UsersTitle string - ConnectionsTitle string - FoldersTitle string - StatusTitle string - Version string + Title string + CurrentURL string + UsersURL string + UserURL string + AdminsURL string + AdminURL string + QuotaScanURL string + ConnectionsURL string + FoldersURL string + FolderURL string + LogoutURL string + ChangeAdminPwdURL string + FolderQuotaScanURL string + StatusURL string + UsersTitle string + AdminsTitle string + ConnectionsTitle string + FoldersTitle string + StatusTitle string + Version string + LoggedAdmin *dataprovider.Admin } type usersPage struct { @@ -75,6 +84,11 @@ type usersPage struct { Users []dataprovider.User } +type adminsPage struct { + basePage + Admins []dataprovider.Admin +} + type foldersPage struct { basePage Folders []vfs.BaseVirtualFolder @@ -103,6 +117,18 @@ type userPage struct { IsAdd bool } +type adminPage struct { + basePage + Admin *dataprovider.Admin + Error string + IsAdd bool +} + +type changePwdPage struct { + basePage + Error string +} + type folderPage struct { basePage Folder vfs.BaseVirtualFolder @@ -115,6 +141,12 @@ type messagePage struct { Success string } +type loginPage struct { + CurrentURL string + Version string + Error string +} + func loadTemplates(templatesPath string) { usersPaths := []string{ filepath.Join(templatesPath, templateBase), @@ -124,6 +156,18 @@ func loadTemplates(templatesPath string) { filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateUser), } + adminsPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateAdmins), + } + adminPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateAdmin), + } + changePwdPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateChangePwd), + } connectionsPaths := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateConnections), @@ -144,43 +188,57 @@ func loadTemplates(templatesPath string) { filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateStatus), } + loginPath := []string{ + filepath.Join(templatesPath, templateLogin), + } usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...)) userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...)) + adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...)) + adminTmpl := utils.LoadTemplate(template.ParseFiles(adminPaths...)) connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...)) messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...)) foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...)) folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...)) statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...)) + loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...)) + changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...)) templates[templateUsers] = usersTmpl templates[templateUser] = userTmpl + templates[templateAdmins] = adminsTmpl + templates[templateAdmin] = adminTmpl templates[templateConnections] = connectionsTmpl templates[templateMessage] = messageTmpl templates[templateFolders] = foldersTmpl templates[templateFolder] = folderTmpl templates[templateStatus] = statusTmpl + templates[templateLogin] = loginTmpl + templates[templateChangePwd] = changePwdTmpl } -func getBasePageData(title, currentURL string) basePage { +func getBasePageData(title, currentURL string, r *http.Request) basePage { return basePage{ - Title: title, - CurrentURL: currentURL, - UsersURL: webUsersPath, - UserURL: webUserPath, - FoldersURL: webFoldersPath, - FolderURL: webFolderPath, - APIUserURL: userPath, - APIConnectionsURL: activeConnectionsPath, - APIQuotaScanURL: quotaScanPath, - APIFoldersURL: folderPath, - APIFolderQuotaScanURL: quotaScanVFolderPath, - ConnectionsURL: webConnectionsPath, - StatusURL: webStatusPath, - UsersTitle: pageUsersTitle, - ConnectionsTitle: pageConnectionsTitle, - FoldersTitle: pageFoldersTitle, - StatusTitle: pageStatusTitle, - Version: version.GetAsString(), + Title: title, + CurrentURL: currentURL, + UsersURL: webUsersPath, + UserURL: webUserPath, + AdminsURL: webAdminsPath, + AdminURL: webAdminPath, + FoldersURL: webFoldersPath, + FolderURL: webFolderPath, + LogoutURL: webLogoutPath, + ChangeAdminPwdURL: webChangeAdminPwdPath, + QuotaScanURL: webQuotaScanPath, + ConnectionsURL: webConnectionsPath, + StatusURL: webStatusPath, + FolderQuotaScanURL: webScanVFolderPath, + UsersTitle: pageUsersTitle, + AdminsTitle: pageAdminsTitle, + ConnectionsTitle: pageConnectionsTitle, + FoldersTitle: pageFoldersTitle, + StatusTitle: pageStatusTitle, + Version: version.GetAsString(), + LoggedAdmin: getAdminFromToken(r), } } @@ -191,16 +249,16 @@ func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) { } } -func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) { +func renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) { var errorString string - if len(body) > 0 { + if body != "" { errorString = body + " " } if err != nil { errorString += err.Error() } data := messagePage{ - basePage: getBasePageData(title, ""), + basePage: getBasePageData(title, "", r), Error: errorString, Success: message, } @@ -208,22 +266,51 @@ func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int renderTemplate(w, templateMessage, data) } -func renderInternalServerErrorPage(w http.ResponseWriter, err error) { - renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "") +func renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) { + renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "") } -func renderBadRequestPage(w http.ResponseWriter, err error) { - renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "") +func renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) { + renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "") } -func renderNotFoundPage(w http.ResponseWriter, err error) { - renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "") +func renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) { + renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body) } -func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) { +func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) { + renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "") +} + +func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) { + data := changePwdPage{ + basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r), + Error: error, + } + + renderTemplate(w, templateChangePwd, data) +} + +func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin, + error string, isAdd bool) { + currentURL := webAdminPath + if !isAdd { + currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username)) + } + data := adminPage{ + basePage: getBasePageData("Add a new user", currentURL, r), + Admin: admin, + Error: error, + IsAdd: isAdd, + } + + renderTemplate(w, templateAdmin, data) +} + +func renderAddUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) { user.SetEmptySecretsIfNil() data := userPage{ - basePage: getBasePageData("Add a new user", webUserPath), + basePage: getBasePageData("Add a new user", webUserPath, r), IsAdd: true, Error: error, User: user, @@ -236,10 +323,10 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri renderTemplate(w, templateUser, data) } -func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) { +func renderUpdateUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) { user.SetEmptySecretsIfNil() data := userPage{ - basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)), + basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)), r), IsAdd: false, Error: error, User: user, @@ -252,9 +339,9 @@ func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error s renderTemplate(w, templateUser, data) } -func renderAddFolderPage(w http.ResponseWriter, folder vfs.BaseVirtualFolder, error string) { +func renderAddFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, error string) { data := folderPage{ - basePage: getBasePageData("Add a new folder", webFolderPath), + basePage: getBasePageData("Add a new folder", webFolderPath, r), Error: error, Folder: folder, } @@ -571,6 +658,26 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er return fs, nil } +func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) { + var admin dataprovider.Admin + err := r.ParseForm() + if err != nil { + return admin, err + } + status, err := strconv.Atoi(r.Form.Get("status")) + if err != nil { + return admin, err + } + admin.Username = r.Form.Get("username") + admin.Password = r.Form.Get("password") + admin.Permissions = r.Form["permissions"] + admin.Email = r.Form.Get("email") + admin.Status = status + admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") + admin.AdditionalInfo = r.Form.Get("additional_info") + return admin, nil +} + func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { var user dataprovider.User err := r.ParseMultipartForm(maxRequestSize) @@ -649,6 +756,152 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { return user, err } +func renderLoginPage(w http.ResponseWriter, error string) { + data := loginPage{ + CurrentURL: webLoginPath, + Version: version.Get().Version, + Error: error, + } + renderTemplate(w, templateLogin, data) +} + +func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) { + renderChangePwdPage(w, r, "") +} + +func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + err := r.ParseForm() + if err != nil { + renderChangePwdPage(w, r, err.Error()) + return + } + err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"), + r.Form.Get("new_password2")) + if err != nil { + renderChangePwdPage(w, r, err.Error()) + return + } + handleWebLogout(w, r) +} + +func handleWebLogout(w http.ResponseWriter, r *http.Request) { + c := jwtTokenClaims{} + c.removeCookie(w) + + http.Redirect(w, r, webLoginPath, http.StatusFound) +} + +func handleWebLogin(w http.ResponseWriter, r *http.Request) { + renderLoginPage(w, "") +} + +func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) { + limit := defaultQueryLimit + if _, ok := r.URL.Query()["qlimit"]; ok { + var err error + limit, err = strconv.Atoi(r.URL.Query().Get("qlimit")) + if err != nil { + limit = defaultQueryLimit + } + } + admins := make([]dataprovider.Admin, 0, limit) + for { + a, err := dataprovider.GetAdmins(limit, len(admins), dataprovider.OrderASC) + if err != nil { + renderInternalServerErrorPage(w, r, err) + return + } + admins = append(admins, a...) + if len(a) < limit { + break + } + } + data := adminsPage{ + basePage: getBasePageData(pageAdminsTitle, webAdminsPath, r), + Admins: admins, + } + renderTemplate(w, templateAdmins, data) +} + +func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) { + admin := &dataprovider.Admin{Status: 1} + renderAddUpdateAdminPage(w, r, admin, "", true) +} + +func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) { + username := getURLParam(r, "username") + admin, err := dataprovider.AdminExists(username) + if err == nil { + renderAddUpdateAdminPage(w, r, &admin, "", false) + } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { + renderNotFoundPage(w, r, err) + } else { + renderInternalServerErrorPage(w, r, err) + } +} + +func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + admin, err := getAdminFromPostFields(r) + if err != nil { + renderAddUpdateAdminPage(w, r, &admin, err.Error(), true) + return + } + err = dataprovider.AddAdmin(&admin) + if err != nil { + renderAddUpdateAdminPage(w, r, &admin, err.Error(), true) + return + } + http.Redirect(w, r, webAdminsPath, http.StatusSeeOther) +} + +func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + + username := getURLParam(r, "username") + admin, err := dataprovider.AdminExists(username) + if _, ok := err.(*dataprovider.RecordNotFoundError); ok { + renderNotFoundPage(w, r, err) + return + } else if err != nil { + renderInternalServerErrorPage(w, r, err) + return + } + + updatedAdmin, err := getAdminFromPostFields(r) + if err != nil { + renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false) + return + } + updatedAdmin.ID = admin.ID + updatedAdmin.Username = admin.Username + if updatedAdmin.Password == "" { + updatedAdmin.Password = admin.Password + } + claims, err := getTokenClaims(r) + if err != nil || claims.Username == "" { + renderAddUpdateAdminPage(w, r, &updatedAdmin, fmt.Sprintf("Invalid token claims: %v", err), false) + return + } + if username == claims.Username { + if claims.isCriticalPermRemoved(updatedAdmin.Permissions) { + renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false) + return + } + if updatedAdmin.Status == 0 { + renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false) + return + } + } + err = dataprovider.UpdateAdmin(&updatedAdmin) + if err != nil { + renderAddUpdateAdminPage(w, r, &admin, err.Error(), false) + return + } + http.Redirect(w, r, webAdminsPath, http.StatusSeeOther) +} + func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { limit := defaultQueryLimit if _, ok := r.URL.Query()["qlimit"]; ok { @@ -660,9 +913,9 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { } users := make([]dataprovider.User, 0, limit) for { - u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "") + u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC) if err != nil { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) return } users = append(users, u...) @@ -671,52 +924,44 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { } } data := usersPage{ - basePage: getBasePageData(pageUsersTitle, webUsersPath), + basePage: getBasePageData(pageUsersTitle, webUsersPath, r), Users: users, } renderTemplate(w, templateUsers, data) } func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("cloneFromId") != "" { - id, err := strconv.ParseInt(r.URL.Query().Get("cloneFromId"), 10, 64) - if err != nil { - renderBadRequestPage(w, err) - return - } - user, err := dataprovider.GetUserByID(id) + if r.URL.Query().Get("cloneFrom") != "" { + username := r.URL.Query().Get("cloneFrom") + user, err := dataprovider.UserExists(username) if err == nil { user.ID = 0 user.Username = "" if err := user.DecryptSecrets(); err != nil { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) return } - renderAddUserPage(w, user, "") + renderAddUserPage(w, r, user, "") } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { - renderNotFoundPage(w, err) + renderNotFoundPage(w, r, err) } else { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) } } else { user := dataprovider.User{Status: 1} - renderAddUserPage(w, user, "") + renderAddUserPage(w, r, user, "") } } func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { - id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) - if err != nil { - renderBadRequestPage(w, err) - return - } - user, err := dataprovider.GetUserByID(id) + username := getURLParam(r, "username") + user, err := dataprovider.UserExists(username) if err == nil { - renderUpdateUserPage(w, user, "") + renderUpdateUserPage(w, r, user, "") } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { - renderNotFoundPage(w, err) + renderNotFoundPage(w, r, err) } else { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) } } @@ -724,40 +969,37 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) user, err := getUserFromPostFields(r) if err != nil { - renderAddUserPage(w, user, err.Error()) + renderAddUserPage(w, r, user, err.Error()) return } err = dataprovider.AddUser(&user) if err == nil { http.Redirect(w, r, webUsersPath, http.StatusSeeOther) } else { - renderAddUserPage(w, user, err.Error()) + renderAddUserPage(w, r, user, err.Error()) } } func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) - if err != nil { - renderBadRequestPage(w, err) - return - } - user, err := dataprovider.GetUserByID(id) + username := getURLParam(r, "username") + user, err := dataprovider.UserExists(username) if _, ok := err.(*dataprovider.RecordNotFoundError); ok { - renderNotFoundPage(w, err) + renderNotFoundPage(w, r, err) return } else if err != nil { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) return } updatedUser, err := getUserFromPostFields(r) if err != nil { - renderUpdateUserPage(w, user, err.Error()) + renderUpdateUserPage(w, r, user, err.Error()) return } updatedUser.ID = user.ID + updatedUser.Username = user.Username updatedUser.SetEmptySecretsIfNil() - if len(updatedUser.Password) == 0 { + if updatedUser.Password == "" { updatedUser.Password = user.Password } updateEncryptedSecrets(&updatedUser, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey, @@ -771,13 +1013,13 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { } http.Redirect(w, r, webUsersPath, http.StatusSeeOther) } else { - renderUpdateUserPage(w, user, err.Error()) + renderUpdateUserPage(w, r, user, err.Error()) } } func handleWebGetStatus(w http.ResponseWriter, r *http.Request) { data := statusPage{ - basePage: getBasePageData(pageStatusTitle, webStatusPath), + basePage: getBasePageData(pageStatusTitle, webStatusPath, r), Status: getServicesStatus(), } renderTemplate(w, templateStatus, data) @@ -786,14 +1028,14 @@ func handleWebGetStatus(w http.ResponseWriter, r *http.Request) { func handleWebGetConnections(w http.ResponseWriter, r *http.Request) { connectionStats := common.Connections.GetStats() data := connectionsPage{ - basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath), + basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r), Connections: connectionStats, } renderTemplate(w, templateConnections, data) } func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) { - renderAddFolderPage(w, vfs.BaseVirtualFolder{}, "") + renderAddFolderPage(w, r, vfs.BaseVirtualFolder{}, "") } func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) { @@ -801,7 +1043,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) { folder := vfs.BaseVirtualFolder{} err := r.ParseForm() if err != nil { - renderAddFolderPage(w, folder, err.Error()) + renderAddFolderPage(w, r, folder, err.Error()) return } folder.MappedPath = r.Form.Get("mapped_path") @@ -810,7 +1052,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) { if err == nil { http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) } else { - renderAddFolderPage(w, folder, err.Error()) + renderAddFolderPage(w, r, folder, err.Error()) } } @@ -827,7 +1069,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) { for { f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "") if err != nil { - renderInternalServerErrorPage(w, err) + renderInternalServerErrorPage(w, r, err) return } folders = append(folders, f...) @@ -837,7 +1079,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) { } data := foldersPage{ - basePage: getBasePageData(pageFoldersTitle, webFoldersPath), + basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r), Folders: folders, } renderTemplate(w, templateFolders, data) diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go new file mode 100644 index 00000000..6dc16bce --- /dev/null +++ b/httpdtest/httpdtest.go @@ -0,0 +1,1246 @@ +// Package httpdtest provides utilities for testing the exposed REST API. +package httpdtest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/go-chi/render" + + "github.com/drakkan/sftpgo/common" + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpclient" + "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" + "github.com/drakkan/sftpgo/utils" + "github.com/drakkan/sftpgo/version" + "github.com/drakkan/sftpgo/vfs" +) + +const ( + tokenPath = "/api/v2/token" + activeConnectionsPath = "/api/v2/connections" + quotaScanPath = "/api/v2/quota-scans" + quotaScanVFolderPath = "/api/v2/folder-quota-scans" + userPath = "/api/v2/users" + versionPath = "/api/v2/version" + folderPath = "/api/v2/folders" + serverStatusPath = "/api/v2/status" + dumpDataPath = "/api/v2/dumpdata" + loadDataPath = "/api/v2/loaddata" + updateUsedQuotaPath = "/api/v2/quota-update" + updateFolderUsedQuotaPath = "/api/v2/folder-quota-update" + defenderBanTime = "/api/v2/defender/bantime" + defenderUnban = "/api/v2/defender/unban" + defenderScore = "/api/v2/defender/score" + adminPath = "/api/v2/admins" + adminPwdPath = "/api/v2/changepwd/admin" +) + +const ( + defaultTokenAuthUser = "admin" + defaultTokenAuthPass = "password" +) + +var ( + httpBaseURL = "http://127.0.0.1:8080" + jwtToken = "" +) + +// SetBaseURL sets the base url to use for HTTP requests. +// Default URL is "http://127.0.0.1:8080" +func SetBaseURL(url string) { + httpBaseURL = url +} + +// SetJWTToken sets the JWT token to use +func SetJWTToken(token string) { + jwtToken = token +} + +func sendHTTPRequest(method, url string, body io.Reader, contentType, token string) (*http.Response, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Set("Content-Type", "application/json") + } + if token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token)) + } + return httpclient.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 + p := path.Join(paths...) + return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/")) +} + +// GetToken tries to return a JWT token +func GetToken(username, password string) (string, map[string]interface{}, error) { + req, err := http.NewRequest(http.MethodGet, buildURLRelativeToBase(tokenPath), nil) + if err != nil { + return "", nil, err + } + req.SetBasicAuth(username, password) + resp, err := httpclient.GetHTTPClient().Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + err = checkResponse(resp.StatusCode, http.StatusOK) + if err != nil { + return "", nil, err + } + responseHolder := make(map[string]interface{}) + err = render.DecodeJSON(resp.Body, &responseHolder) + if err != nil { + return "", nil, err + } + return responseHolder["access_token"].(string), responseHolder, nil +} + +func getDefaultToken() string { + if jwtToken != "" { + return jwtToken + } + token, _, err := GetToken(defaultTokenAuthUser, defaultTokenAuthPass) + if err != nil { + return "" + } + return token +} + +// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode. +func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) { + var newUser dataprovider.User + var body []byte + userAsJSON, _ := json.Marshal(user) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(userPath), bytes.NewBuffer(userAsJSON), + "application/json", getDefaultToken()) + if err != nil { + return newUser, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if expectedStatusCode != http.StatusCreated { + body, _ = getResponseBody(resp) + return newUser, body, err + } + if err == nil { + err = render.DecodeJSON(resp.Body, &newUser) + } else { + body, _ = getResponseBody(resp) + } + if err == nil { + err = checkUser(&user, &newUser) + } + return newUser, body, err +} + +// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode. +func UpdateUser(user dataprovider.User, expectedStatusCode int, disconnect string) (dataprovider.User, []byte, error) { + var newUser dataprovider.User + var body []byte + url, err := addDisconnectQueryParam(buildURLRelativeToBase(userPath, url.PathEscape(user.Username)), disconnect) + if err != nil { + return user, body, err + } + userAsJSON, _ := json.Marshal(user) + resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "application/json", + getDefaultToken()) + if err != nil { + return user, body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + err = checkResponse(resp.StatusCode, expectedStatusCode) + if expectedStatusCode != http.StatusOK { + return newUser, body, err + } + if err == nil { + newUser, body, err = GetUserByUsername(user.Username, expectedStatusCode) + } + if err == nil { + err = checkUser(&user, &newUser) + } + return newUser, body, err +} + +// 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 + resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(userPath, url.PathEscape(user.Username)), + nil, "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// GetUserByUsername gets a user by username and checks the received HTTP Status code against expectedStatusCode. +func GetUserByUsername(username string, expectedStatusCode int) (dataprovider.User, []byte, error) { + var user dataprovider.User + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(userPath, url.PathEscape(username)), + nil, "", getDefaultToken()) + if err != nil { + return user, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &user) + } else { + body, _ = getResponseBody(resp) + } + return user, body, err +} + +// GetUsers returns a list of users and checks the received HTTP Status code against expectedStatusCode. +// The number of results can be limited specifying a limit. +// Some results can be skipped specifying an offset. +func GetUsers(limit, offset int64, expectedStatusCode int) ([]dataprovider.User, []byte, error) { + var users []dataprovider.User + var body []byte + url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(userPath), limit, offset) + if err != nil { + return users, body, err + } + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return users, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &users) + } else { + body, _ = getResponseBody(resp) + } + return users, body, err +} + +// AddAdmin adds a new user and checks the received HTTP Status code against expectedStatusCode. +func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) { + var newAdmin dataprovider.Admin + var body []byte + asJSON, _ := json.Marshal(admin) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(adminPath), bytes.NewBuffer(asJSON), + "application/json", getDefaultToken()) + if err != nil { + return newAdmin, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if expectedStatusCode != http.StatusCreated { + body, _ = getResponseBody(resp) + return newAdmin, body, err + } + if err == nil { + err = render.DecodeJSON(resp.Body, &newAdmin) + } else { + body, _ = getResponseBody(resp) + } + if err == nil { + err = checkAdmin(&admin, &newAdmin) + } + return newAdmin, body, err +} + +// UpdateAdmin updates an existing admin and checks the received HTTP Status code against expectedStatusCode. +func UpdateAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) { + var newAdmin dataprovider.Admin + var body []byte + + asJSON, _ := json.Marshal(admin) + resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(adminPath, url.PathEscape(admin.Username)), + bytes.NewBuffer(asJSON), "application/json", + getDefaultToken()) + if err != nil { + return newAdmin, body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + err = checkResponse(resp.StatusCode, expectedStatusCode) + if expectedStatusCode != http.StatusOK { + return newAdmin, body, err + } + if err == nil { + newAdmin, body, err = GetAdminByUsername(admin.Username, expectedStatusCode) + } + if err == nil { + err = checkAdmin(&admin, &newAdmin) + } + return newAdmin, body, err +} + +// RemoveAdmin removes an existing admin and checks the received HTTP Status code against expectedStatusCode. +func RemoveAdmin(admin dataprovider.Admin, expectedStatusCode int) ([]byte, error) { + var body []byte + resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(adminPath, url.PathEscape(admin.Username)), + nil, "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// GetAdminByUsername gets an admin by username and checks the received HTTP Status code against expectedStatusCode. +func GetAdminByUsername(username string, expectedStatusCode int) (dataprovider.Admin, []byte, error) { + var admin dataprovider.Admin + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(adminPath, url.PathEscape(username)), + nil, "", getDefaultToken()) + if err != nil { + return admin, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &admin) + } else { + body, _ = getResponseBody(resp) + } + return admin, body, err +} + +// GetAdmins returns a list of admins and checks the received HTTP Status code against expectedStatusCode. +// The number of results can be limited specifying a limit. +// Some results can be skipped specifying an offset. +func GetAdmins(limit, offset int64, expectedStatusCode int) ([]dataprovider.Admin, []byte, error) { + var admins []dataprovider.Admin + var body []byte + url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(adminPath), limit, offset) + if err != nil { + return admins, body, err + } + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return admins, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &admins) + } else { + body, _ = getResponseBody(resp) + } + return admins, body, err +} + +// ChangeAdminPassword changes the password for an existing admin +func ChangeAdminPassword(currentPassword, newPassword string, expectedStatusCode int) ([]byte, error) { + var body []byte + + pwdChange := make(map[string]string) + pwdChange["current_password"] = currentPassword + pwdChange["new_password"] = newPassword + + asJSON, _ := json.Marshal(&pwdChange) + resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(adminPwdPath), + bytes.NewBuffer(asJSON), "application/json", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + + err = checkResponse(resp.StatusCode, expectedStatusCode) + body, _ = getResponseBody(resp) + + return body, err +} + +// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode. +func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) { + var quotaScans []common.ActiveQuotaScan + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "", getDefaultToken()) + if err != nil { + return quotaScans, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, "aScans) + } else { + body, _ = getResponseBody(resp) + } + return quotaScans, body, err +} + +// StartQuotaScan starts a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode. +func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, error) { + var body []byte + userAsJSON, _ := json.Marshal(user) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanPath), bytes.NewBuffer(userAsJSON), + "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// UpdateQuotaUsage updates the user used quota limits and checks the received HTTP Status code against expectedStatusCode. +func UpdateQuotaUsage(user dataprovider.User, mode string, expectedStatusCode int) ([]byte, error) { + var body []byte + userAsJSON, _ := json.Marshal(user) + url, err := addModeQueryParam(buildURLRelativeToBase(updateUsedQuotaPath), mode) + if err != nil { + return body, err + } + resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// GetConnections returns status and stats for active SFTP/SCP connections +func GetConnections(expectedStatusCode int) ([]common.ConnectionStatus, []byte, error) { + var connections []common.ConnectionStatus + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(activeConnectionsPath), nil, "", getDefaultToken()) + if err != nil { + return connections, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &connections) + } else { + body, _ = getResponseBody(resp) + } + return connections, body, err +} + +// CloseConnection closes an active connection identified by connectionID +func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) { + var body []byte + resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), + nil, "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + body, _ = getResponseBody(resp) + return body, err +} + +// AddFolder adds a new folder and checks the received HTTP Status code against expectedStatusCode +func AddFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) (vfs.BaseVirtualFolder, []byte, error) { + var newFolder vfs.BaseVirtualFolder + var body []byte + folderAsJSON, _ := json.Marshal(folder) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(folderPath), bytes.NewBuffer(folderAsJSON), + "application/json", getDefaultToken()) + if err != nil { + return newFolder, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if expectedStatusCode != http.StatusCreated { + body, _ = getResponseBody(resp) + return newFolder, body, err + } + if err == nil { + err = render.DecodeJSON(resp.Body, &newFolder) + } else { + body, _ = getResponseBody(resp) + } + if err == nil { + err = checkFolder(&folder, &newFolder) + } + return newFolder, body, err +} + +// RemoveFolder removes an existing user and checks the received HTTP Status code against expectedStatusCode. +func RemoveFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) { + var body []byte + baseURL := buildURLRelativeToBase(folderPath) + url, err := url.Parse(baseURL) + if err != nil { + return body, err + } + q := url.Query() + q.Add("folder_path", folder.MappedPath) + url.RawQuery = q.Encode() + resp, err := sendHTTPRequest(http.MethodDelete, url.String(), nil, "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// GetFolders returns a list of folders and checks the received HTTP Status code against expectedStatusCode. +// The number of results can be limited specifying a limit. +// Some results can be skipped specifying an offset. +// The results can be filtered specifying a folder path, the folder path filter is an exact match +func GetFolders(limit int64, offset int64, mappedPath string, expectedStatusCode int) ([]vfs.BaseVirtualFolder, []byte, error) { + var folders []vfs.BaseVirtualFolder + var body []byte + url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(folderPath), limit, offset) + if err != nil { + return folders, body, err + } + if len(mappedPath) > 0 { + q := url.Query() + q.Add("folder_path", mappedPath) + url.RawQuery = q.Encode() + } + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return folders, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &folders) + } else { + body, _ = getResponseBody(resp) + } + return folders, body, err +} + +// GetFoldersQuotaScans gets active quota scans for folders and checks the received HTTP Status code against expectedStatusCode. +func GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQuotaScan, []byte, error) { + var quotaScans []common.ActiveVirtualFolderQuotaScan + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, "", getDefaultToken()) + if err != nil { + return quotaScans, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, "aScans) + } else { + body, _ = getResponseBody(resp) + } + return quotaScans, body, err +} + +// StartFolderQuotaScan start a new quota scan for the given folder and checks the received HTTP Status code against expectedStatusCode. +func StartFolderQuotaScan(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) { + var body []byte + folderAsJSON, _ := json.Marshal(folder) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanVFolderPath), + bytes.NewBuffer(folderAsJSON), "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// UpdateFolderQuotaUsage updates the folder used quota limits and checks the received HTTP Status code against expectedStatusCode. +func UpdateFolderQuotaUsage(folder vfs.BaseVirtualFolder, mode string, expectedStatusCode int) ([]byte, error) { + var body []byte + folderAsJSON, _ := json.Marshal(folder) + url, err := addModeQueryParam(buildURLRelativeToBase(updateFolderUsedQuotaPath), mode) + if err != nil { + return body, err + } + resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(folderAsJSON), "", getDefaultToken()) + if err != nil { + return body, err + } + defer resp.Body.Close() + body, _ = getResponseBody(resp) + return body, checkResponse(resp.StatusCode, expectedStatusCode) +} + +// GetVersion returns version details +func GetVersion(expectedStatusCode int) (version.Info, []byte, error) { + var appVersion version.Info + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(versionPath), nil, "", getDefaultToken()) + if err != nil { + return appVersion, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &appVersion) + } else { + body, _ = getResponseBody(resp) + } + return appVersion, body, err +} + +// GetStatus returns the server status +func GetStatus(expectedStatusCode int) (httpd.ServicesStatus, []byte, error) { + var response httpd.ServicesStatus + var body []byte + resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(serverStatusPath), nil, "", getDefaultToken()) + if err != nil { + return response, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && (expectedStatusCode == http.StatusOK) { + err = render.DecodeJSON(resp.Body, &response) + } else { + body, _ = getResponseBody(resp) + } + return response, body, err +} + +// GetBanTime returns the ban time for the given IP address +func GetBanTime(ip string, expectedStatusCode int) (map[string]interface{}, []byte, error) { + var response map[string]interface{} + var body []byte + url, err := url.Parse(buildURLRelativeToBase(defenderBanTime)) + if err != nil { + return response, body, err + } + q := url.Query() + q.Add("ip", ip) + url.RawQuery = q.Encode() + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return response, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &response) + } else { + body, _ = getResponseBody(resp) + } + return response, body, err +} + +// GetScore returns the score for the given IP address +func GetScore(ip string, expectedStatusCode int) (map[string]interface{}, []byte, error) { + var response map[string]interface{} + var body []byte + url, err := url.Parse(buildURLRelativeToBase(defenderScore)) + if err != nil { + return response, body, err + } + q := url.Query() + q.Add("ip", ip) + url.RawQuery = q.Encode() + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return response, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &response) + } else { + body, _ = getResponseBody(resp) + } + return response, body, err +} + +// UnbanIP unbans the given IP address +func UnbanIP(ip string, expectedStatusCode int) error { + postBody := make(map[string]string) + postBody["ip"] = ip + asJSON, _ := json.Marshal(postBody) + resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(defenderUnban), bytes.NewBuffer(asJSON), + "", getDefaultToken()) + if err != nil { + return err + } + defer resp.Body.Close() + return checkResponse(resp.StatusCode, expectedStatusCode) +} + +// Dumpdata requests a backup to outputFile. +// outputFile is relative to the configured backups_path +func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]interface{}, []byte, error) { + var response map[string]interface{} + var body []byte + url, err := url.Parse(buildURLRelativeToBase(dumpDataPath)) + if err != nil { + return response, body, err + } + q := url.Query() + q.Add("output_file", outputFile) + if len(indent) > 0 { + q.Add("indent", indent) + } + url.RawQuery = q.Encode() + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return response, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &response) + } else { + body, _ = getResponseBody(resp) + } + return response, body, err +} + +// Loaddata restores a backup. +// New users are added, existing users are updated. Users will be restored one by one and the restore is stopped if a +// user cannot be added/updated, so it could happen a partial restore +func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[string]interface{}, []byte, error) { + var response map[string]interface{} + var body []byte + url, err := url.Parse(buildURLRelativeToBase(loadDataPath)) + if err != nil { + return response, body, err + } + q := url.Query() + q.Add("input_file", inputFile) + if len(scanQuota) > 0 { + q.Add("scan_quota", scanQuota) + } + if len(mode) > 0 { + q.Add("mode", mode) + } + url.RawQuery = q.Encode() + resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken()) + if err != nil { + return response, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &response) + } else { + body, _ = getResponseBody(resp) + } + return response, body, err +} + +func checkResponse(actual int, expected int) error { + if expected != actual { + return fmt.Errorf("wrong status code: got %v want %v", actual, expected) + } + return nil +} + +func getResponseBody(resp *http.Response) ([]byte, error) { + return ioutil.ReadAll(resp.Body) +} + +func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder) error { + if expected.ID <= 0 { + if actual.ID <= 0 { + return errors.New("actual folder ID must be > 0") + } + } else { + if actual.ID != expected.ID { + return errors.New("folder ID mismatch") + } + } + if expected.MappedPath != actual.MappedPath { + return errors.New("mapped path mismatch") + } + if expected.LastQuotaUpdate != actual.LastQuotaUpdate { + return errors.New("last quota update mismatch") + } + if expected.UsedQuotaSize != actual.UsedQuotaSize { + return errors.New("used quota size mismatch") + } + if expected.UsedQuotaFiles != actual.UsedQuotaFiles { + return errors.New("used quota files mismatch") + } + if len(expected.Users) != len(actual.Users) { + return errors.New("folder users mismatch") + } + for _, u := range actual.Users { + if !utils.IsStringInSlice(u, expected.Users) { + return errors.New("folder users mismatch") + } + } + return nil +} + +func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error { + if actual.Password != "" { + return errors.New("Admin password must not be visible") + } + if expected.ID <= 0 { + if actual.ID <= 0 { + return errors.New("actual admin ID must be > 0") + } + } else { + if actual.ID != expected.ID { + return errors.New("admin ID mismatch") + } + } + if expected.Username != actual.Username { + return errors.New("Username mismatch") + } + if expected.Email != actual.Email { + return errors.New("Email mismatch") + } + if expected.Status != actual.Status { + return errors.New("Status mismatch") + } + if expected.AdditionalInfo != actual.AdditionalInfo { + return errors.New("AdditionalInfo mismatch") + } + if len(expected.Permissions) != len(actual.Permissions) { + return errors.New("Permissions mismatch") + } + for _, p := range expected.Permissions { + if !utils.IsStringInSlice(p, actual.Permissions) { + return errors.New("Permissions content mismatch") + } + } + if len(expected.Filters.AllowList) != len(actual.Filters.AllowList) { + return errors.New("AllowList mismatch") + } + for _, v := range expected.Filters.AllowList { + if !utils.IsStringInSlice(v, actual.Filters.AllowList) { + return errors.New("AllowList content mismatch") + } + } + + return nil +} + +func checkUser(expected *dataprovider.User, actual *dataprovider.User) error { + if actual.Password != "" { + return errors.New("User password must not be visible") + } + if expected.ID <= 0 { + if actual.ID <= 0 { + return errors.New("actual user ID must be > 0") + } + } else { + if actual.ID != expected.ID { + return errors.New("user ID mismatch") + } + } + if len(expected.Permissions) != len(actual.Permissions) { + return errors.New("Permissions mismatch") + } + for dir, perms := range expected.Permissions { + if actualPerms, ok := actual.Permissions[dir]; ok { + for _, v := range actualPerms { + if !utils.IsStringInSlice(v, perms) { + return errors.New("Permissions contents mismatch") + } + } + } else { + return errors.New("Permissions directories mismatch") + } + } + if err := compareUserFilters(expected, actual); err != nil { + return err + } + if err := compareUserFsConfig(expected, actual); err != nil { + return err + } + if err := compareUserVirtualFolders(expected, actual); err != nil { + return err + } + return compareEqualsUserFields(expected, actual) +} + +func compareUserVirtualFolders(expected *dataprovider.User, actual *dataprovider.User) error { + if len(actual.VirtualFolders) != len(expected.VirtualFolders) { + return errors.New("Virtual folders mismatch") + } + for _, v := range actual.VirtualFolders { + found := false + for _, v1 := range expected.VirtualFolders { + if path.Clean(v.VirtualPath) == path.Clean(v1.VirtualPath) && + filepath.Clean(v.MappedPath) == filepath.Clean(v1.MappedPath) { + found = true + break + } + } + if !found { + return errors.New("Virtual folders mismatch") + } + } + return nil +} + +func compareUserFsConfig(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.FsConfig.Provider != actual.FsConfig.Provider { + return errors.New("Fs provider mismatch") + } + if err := compareS3Config(expected, actual); err != nil { + return err + } + if err := compareGCSConfig(expected, actual); err != nil { + return err + } + if err := compareAzBlobConfig(expected, actual); err != nil { + return err + } + if err := checkEncryptedSecret(expected.FsConfig.CryptConfig.Passphrase, actual.FsConfig.CryptConfig.Passphrase); err != nil { + return err + } + if err := compareSFTPFsConfig(expected, actual); err != nil { + return err + } + return nil +} + +func compareS3Config(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.FsConfig.S3Config.Bucket != actual.FsConfig.S3Config.Bucket { + return errors.New("S3 bucket mismatch") + } + if expected.FsConfig.S3Config.Region != actual.FsConfig.S3Config.Region { + return errors.New("S3 region mismatch") + } + if expected.FsConfig.S3Config.AccessKey != actual.FsConfig.S3Config.AccessKey { + return errors.New("S3 access key mismatch") + } + if err := checkEncryptedSecret(expected.FsConfig.S3Config.AccessSecret, actual.FsConfig.S3Config.AccessSecret); err != nil { + return fmt.Errorf("S3 access secret mismatch: %v", err) + } + if expected.FsConfig.S3Config.Endpoint != actual.FsConfig.S3Config.Endpoint { + return errors.New("S3 endpoint mismatch") + } + if expected.FsConfig.S3Config.StorageClass != actual.FsConfig.S3Config.StorageClass { + return errors.New("S3 storage class mismatch") + } + if expected.FsConfig.S3Config.UploadPartSize != actual.FsConfig.S3Config.UploadPartSize { + return errors.New("S3 upload part size mismatch") + } + if expected.FsConfig.S3Config.UploadConcurrency != actual.FsConfig.S3Config.UploadConcurrency { + return errors.New("S3 upload concurrency mismatch") + } + if expected.FsConfig.S3Config.KeyPrefix != actual.FsConfig.S3Config.KeyPrefix && + expected.FsConfig.S3Config.KeyPrefix+"/" != actual.FsConfig.S3Config.KeyPrefix { + return errors.New("S3 key prefix mismatch") + } + return nil +} + +func compareGCSConfig(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.FsConfig.GCSConfig.Bucket != actual.FsConfig.GCSConfig.Bucket { + return errors.New("GCS bucket mismatch") + } + if expected.FsConfig.GCSConfig.StorageClass != actual.FsConfig.GCSConfig.StorageClass { + return errors.New("GCS storage class mismatch") + } + if expected.FsConfig.GCSConfig.KeyPrefix != actual.FsConfig.GCSConfig.KeyPrefix && + expected.FsConfig.GCSConfig.KeyPrefix+"/" != actual.FsConfig.GCSConfig.KeyPrefix { + return errors.New("GCS key prefix mismatch") + } + if expected.FsConfig.GCSConfig.AutomaticCredentials != actual.FsConfig.GCSConfig.AutomaticCredentials { + return errors.New("GCS automatic credentials mismatch") + } + return nil +} + +func compareSFTPFsConfig(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.FsConfig.SFTPConfig.Endpoint != actual.FsConfig.SFTPConfig.Endpoint { + return errors.New("SFTPFs endpoint mismatch") + } + if expected.FsConfig.SFTPConfig.Username != actual.FsConfig.SFTPConfig.Username { + return errors.New("SFTPFs username mismatch") + } + if err := checkEncryptedSecret(expected.FsConfig.SFTPConfig.Password, actual.FsConfig.SFTPConfig.Password); err != nil { + return fmt.Errorf("SFTPFs password mismatch: %v", err) + } + if err := checkEncryptedSecret(expected.FsConfig.SFTPConfig.PrivateKey, actual.FsConfig.SFTPConfig.PrivateKey); err != nil { + return fmt.Errorf("SFTPFs private key mismatch: %v", err) + } + if expected.FsConfig.SFTPConfig.Prefix != actual.FsConfig.SFTPConfig.Prefix { + if expected.FsConfig.SFTPConfig.Prefix != "" && actual.FsConfig.SFTPConfig.Prefix != "/" { + return errors.New("SFTPFs prefix mismatch") + } + } + if len(expected.FsConfig.SFTPConfig.Fingerprints) != len(actual.FsConfig.SFTPConfig.Fingerprints) { + return errors.New("SFTPFs fingerprints mismatch") + } + for _, value := range actual.FsConfig.SFTPConfig.Fingerprints { + if !utils.IsStringInSlice(value, expected.FsConfig.SFTPConfig.Fingerprints) { + return errors.New("SFTPFs fingerprints mismatch") + } + } + return nil +} + +func compareAzBlobConfig(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.FsConfig.AzBlobConfig.Container != actual.FsConfig.AzBlobConfig.Container { + return errors.New("Azure Blob container mismatch") + } + if expected.FsConfig.AzBlobConfig.AccountName != actual.FsConfig.AzBlobConfig.AccountName { + return errors.New("Azure Blob account name mismatch") + } + if err := checkEncryptedSecret(expected.FsConfig.AzBlobConfig.AccountKey, actual.FsConfig.AzBlobConfig.AccountKey); err != nil { + return fmt.Errorf("Azure Blob account key mismatch: %v", err) + } + if expected.FsConfig.AzBlobConfig.Endpoint != actual.FsConfig.AzBlobConfig.Endpoint { + return errors.New("Azure Blob endpoint mismatch") + } + if expected.FsConfig.AzBlobConfig.SASURL != actual.FsConfig.AzBlobConfig.SASURL { + return errors.New("Azure Blob SASL URL mismatch") + } + if expected.FsConfig.AzBlobConfig.UploadPartSize != actual.FsConfig.AzBlobConfig.UploadPartSize { + return errors.New("Azure Blob upload part size mismatch") + } + if expected.FsConfig.AzBlobConfig.UploadConcurrency != actual.FsConfig.AzBlobConfig.UploadConcurrency { + return errors.New("Azure Blob upload concurrency mismatch") + } + if expected.FsConfig.AzBlobConfig.KeyPrefix != actual.FsConfig.AzBlobConfig.KeyPrefix && + expected.FsConfig.AzBlobConfig.KeyPrefix+"/" != actual.FsConfig.AzBlobConfig.KeyPrefix { + return errors.New("Azure Blob key prefix mismatch") + } + if expected.FsConfig.AzBlobConfig.UseEmulator != actual.FsConfig.AzBlobConfig.UseEmulator { + return errors.New("Azure Blob use emulator mismatch") + } + if expected.FsConfig.AzBlobConfig.AccessTier != actual.FsConfig.AzBlobConfig.AccessTier { + return errors.New("Azure Blob access tier mismatch") + } + return nil +} + +func areSecretEquals(expected, actual *kms.Secret) bool { + if expected == nil && actual == nil { + return true + } + if expected != nil && expected.IsEmpty() && actual == nil { + return true + } + if actual != nil && actual.IsEmpty() && expected == nil { + return true + } + return false +} + +func checkEncryptedSecret(expected, actual *kms.Secret) error { + if areSecretEquals(expected, actual) { + return nil + } + if expected == nil && actual != nil && !actual.IsEmpty() { + return errors.New("secret mismatch") + } + if actual == nil && expected != nil && !expected.IsEmpty() { + return errors.New("secret mismatch") + } + if expected.IsPlain() && actual.IsEncrypted() { + if actual.GetPayload() == "" { + return errors.New("invalid secret payload") + } + if actual.GetAdditionalData() != "" { + return errors.New("invalid secret additional data") + } + if actual.GetKey() != "" { + return errors.New("invalid secret key") + } + } else { + if expected.GetStatus() != actual.GetStatus() || expected.GetPayload() != actual.GetPayload() { + return errors.New("secret mismatch") + } + } + return nil +} + +func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User) error { + if len(expected.Filters.AllowedIP) != len(actual.Filters.AllowedIP) { + return errors.New("AllowedIP mismatch") + } + if len(expected.Filters.DeniedIP) != len(actual.Filters.DeniedIP) { + return errors.New("DeniedIP mismatch") + } + if len(expected.Filters.DeniedLoginMethods) != len(actual.Filters.DeniedLoginMethods) { + return errors.New("Denied login methods mismatch") + } + if len(expected.Filters.DeniedProtocols) != len(actual.Filters.DeniedProtocols) { + return errors.New("Denied protocols mismatch") + } + if expected.Filters.MaxUploadFileSize != actual.Filters.MaxUploadFileSize { + return errors.New("Max upload file size mismatch") + } + for _, IPMask := range expected.Filters.AllowedIP { + if !utils.IsStringInSlice(IPMask, actual.Filters.AllowedIP) { + return errors.New("AllowedIP contents mismatch") + } + } + for _, IPMask := range expected.Filters.DeniedIP { + if !utils.IsStringInSlice(IPMask, actual.Filters.DeniedIP) { + return errors.New("DeniedIP contents mismatch") + } + } + for _, method := range expected.Filters.DeniedLoginMethods { + if !utils.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) { + return errors.New("Denied login methods contents mismatch") + } + } + for _, protocol := range expected.Filters.DeniedProtocols { + if !utils.IsStringInSlice(protocol, actual.Filters.DeniedProtocols) { + return errors.New("Denied protocols contents mismatch") + } + } + if err := compareUserFileExtensionsFilters(expected, actual); err != nil { + return err + } + return compareUserFilePatternsFilters(expected, actual) +} + +func checkFilterMatch(expected []string, actual []string) bool { + if len(expected) != len(actual) { + return false + } + for _, e := range expected { + if !utils.IsStringInSlice(strings.ToLower(e), actual) { + return false + } + } + return true +} + +func compareUserFilePatternsFilters(expected *dataprovider.User, actual *dataprovider.User) error { + if len(expected.Filters.FilePatterns) != len(actual.Filters.FilePatterns) { + return errors.New("file patterns mismatch") + } + for _, f := range expected.Filters.FilePatterns { + found := false + for _, f1 := range actual.Filters.FilePatterns { + if path.Clean(f.Path) == path.Clean(f1.Path) { + if !checkFilterMatch(f.AllowedPatterns, f1.AllowedPatterns) || + !checkFilterMatch(f.DeniedPatterns, f1.DeniedPatterns) { + return errors.New("file patterns contents mismatch") + } + found = true + } + } + if !found { + return errors.New("file patterns contents mismatch") + } + } + return nil +} + +func compareUserFileExtensionsFilters(expected *dataprovider.User, actual *dataprovider.User) error { + if len(expected.Filters.FileExtensions) != len(actual.Filters.FileExtensions) { + return errors.New("file extensions mismatch") + } + for _, f := range expected.Filters.FileExtensions { + found := false + for _, f1 := range actual.Filters.FileExtensions { + if path.Clean(f.Path) == path.Clean(f1.Path) { + if !checkFilterMatch(f.AllowedExtensions, f1.AllowedExtensions) || + !checkFilterMatch(f.DeniedExtensions, f1.DeniedExtensions) { + return errors.New("file extensions contents mismatch") + } + found = true + } + } + if !found { + return errors.New("file extensions contents mismatch") + } + } + return nil +} + +func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.User) error { + if expected.Username != actual.Username { + return errors.New("Username mismatch") + } + if expected.HomeDir != actual.HomeDir { + return errors.New("HomeDir mismatch") + } + if expected.UID != actual.UID { + return errors.New("UID mismatch") + } + if expected.GID != actual.GID { + return errors.New("GID mismatch") + } + if expected.MaxSessions != actual.MaxSessions { + return errors.New("MaxSessions mismatch") + } + if expected.QuotaSize != actual.QuotaSize { + return errors.New("QuotaSize mismatch") + } + if expected.QuotaFiles != actual.QuotaFiles { + return errors.New("QuotaFiles mismatch") + } + if len(expected.Permissions) != len(actual.Permissions) { + return errors.New("Permissions mismatch") + } + if expected.UploadBandwidth != actual.UploadBandwidth { + return errors.New("UploadBandwidth mismatch") + } + if expected.DownloadBandwidth != actual.DownloadBandwidth { + return errors.New("DownloadBandwidth mismatch") + } + if expected.Status != actual.Status { + return errors.New("Status mismatch") + } + if expected.ExpirationDate != actual.ExpirationDate { + return errors.New("ExpirationDate mismatch") + } + if expected.AdditionalInfo != actual.AdditionalInfo { + return errors.New("AdditionalInfo mismatch") + } + return nil +} + +func addLimitAndOffsetQueryParams(rawurl string, limit, offset int64) (*url.URL, error) { + url, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + q := url.Query() + if limit > 0 { + q.Add("limit", strconv.FormatInt(limit, 10)) + } + if offset > 0 { + q.Add("offset", strconv.FormatInt(offset, 10)) + } + url.RawQuery = q.Encode() + return url, err +} + +func addModeQueryParam(rawurl, mode string) (*url.URL, error) { + url, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + q := url.Query() + if len(mode) > 0 { + q.Add("mode", mode) + } + url.RawQuery = q.Encode() + return url, err +} + +func addDisconnectQueryParam(rawurl, disconnect string) (*url.URL, error) { + url, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + q := url.Query() + if len(disconnect) > 0 { + q.Add("disconnect", disconnect) + } + url.RawQuery = q.Encode() + return url, err +} diff --git a/pkgs/build.sh b/pkgs/build.sh index cc0cd7ea..c9efe78d 100755 --- a/pkgs/build.sh +++ b/pkgs/build.sh @@ -18,7 +18,6 @@ cd dist BASE_DIR="../.." cp ${BASE_DIR}/sftpgo.json . -cp ${BASE_DIR}/examples/rest-api-cli/sftpgo_api_cli . sed -i "s|sftpgo.db|/var/lib/sftpgo/sftpgo.db|" sftpgo.json sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" sftpgo.json sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json @@ -61,9 +60,6 @@ contents: - src: "${BASE_DIR}/init/sftpgo.service" dst: "/lib/systemd/system/sftpgo.service" - - src: "./sftpgo_api_cli" - dst: "/usr/bin/sftpgo_api_cli" - - src: "${BASE_DIR}/templates/*" dst: "/usr/share/sftpgo/templates/" @@ -84,9 +80,6 @@ overrides: recommends: - bash-completion - mime-support - suggests: - - python3-requests - - python3-pygments scripts: postinstall: ../scripts/deb/postinstall.sh preremove: ../scripts/deb/preremove.sh @@ -95,7 +88,6 @@ overrides: recommends: - bash-completion - mailcap - # centos 8 has python3-requests, centos 6/7 python-requests scripts: postinstall: ../scripts/rpm/postinstall preremove: ../scripts/rpm/preremove @@ -112,6 +104,5 @@ tar xvf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz nfpm chmod 755 nfpm mkdir rpm ./nfpm -f nfpm.yaml pkg -p rpm -t rpm -sed -i "s|env python|env python3|" sftpgo_api_cli mkdir deb ./nfpm -f nfpm.yaml pkg -p deb -t deb diff --git a/pkgs/debian/sftpgo.install b/pkgs/debian/sftpgo.install index 9aeeaf77..4848fe37 100644 --- a/pkgs/debian/sftpgo.install +++ b/pkgs/debian/sftpgo.install @@ -1,5 +1,4 @@ sftpgo usr/bin -examples/rest-api-cli/sftpgo_api_cli usr/bin sftpgo.json etc/sftpgo init/sftpgo.service lib/systemd/system bash_completion/sftpgo usr/share/bash-completion/completions diff --git a/pkgs/debian/sftpgo.install.arm64 b/pkgs/debian/sftpgo.install.arm64 index 114c5da3..1ac40d57 100644 --- a/pkgs/debian/sftpgo.install.arm64 +++ b/pkgs/debian/sftpgo.install.arm64 @@ -1,5 +1,4 @@ arm64/sftpgo usr/bin -examples/rest-api-cli/sftpgo_api_cli usr/bin sftpgo.json etc/sftpgo init/sftpgo.service lib/systemd/system bash_completion/sftpgo usr/share/bash-completion/completions diff --git a/pkgs/debian/sftpgo.install.ppc64el b/pkgs/debian/sftpgo.install.ppc64el index 5ae72c41..4f3df6f7 100644 --- a/pkgs/debian/sftpgo.install.ppc64el +++ b/pkgs/debian/sftpgo.install.ppc64el @@ -1,5 +1,4 @@ ppc64le/sftpgo usr/bin -examples/rest-api-cli/sftpgo_api_cli usr/bin sftpgo.json etc/sftpgo init/sftpgo.service lib/systemd/system bash_completion/sftpgo usr/share/bash-completion/completions diff --git a/service/service.go b/service/service.go index 80647976..4ac0cd79 100644 --- a/service/service.go +++ b/service/service.go @@ -97,7 +97,7 @@ func (s *Service) Start() error { providerConf := config.GetProviderConf() - err = dataprovider.Initialize(providerConf, s.ConfigDir) + err = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0) if err != nil { logger.Error(logSender, "", "error initializing data provider: %v", err) logger.ErrorToConsole("error initializing data provider: %v", err) diff --git a/sftpd/cryptfs_test.go b/sftpd/cryptfs_test.go index 64a78e36..0a5b21f4 100644 --- a/sftpd/cryptfs_test.go +++ b/sftpd/cryptfs_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/drakkan/sftpgo/dataprovider" - "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/vfs" ) @@ -27,7 +27,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) { usePubKey := false u := getTestUserWithCryptFs(usePubKey) u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -56,7 +56,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, encryptedFileSize, info.Size()) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -73,7 +73,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) { assert.NoError(t, err) _, err = client.Lstat(testFileName) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize) @@ -82,7 +82,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -93,7 +93,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) { usePubKey := false u := getTestUserWithCryptFs(usePubKey) u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -113,7 +113,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) { assert.NoError(t, err) } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -122,7 +122,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) { func TestEmptyFile(t *testing.T) { usePubKey := true u := getTestUserWithCryptFs(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -152,7 +152,7 @@ func TestEmptyFile(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -162,7 +162,7 @@ func TestUploadResumeCryptFs(t *testing.T) { // upload resume is not supported usePubKey := true u := getTestUserWithCryptFs(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -183,7 +183,7 @@ func TestUploadResumeCryptFs(t *testing.T) { assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED") } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -193,7 +193,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { usePubKey := false u := getTestUserWithCryptFs(usePubKey) u.QuotaFiles = 1000 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -213,7 +213,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { // now replace the same file, the quota must not change err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -221,7 +221,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { // replacing a symlink is like uploading a new file err = client.Symlink(testFileName, testFileName+".link") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -229,14 +229,14 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { expectedQuotaSize = expectedQuotaSize + encryptedFileSize err = sftpUploadFile(testFilePath, testFileName+".link", testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) } // now set a quota size restriction and upload the same file, upload should fail for space limit exceeded user.QuotaSize = encryptedFileSize*2 - 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -246,7 +246,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { err = client.Remove(testFileName) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -256,7 +256,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) { func TestQuotaScanCryptFs(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated) assert.NoError(t, err) testFileSize := int64(65535) encryptedFileSize, err := getEncryptedFileSize(testFileSize) @@ -274,25 +274,25 @@ func TestQuotaScanCryptFs(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) // create user with the same home dir, so there is at least an untracked file - user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartQuotaScan(user, http.StatusAccepted) + _, err = httpdtest.StartQuotaScan(user, http.StatusAccepted) assert.NoError(t, err) assert.Eventually(t, func() bool { - scans, _, err := httpd.GetQuotaScans(http.StatusOK) + scans, _, err := httpdtest.GetQuotaScans(http.StatusOK) if err == nil { return len(scans) == 0 } return false }, 1*time.Second, 50*time.Millisecond) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -300,7 +300,7 @@ func TestQuotaScanCryptFs(t *testing.T) { func TestGetMimeTypeCryptFs(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -325,7 +325,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) { assert.Equal(t, "text/plain; charset=utf-8", mime) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -334,7 +334,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) { func TestTruncate(t *testing.T) { // truncate is not supported usePubKey := true - user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -352,7 +352,7 @@ func TestTruncate(t *testing.T) { assert.Error(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -365,7 +365,7 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) { usePubKey := true u := getTestUserWithCryptFs(usePubKey) u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(131074) @@ -395,20 +395,20 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) { } err = os.Remove(localPath) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) // now overwrite the existing file err = scpUpload(testFilePath, remoteUpPath, false, false) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -422,7 +422,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) { } usePubKey := true u := getTestUserWithCryptFs(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testBaseDirName := "atestdir" testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName) @@ -467,7 +467,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } diff --git a/sftpd/scp.go b/sftpd/scp.go index fa4eb0fb..b7980fcd 100644 --- a/sftpd/scp.go +++ b/sftpd/scp.go @@ -152,6 +152,7 @@ func (c *scpCommand) getUploadFileData(sizeToRead int64, transfer *transfer) err } if sizeToRead > 0 { + // we could replace this method with io.CopyN implementing "Write" method in transfer struct remaining := sizeToRead buf := make([]byte, int64(math.Min(32768, float64(sizeToRead)))) for { @@ -420,6 +421,7 @@ func (c *scpCommand) sendDownloadFileData(filePath string, stat os.FileInfo, tra return err } + // we could replace this method with io.CopyN implementing "Read" method in transfer struct buf := make([]byte, 32768) var n int for { diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 6fa91914..b9f29c5c 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -40,7 +40,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" - "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" @@ -177,7 +177,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) if err != nil { logger.ErrorToConsole("error initializing data provider: %v", err) os.Exit(1) @@ -342,7 +342,7 @@ func TestBasicSFTPHandling(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -362,7 +362,7 @@ func TestBasicSFTPHandling(t *testing.T) { localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -370,7 +370,7 @@ func TestBasicSFTPHandling(t *testing.T) { assert.NoError(t, err) _, err = client.Lstat(testFileName) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize) @@ -379,7 +379,7 @@ func TestBasicSFTPHandling(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -389,11 +389,11 @@ func TestBasicSFTPHandling(t *testing.T) { func TestBasicSFTPFsHandling(t *testing.T) { usePubKey := true - baseUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + baseUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) u := getTestSFTPUser(usePubKey) u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -412,7 +412,7 @@ func TestBasicSFTPFsHandling(t *testing.T) { localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -434,7 +434,7 @@ func TestBasicSFTPFsHandling(t *testing.T) { assert.NoError(t, err) _, err = client.Lstat(testFileName) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) @@ -449,7 +449,7 @@ func TestBasicSFTPFsHandling(t *testing.T) { assert.False(t, contents[0].IsDir()) assert.True(t, contents[0].Mode().IsRegular()) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -460,9 +460,9 @@ func TestBasicSFTPFsHandling(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(baseUser, http.StatusOK) + _, err = httpdtest.RemoveUser(baseUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(baseUser.GetHomeDir()) assert.NoError(t, err) @@ -486,7 +486,7 @@ func TestDefender(t *testing.T) { assert.NoError(t, err) usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -505,7 +505,7 @@ func TestDefender(t *testing.T) { _, err = getSftpClient(user, usePubKey) assert.Error(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -518,11 +518,11 @@ func TestOpenReadWrite(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaSize = 6553600 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaSize = 6553600 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -558,9 +558,9 @@ func TestOpenReadWrite(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -571,11 +571,11 @@ func TestOpenReadWritePerm(t *testing.T) { u := getTestUser(usePubKey) // we cannot read inside "/sub", rename is needed otherwise the atomic upload will fail for the sftpfs user u.Permissions["/sub"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermRename} - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.Permissions["/sub"] = []string{dataprovider.PermUpload, dataprovider.PermListItems} - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -604,9 +604,9 @@ func TestOpenReadWritePerm(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -617,7 +617,7 @@ func TestConcurrency(t *testing.T) { numLogins := 50 u := getTestUser(usePubKey) u.QuotaFiles = numLogins + 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) var wg sync.WaitGroup testFilePath := filepath.Join(homeBasePath, testFileName) @@ -691,7 +691,7 @@ func TestConcurrency(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -699,7 +699,7 @@ func TestConcurrency(t *testing.T) { func TestProxyProtocol(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) // remove the home dir to test auto creation err = os.RemoveAll(user.HomeDir) @@ -713,7 +713,7 @@ func TestProxyProtocol(t *testing.T) { if !assert.Error(t, err) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -722,12 +722,12 @@ func TestProxyProtocol(t *testing.T) { func TestUploadResume(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -764,9 +764,9 @@ func TestUploadResume(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -774,7 +774,7 @@ func TestUploadResume(t *testing.T) { func TestDirCommands(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) // remove the home dir to test auto creation err = os.RemoveAll(user.HomeDir) @@ -814,7 +814,7 @@ func TestDirCommands(t *testing.T) { err = client.RemoveDirectory("/test") assert.Error(t, err, "remove missing path must fail") } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -822,7 +822,7 @@ func TestDirCommands(t *testing.T) { func TestRemove(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -852,7 +852,7 @@ func TestRemove(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -860,7 +860,7 @@ func TestRemove(t *testing.T) { func TestLink(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -887,7 +887,7 @@ func TestLink(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -895,9 +895,9 @@ func TestLink(t *testing.T) { func TestStat(t *testing.T) { usePubKey := false - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -987,9 +987,9 @@ func TestStat(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1000,9 +1000,9 @@ func TestStatChownChmod(t *testing.T) { t.Skip("chown is not supported on Windows, chmod is partially supported") } usePubKey := true - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -1036,9 +1036,9 @@ func TestStatChownChmod(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1046,9 +1046,9 @@ func TestStatChownChmod(t *testing.T) { func TestSFTPFsLoginWrongFingerprint(t *testing.T) { usePubKey := true - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(sftpUser, usePubKey) @@ -1059,7 +1059,7 @@ func TestSFTPFsLoginWrongFingerprint(t *testing.T) { } sftpUser.FsConfig.SFTPConfig.Fingerprints = append(sftpUser.FsConfig.SFTPConfig.Fingerprints, "wrong") - _, _, err = httpd.UpdateUser(sftpUser, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(sftpUser, usePubKey) if assert.NoError(t, err) { @@ -1069,14 +1069,14 @@ func TestSFTPFsLoginWrongFingerprint(t *testing.T) { } sftpUser.FsConfig.SFTPConfig.Fingerprints = []string{"wrong"} - _, _, err = httpd.UpdateUser(sftpUser, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, "") assert.NoError(t, err) _, err = getSftpClient(sftpUser, usePubKey) assert.Error(t, err) - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1084,9 +1084,9 @@ func TestSFTPFsLoginWrongFingerprint(t *testing.T) { func TestChtimes(t *testing.T) { usePubKey := false - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -1124,9 +1124,9 @@ func TestChtimes(t *testing.T) { } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1135,7 +1135,7 @@ func TestChtimes(t *testing.T) { // basic tests to verify virtual chroot, should be improved to cover more cases ... func TestEscapeHomeDir(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) dirOutsideHome := filepath.Join(homeBasePath, defaultUsername+"1", "dir") err = os.MkdirAll(dirOutsideHome, os.ModePerm) @@ -1186,7 +1186,7 @@ func TestEscapeHomeDir(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1196,7 +1196,7 @@ func TestEscapeHomeDir(t *testing.T) { func TestEscapeSFTPFsPrefix(t *testing.T) { usePubKey := false - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) u := getTestSFTPUser(usePubKey) sftpPrefix := "/prefix" @@ -1205,7 +1205,7 @@ func TestEscapeSFTPFsPrefix(t *testing.T) { out1 := "out1" out2 := "out2" u.FsConfig.SFTPConfig.Prefix = sftpPrefix - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(localUser, usePubKey) if assert.NoError(t, err) { @@ -1238,9 +1238,9 @@ func TestEscapeSFTPFsPrefix(t *testing.T) { assert.Error(t, err) } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1248,9 +1248,9 @@ func TestEscapeSFTPFsPrefix(t *testing.T) { func TestGetMimeTypeSFTPFs(t *testing.T) { usePubKey := false - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(localUser, usePubKey) if assert.NoError(t, err) { @@ -1276,9 +1276,9 @@ func TestGetMimeTypeSFTPFs(t *testing.T) { assert.Equal(t, "text/plain; charset=utf-8", mime) } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1288,7 +1288,7 @@ func TestHomeSpecialChars(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.HomeDir = filepath.Join(homeBasePath, "abc açà#&%lk") - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -1308,7 +1308,7 @@ func TestHomeSpecialChars(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1317,13 +1317,13 @@ func TestHomeSpecialChars(t *testing.T) { func TestLogin(t *testing.T) { u := getTestUser(false) u.PublicKeys = []string{testPubKey} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, false) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin) } @@ -1340,7 +1340,7 @@ func TestLogin(t *testing.T) { // testPubKey1 is not authorized user.PublicKeys = []string{testPubKey1} user.Password = "" - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, true) if !assert.Error(t, err, "login with invalid public key must fail") { @@ -1349,14 +1349,14 @@ func TestLogin(t *testing.T) { // login a user with multiple public keys, only the second one is valid user.PublicKeys = []string{testPubKey1, testPubKey} user.Password = "" - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, true) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1365,7 +1365,7 @@ func TestLogin(t *testing.T) { func TestLoginUserCert(t *testing.T) { u := getTestUser(true) u.PublicKeys = []string{testCertValid, testCertUntrustedCA, testHostCert, testCertOtherSourceAddress, testCertExpired} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) // try login using a cert signed from a trusted CA signer, err := getSignerForUserCert([]byte(testCertValid)) @@ -1403,14 +1403,14 @@ func TestLoginUserCert(t *testing.T) { if !assert.Error(t, err) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) // now login with a username not in the set of valid principals for the given certificate u.Username += "1" - user, _, err = httpd.AddUser(u, http.StatusOK) + user, _, err = httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) signer, err = getSignerForUserCert([]byte(testCertValid)) @@ -1420,7 +1420,7 @@ func TestLoginUserCert(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1435,7 +1435,7 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) { dataprovider.LoginMethodPassword, dataprovider.SSHLoginMethodKeyboardInteractive, }...) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, true) if !assert.Error(t, err, "login with public key is disallowed and must fail") { @@ -1465,7 +1465,7 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) { } _, err = getCustomAuthSftpClient(user, authMethods, "") assert.Error(t, err, "multi step auth login with wrong order must fail") - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1483,7 +1483,7 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) { dataprovider.LoginMethodPassword, dataprovider.SSHLoginMethodKeyboardInteractive, }...) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), os.ModePerm) assert.NoError(t, err) @@ -1532,7 +1532,7 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) { dataprovider.LoginMethodPassword, dataprovider.SSHLoginMethodKeyboardInteractive, }...) - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) _, err = getCustomAuthSftpClient(user, authMethods, sftpSrvAddr2222) assert.Error(t, err) @@ -1542,7 +1542,7 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1558,7 +1558,7 @@ func TestMultiStepLoginCertAndPwd(t *testing.T) { dataprovider.LoginMethodPassword, dataprovider.SSHLoginMethodKeyboardInteractive, }...) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) signer, err := getSignerForUserCert([]byte(testCertValid)) assert.NoError(t, err) @@ -1583,7 +1583,7 @@ func TestMultiStepLoginCertAndPwd(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1591,25 +1591,25 @@ func TestMultiStepLoginCertAndPwd(t *testing.T) { func TestLoginUserStatus(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin) } user.Status = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if !assert.Error(t, err, "login for a disabled user must fail") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1617,32 +1617,32 @@ func TestLoginUserStatus(t *testing.T) { func TestLoginUserExpiration(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin) } user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) - 120000 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if !assert.Error(t, err, "login for an expired user must fail") { client.Close() } user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) + 120000 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1664,7 +1664,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) - err := dataprovider.Initialize(providerConf, configDir) + err := dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) if _, err = os.Stat(credentialsFile); err == nil { @@ -1672,7 +1672,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, os.Remove(credentialsFile)) } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) @@ -1686,7 +1686,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { defer client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1694,7 +1694,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) assert.NoError(t, config.LoadConfig(configDir, "")) providerConf = config.GetProviderConf() - assert.NoError(t, dataprovider.Initialize(providerConf, configDir)) + assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true)) } func TestLoginInvalidFs(t *testing.T) { @@ -1703,7 +1703,7 @@ func TestLoginInvalidFs(t *testing.T) { u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) providerConf := config.GetProviderConf() @@ -1721,7 +1721,7 @@ func TestLoginInvalidFs(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1730,14 +1730,14 @@ func TestLoginInvalidFs(t *testing.T) { func TestDeniedProtocols(t *testing.T) { u := getTestUser(true) u.Filters.DeniedProtocols = []string{common.ProtocolSSH} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, true) if !assert.Error(t, err, "SSH protocol is disabled, authentication must fail") { client.Close() } user.Filters.DeniedProtocols = []string{common.ProtocolFTP, common.ProtocolWebDAV} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, true) if assert.NoError(t, err) { @@ -1745,7 +1745,7 @@ func TestDeniedProtocols(t *testing.T) { assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1754,14 +1754,14 @@ func TestDeniedProtocols(t *testing.T) { func TestDeniedLoginMethods(t *testing.T) { u := getTestUser(true) u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.LoginMethodPassword} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, true) if !assert.Error(t, err, "public key login is disabled, authentication must fail") { client.Close() } user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.LoginMethodPassword} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, true) if assert.NoError(t, err) { @@ -1769,7 +1769,7 @@ func TestDeniedLoginMethods(t *testing.T) { assert.NoError(t, checkBasicSFTP(client)) } user.Password = defaultPassword - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, false) @@ -1777,14 +1777,14 @@ func TestDeniedLoginMethods(t *testing.T) { client.Close() } user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPublicKey} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, false) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1795,18 +1795,18 @@ func TestLoginWithIPFilters(t *testing.T) { u := getTestUser(usePubKey) u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"} u.Filters.AllowedIP = []string{} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin) } user.Filters.AllowedIP = []string{"127.0.0.0/8"} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -1814,13 +1814,13 @@ func TestLoginWithIPFilters(t *testing.T) { assert.NoError(t, checkBasicSFTP(client)) } user.Filters.AllowedIP = []string{"172.19.0.0/16"} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if !assert.Error(t, err, "login from an not allowed IP must fail") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1828,19 +1828,19 @@ func TestLoginWithIPFilters(t *testing.T) { func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) user.Password = "" user.PublicKeys = []string{} // password and public key should remain unchanged - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1848,19 +1848,19 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) { usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) user.Password = "" user.PublicKeys = []string{} // password and public key should remain unchanged - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1870,7 +1870,7 @@ func TestLoginKeyboardInteractiveAuth(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") } - user, _, err := httpd.AddUser(getTestUser(false), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(false), http.StatusCreated) assert.NoError(t, err) err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), os.ModePerm) assert.NoError(t, err) @@ -1880,14 +1880,14 @@ func TestLoginKeyboardInteractiveAuth(t *testing.T) { assert.NoError(t, checkBasicSFTP(client)) } user.Status = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getKeyboardInteractiveSftpClient(user, []string{"1", "2"}) if !assert.Error(t, err, "keyboard interactive auth must fail the user is disabled") { client.Close() } user.Status = 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, -1), os.ModePerm) assert.NoError(t, err) @@ -1907,7 +1907,7 @@ func TestLoginKeyboardInteractiveAuth(t *testing.T) { if !assert.Error(t, err, "keyboard interactive auth must fail the script returned bad json") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1927,10 +1927,10 @@ func TestPreLoginScript(t *testing.T) { err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm) assert.NoError(t, err) providerConf.PreLoginHook = preLoginPath - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(u, usePubKey) if assert.NoError(t, err) { @@ -1950,7 +1950,7 @@ func TestPreLoginScript(t *testing.T) { if !assert.Error(t, err, "pre-login script returned a disabled user, login must fail") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1959,7 +1959,7 @@ func TestPreLoginScript(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(preLoginPath) assert.NoError(t, err) @@ -1979,22 +1979,19 @@ func TestPreLoginUserCreation(t *testing.T) { err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm) assert.NoError(t, err) providerConf.PreLoginHook = preLoginPath - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound) assert.NoError(t, err) - assert.Equal(t, 0, len(users)) client, err := getSftpClient(u, usePubKey) if assert.NoError(t, err) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user := users[0] - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2003,7 +2000,7 @@ func TestPreLoginUserCreation(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(preLoginPath) assert.NoError(t, err) @@ -2017,7 +2014,7 @@ func TestPostConnectHook(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm) assert.NoError(t, err) @@ -2034,7 +2031,7 @@ func TestPostConnectHook(t *testing.T) { client.Close() } - common.Config.PostConnectHook = "http://127.0.0.1:8080/api/v1/version" + common.Config.PostConnectHook = "http://127.0.0.1:8080/healthz" client, err = getSftpClient(u, usePubKey) if assert.NoError(t, err) { @@ -2049,7 +2046,7 @@ func TestPostConnectHook(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2073,9 +2070,9 @@ func TestCheckPwdHook(t *testing.T) { assert.NoError(t, err) providerConf.CheckPasswordHook = checkPwdPath providerConf.CheckPasswordScope = 1 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) @@ -2101,15 +2098,15 @@ func TestCheckPwdHook(t *testing.T) { assert.NoError(t, err) client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) providerConf.CheckPasswordScope = 6 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - user, _, err = httpd.AddUser(u, http.StatusOK) + user, _, err = httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.Password = defaultPassword + "1" client, err = getSftpClient(user, usePubKey) @@ -2117,7 +2114,7 @@ func TestCheckPwdHook(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2127,7 +2124,7 @@ func TestCheckPwdHook(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(checkPwdPath) assert.NoError(t, err) @@ -2149,7 +2146,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) testFileSize := int64(65535) @@ -2179,26 +2176,23 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { defer client.Close() assert.NoError(t, checkBasicSFTP(client)) } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - if assert.Equal(t, 1, len(users)) { - user := users[0] - assert.Equal(t, 0, len(user.PublicKeys)) - assert.Equal(t, testFileSize, user.UsedQuotaSize) - assert.Equal(t, 1, user.UsedQuotaFiles) + assert.Equal(t, 0, len(user.PublicKeys)) + assert.Equal(t, testFileSize, user.UsedQuotaSize) + assert.Equal(t, 1, user.UsedQuotaFiles) - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - err = os.RemoveAll(user.GetHomeDir()) - assert.NoError(t, err) - } + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -2221,7 +2215,7 @@ func TestExternalAuthDifferentUsername(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) // the user logins using "defaultUsername" and the external auth returns "extAuthUsername" @@ -2246,30 +2240,26 @@ func TestExternalAuthDifferentUsername(t *testing.T) { assert.NoError(t, err) } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + _, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound) assert.NoError(t, err) - assert.Equal(t, 0, len(users)) - users, _, err = httpd.GetUsers(0, 0, extAuthUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(extAuthUsername, http.StatusOK) assert.NoError(t, err) - if assert.Equal(t, 1, len(users)) { - user := users[0] - assert.Equal(t, 0, len(user.PublicKeys)) - assert.Equal(t, testFileSize, user.UsedQuotaSize) - assert.Equal(t, 1, user.UsedQuotaFiles) + assert.Equal(t, 0, len(user.PublicKeys)) + assert.Equal(t, testFileSize, user.UsedQuotaSize) + assert.Equal(t, 1, user.UsedQuotaFiles) - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - err = os.RemoveAll(user.GetHomeDir()) - assert.NoError(t, err) - } + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -2306,7 +2296,7 @@ func TestLoginExternalAuth(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = authScope - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) client, err := getSftpClient(u, usePubKey) @@ -2325,30 +2315,27 @@ func TestLoginExternalAuth(t *testing.T) { if !assert.Error(t, err, "external auth login with valid user but invalid auth scope must fail") { client.Close() } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - if assert.Len(t, users, 1) { - user := users[0] - if assert.Len(t, user.VirtualFolders, 1) { - folder := user.VirtualFolders[0] - assert.Equal(t, mappedPath, folder.MappedPath) - assert.Equal(t, 1+authScope, folder.QuotaFiles) - assert.Equal(t, 10+int64(authScope), folder.QuotaSize) - } - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - err = os.RemoveAll(user.GetHomeDir()) - assert.NoError(t, err) + if assert.Len(t, user.VirtualFolders, 1) { + folder := user.VirtualFolders[0] + assert.Equal(t, mappedPath, folder.MappedPath) + assert.Equal(t, 1+authScope, folder.QuotaFiles) + assert.Equal(t, 10+int64(authScope), folder.QuotaSize) } + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -2370,7 +2357,7 @@ func TestLoginExternalAuthInteractive(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 4 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), os.ModePerm) @@ -2391,11 +2378,9 @@ func TestLoginExternalAuthInteractive(t *testing.T) { if !assert.Error(t, err, "external auth login with valid user but invalid auth scope must fail") { client.Close() } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user := users[0] - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2405,7 +2390,7 @@ func TestLoginExternalAuthInteractive(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -2426,7 +2411,7 @@ func TestLoginExternalAuthErrors(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) client, err := getSftpClient(u, usePubKey) @@ -2440,16 +2425,15 @@ func TestLoginExternalAuthErrors(t *testing.T) { if !assert.Error(t, err, "login must fail, external auth returns a non json response") { client.Close() } - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + _, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound) assert.NoError(t, err) - assert.Equal(t, 0, len(users)) err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -2462,12 +2446,12 @@ func TestQuotaDisabledError(t *testing.T) { assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.TrackQuota = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) usePubKey := false u := getTestUser(usePubKey) u.QuotaFiles = 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2485,7 +2469,7 @@ func TestQuotaDisabledError(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2495,7 +2479,7 @@ func TestQuotaDisabledError(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } @@ -2505,7 +2489,7 @@ func TestMaxConnections(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2516,7 +2500,7 @@ func TestMaxConnections(t *testing.T) { c.Close() } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2529,7 +2513,7 @@ func TestMaxSessions(t *testing.T) { u := getTestUser(usePubKey) u.Username += "1" u.MaxSessions = 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2540,7 +2524,7 @@ func TestMaxSessions(t *testing.T) { c.Close() } } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2550,11 +2534,11 @@ func TestQuotaFileReplace(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaFiles = 1000 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaFiles = 1000 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(65535) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -2571,7 +2555,7 @@ func TestQuotaFileReplace(t *testing.T) { // now replace the same file, the quota must not change err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -2579,7 +2563,7 @@ func TestQuotaFileReplace(t *testing.T) { // replacing a symlink is like uploading a new file err = client.Symlink(testFileName, testFileName+".link") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -2587,14 +2571,14 @@ func TestQuotaFileReplace(t *testing.T) { expectedQuotaSize += testFileSize err = sftpUploadFile(testFilePath, testFileName+".link", testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) } // now set a quota size restriction and upload the same file, upload should fail for space limit exceeded user.QuotaSize = testFileSize*2 - 1 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2609,16 +2593,16 @@ func TestQuotaFileReplace(t *testing.T) { assert.NoError(t, err) user.UsedQuotaFiles = 0 user.UsedQuotaSize = 0 - _, err = httpd.UpdateQuotaUsage(user, "reset", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK) assert.NoError(t, err) user.QuotaSize = 0 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -2630,11 +2614,11 @@ func TestQuotaRename(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.QuotaFiles = 1000 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaFiles = 1000 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(65535) testFileSize1 := int64(65537) @@ -2655,13 +2639,13 @@ func TestQuotaRename(t *testing.T) { assert.NoError(t, err) err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) err = client.Rename(testFileName1, testFileName+".rename") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) @@ -2680,7 +2664,7 @@ func TestQuotaRename(t *testing.T) { assert.NoError(t, err) err = client.Rename("testdir", "testdir1") assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) @@ -2691,13 +2675,13 @@ func TestQuotaRename(t *testing.T) { assert.NoError(t, err) err = sftpUploadFile(testFilePath1, path.Join(testDir, testFileName1), testFileSize1, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 4, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2+testFileSize1*2, user.UsedQuotaSize) err = client.Rename(testDir, testDir+"1") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 4, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2+testFileSize1*2, user.UsedQuotaSize) @@ -2706,14 +2690,14 @@ func TestQuotaRename(t *testing.T) { assert.NoError(t, err) user.UsedQuotaFiles = 0 user.UsedQuotaSize = 0 - _, err = httpd.UpdateQuotaUsage(user, "reset", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK) assert.NoError(t, err) } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -2725,7 +2709,7 @@ func TestQuotaRename(t *testing.T) { func TestQuotaScan(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) testFileSize := int64(65535) expectedQuotaSize := user.UsedQuotaSize + testFileSize @@ -2741,25 +2725,25 @@ func TestQuotaScan(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) // create user with the same home dir, so there is at least an untracked file - user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartQuotaScan(user, http.StatusAccepted) + _, err = httpdtest.StartQuotaScan(user, http.StatusAccepted) assert.NoError(t, err) assert.Eventually(t, func() bool { - scans, _, err := httpd.GetQuotaScans(http.StatusOK) + scans, _, err := httpdtest.GetQuotaScans(http.StatusOK) if err == nil { return len(scans) == 0 } return false }, 1*time.Second, 50*time.Millisecond) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2780,11 +2764,11 @@ func TestQuotaLimits(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaFiles = 1 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaFiles = 1 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(65535) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -2816,7 +2800,7 @@ func TestQuotaLimits(t *testing.T) { // test quota size user.QuotaSize = testFileSize - 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2831,7 +2815,7 @@ func TestQuotaLimits(t *testing.T) { // now test quota limits while uploading the current file, we have 1 bytes remaining user.QuotaSize = testFileSize + 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2857,7 +2841,7 @@ func TestQuotaLimits(t *testing.T) { assert.NoError(t, err) user.UsedQuotaFiles = 0 user.UsedQuotaSize = 0 - _, err = httpd.UpdateQuotaUsage(user, "reset", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK) assert.NoError(t, err) } } @@ -2867,9 +2851,9 @@ func TestQuotaLimits(t *testing.T) { assert.NoError(t, err) err = os.Remove(testFilePath2) assert.NoError(t, err) - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -2880,7 +2864,7 @@ func TestUploadMaxSize(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Filters.MaxUploadFileSize = testFileSize + 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) err = createTestFile(testFilePath, testFileSize) @@ -2907,7 +2891,7 @@ func TestUploadMaxSize(t *testing.T) { assert.NoError(t, err) err = os.Remove(testFilePath1) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2919,12 +2903,12 @@ func TestBandwidthAndConnections(t *testing.T) { u := getTestUser(usePubKey) u.UploadBandwidth = 120 u.DownloadBandwidth = 100 - wantedUploadElapsed := 1000 * (testFileSize / 1000) / u.UploadBandwidth - wantedDownloadElapsed := 1000 * (testFileSize / 1000) / u.DownloadBandwidth + wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth + wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth // 100 ms tolerance wantedUploadElapsed -= 100 wantedDownloadElapsed -= 100 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -2964,7 +2948,7 @@ func TestBandwidthAndConnections(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -2974,7 +2958,7 @@ func TestBandwidthAndConnections(t *testing.T) { func TestPatternsFilters(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(131072) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -2996,7 +2980,7 @@ func TestPatternsFilters(t *testing.T) { DeniedPatterns: []string{}, }, } - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3016,7 +3000,7 @@ func TestPatternsFilters(t *testing.T) { err = client.Rename("dir.zip", "dir1.zip") assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -3030,7 +3014,7 @@ func TestPatternsFilters(t *testing.T) { func TestExtensionsFilters(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(131072) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -3061,7 +3045,7 @@ func TestExtensionsFilters(t *testing.T) { DeniedPatterns: []string{}, }, } - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3083,7 +3067,7 @@ func TestExtensionsFilters(t *testing.T) { err = client.Rename("dir.zip", "dir1.zip") assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -3113,7 +3097,7 @@ func TestVirtualFolders(t *testing.T) { err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3188,9 +3172,9 @@ func TestVirtualFolders(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -3250,7 +3234,7 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3293,11 +3277,11 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) { err = client.Rename(path.Join(vdirPath1, testFileName+".rename"), testFileName) assert.Error(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -3325,11 +3309,11 @@ func TestTruncateQuotaLimits(t *testing.T) { VirtualPath: vdirPath, QuotaFiles: 10, }) - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaSize = 20 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -3345,7 +3329,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.NoError(t, err) expectedQuotaFiles := 0 expectedQuotaSize := int64(2) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -3357,7 +3341,7 @@ func TestTruncateQuotaLimits(t *testing.T) { err = f.Truncate(5) assert.NoError(t, err) expectedQuotaSize = int64(5) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -3370,7 +3354,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.NoError(t, err) expectedQuotaFiles = 1 expectedQuotaSize = int64(5) + int64(len(data)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -3378,7 +3362,7 @@ func TestTruncateQuotaLimits(t *testing.T) { // now truncate by path err = client.Truncate(testFileName, 5) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(5), user.UsedQuotaSize) @@ -3387,7 +3371,7 @@ func TestTruncateQuotaLimits(t *testing.T) { if assert.NoError(t, err) { err = f.Close() assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(5), user.UsedQuotaSize) @@ -3397,7 +3381,7 @@ func TestTruncateQuotaLimits(t *testing.T) { if assert.NoError(t, err) { err = f.Close() assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) @@ -3410,7 +3394,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.Equal(t, len(data), n) err = f.Truncate(11) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(11), user.UsedQuotaSize) @@ -3421,7 +3405,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.Equal(t, len(data), n) err = f.Truncate(5) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(5), user.UsedQuotaSize) @@ -3432,7 +3416,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.Equal(t, len(data), n) err = f.Truncate(12) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, int64(12), user.UsedQuotaSize) @@ -3445,7 +3429,7 @@ func TestTruncateQuotaLimits(t *testing.T) { err = f.Close() assert.Error(t, err) // the file is deleted - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) @@ -3463,7 +3447,7 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.NoError(t, err) expectedQuotaFiles := 0 expectedQuotaSize := int64(2) - folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { fold := folder[0] @@ -3473,7 +3457,7 @@ func TestTruncateQuotaLimits(t *testing.T) { err = f.Close() assert.NoError(t, err) expectedQuotaFiles = 1 - folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { fold := folder[0] @@ -3483,7 +3467,7 @@ func TestTruncateQuotaLimits(t *testing.T) { } err = client.Truncate(vfileName, 1) assert.NoError(t, err) - folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { fold := folder[0] @@ -3495,21 +3479,21 @@ func TestTruncateQuotaLimits(t *testing.T) { assert.NoError(t, err) user.UsedQuotaFiles = 0 user.UsedQuotaSize = 0 - _, err = httpd.UpdateQuotaUsage(user, "reset", http.StatusOK) + _, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK) assert.NoError(t, err) user.QuotaSize = 0 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(mappedPath) assert.NoError(t, err) @@ -3565,7 +3549,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath3, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3637,13 +3621,13 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) { err = client.Rename(aDir, path.Join(vdirPath3, aDir)) assert.Error(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath3}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath3}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -3689,7 +3673,7 @@ func TestVirtualFoldersQuotaValues(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3711,18 +3695,18 @@ func TestVirtualFoldersQuotaValues(t *testing.T) { assert.NoError(t, err) expectedQuotaFiles := 2 expectedQuotaSize := testFileSize * 2 - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3735,14 +3719,14 @@ func TestVirtualFoldersQuotaValues(t *testing.T) { err = client.Remove(path.Join(vdirPath2, testFileName)) assert.NoError(t, err) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, int64(0), f.UsedQuotaSize) assert.Equal(t, 0, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3752,11 +3736,11 @@ func TestVirtualFoldersQuotaValues(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -3796,7 +3780,7 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -3828,18 +3812,18 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { assert.NoError(t, err) err = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 2, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3859,11 +3843,11 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { // - vdir2/dir2/testFileName1 err = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath1, dir1, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3877,18 +3861,18 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { // - vdir2/dir2/testFileName1 err = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath2, dir1, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 2, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3901,18 +3885,18 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName.rename (initial testFileName1) err = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3924,18 +3908,18 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName.rename (initial testFileName1) err = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath1, dir1, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3951,18 +3935,18 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { assert.NoError(t, err) err = client.Rename(path.Join(vdirPath2, dir1), path.Join(vdirPath2, dir2)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -3975,11 +3959,11 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) { err = os.Remove(testFilePath1) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -4019,7 +4003,7 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -4064,18 +4048,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName1.rename err = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName1+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4089,18 +4073,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName1.rename err = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath1, dir2, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize*2, f.UsedQuotaSize) assert.Equal(t, 2, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4113,18 +4097,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName1.rename err = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath2, dir2, testFileName1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4136,18 +4120,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // - vdir2/dir2/testFileName1 (is the initial testFileName) err = client.Rename(path.Join(vdirPath2, dir1, testFileName1+".rename"), path.Join(vdirPath1, dir2, testFileName+".rename")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4173,18 +4157,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // rename directories between the two virtual folders err = client.Rename(path.Join(vdirPath2, dir2), path.Join(vdirPath1, dir1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 5, user.UsedQuotaFiles) assert.Equal(t, testFileSize1*3+testFileSize*2, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1*3+testFileSize*2, f.UsedQuotaSize) assert.Equal(t, 5, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4194,18 +4178,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { // now move on vpath2 err = client.Rename(path.Join(vdirPath1, dir2), path.Join(vdirPath2, dir1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize1*2+testFileSize, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize) assert.Equal(t, 3, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4218,11 +4202,11 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) { err = os.Remove(testFilePath1) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -4262,7 +4246,7 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -4307,18 +4291,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - vdir2/dir2/testFileName1 err = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(testFileName)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4332,18 +4316,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName err = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(testFileName1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize1, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4356,18 +4340,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - vdir2/dir1/testFileName err = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(testFileName)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize1+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, int64(0), f.UsedQuotaSize) assert.Equal(t, 0, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4379,18 +4363,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - testFileName1 (initial testFileName) err = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(testFileName1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, int64(0), f.UsedQuotaSize) assert.Equal(t, 0, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4414,18 +4398,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - dir1/testFileName1 err = client.Rename(path.Join(vdirPath2, dir1), dir1) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 6, user.UsedQuotaFiles) assert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 2, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4440,18 +4424,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { // - dir1/testFileName1 err = client.Rename(path.Join(vdirPath1, dir1), dir2) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 6, user.UsedQuotaFiles) assert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, int64(0), f.UsedQuotaSize) assert.Equal(t, 0, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4464,11 +4448,11 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) { err = os.Remove(testFilePath1) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -4508,7 +4492,7 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -4545,11 +4529,11 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir1/dir1/testFileName1 err = client.Rename(testFileName1, path.Join(vdirPath1, dir1, testFileName1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4561,11 +4545,11 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir1/dir1/testFileName1 err = client.Rename(testFileName, path.Join(vdirPath2, dir1, testFileName)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4581,7 +4565,7 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { assert.NoError(t, err) err = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize) @@ -4591,18 +4575,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir2/dir1/testFileName err = client.Rename(testFileName, path.Join(vdirPath1, dir1, testFileName1)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4614,18 +4598,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir2/dir1/testFileName (initial testFileName1) err = client.Rename(testFileName1, path.Join(vdirPath2, dir1, testFileName)) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4643,18 +4627,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /dir1/testFileName1 // - /vdir1/dir1/testFileName1 (initial testFileName) // - /vdir2/dir1/testFileName (initial testFileName1) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4667,18 +4651,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir2/dir1/testFileName (initial testFileName1) err = client.Rename(dir1, path.Join(vdirPath1, "adir")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 3, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4699,18 +4683,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { // - /vdir2/adir/testFileName1 err = client.Rename(dir1, path.Join(vdirPath2, "adir")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize*2+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 3, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -4723,11 +4707,11 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) { err = os.Remove(testFilePath1) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -4766,7 +4750,7 @@ func TestVirtualFoldersLink(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -4815,11 +4799,11 @@ func TestVirtualFoldersLink(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -4836,7 +4820,7 @@ func TestOverlappedMappedFolders(t *testing.T) { assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.TrackQuota = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) usePubKey := false @@ -4862,7 +4846,7 @@ func TestOverlappedMappedFolders(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -4912,7 +4896,7 @@ func TestOverlappedMappedFolders(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) if providerConf.Driver != dataprovider.MemoryDataProviderName { @@ -4921,15 +4905,15 @@ func TestOverlappedMappedFolders(t *testing.T) { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) } - _, _, err = httpd.AddUser(u, http.StatusOK) + _, _, err = httpdtest.AddUser(u, http.StatusCreated) assert.Error(t, err) err = os.RemoveAll(user.GetHomeDir()) @@ -5029,27 +5013,27 @@ func TestVirtualFolderQuotaScan(t *testing.T) { assert.NoError(t, err) expectedQuotaSize := testFileSize expectedQuotaFiles := 1 - folder, _, err := httpd.AddFolder(vfs.BaseVirtualFolder{ + folder, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{ MappedPath: mappedPath, - }, http.StatusOK) + }, http.StatusCreated) assert.NoError(t, err) - _, err = httpd.StartFolderQuotaScan(folder, http.StatusAccepted) + _, err = httpdtest.StartFolderQuotaScan(folder, http.StatusAccepted) assert.NoError(t, err) assert.Eventually(t, func() bool { - scans, _, err := httpd.GetFoldersQuotaScans(http.StatusOK) + scans, _, err := httpdtest.GetFoldersQuotaScans(http.StatusOK) if err == nil { return len(scans) == 0 } return false }, 1*time.Second, 50*time.Millisecond) - folders, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folders, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folders, 1) { folder = folders[0] assert.Equal(t, expectedQuotaFiles, folder.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, folder.UsedQuotaSize) } - _, err = httpd.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(mappedPath) assert.NoError(t, err) @@ -5103,7 +5087,7 @@ func TestVFolderQuotaSize(t *testing.T) { testFilePath := filepath.Join(homeBasePath, testFileName) err = createTestFile(testFilePath, testFileSize) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5119,33 +5103,33 @@ func TestVFolderQuotaSize(t *testing.T) { // now vdir2 is over quota err = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName+".quota"), testFileSize, client) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize, user.UsedQuotaSize) // remove a file err = client.Remove(testFileName) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) // upload to vdir1 must work now err = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize, f.UsedQuotaSize) assert.Equal(t, 1, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -5164,7 +5148,7 @@ func TestVFolderQuotaSize(t *testing.T) { QuotaFiles: 10, QuotaSize: testFileSize*2 + 1, }) - user1, _, err := httpd.AddUser(u, http.StatusOK) + user1, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err = getSftpClient(user1, usePubKey) if assert.NoError(t, err) { @@ -5176,14 +5160,14 @@ func TestVFolderQuotaSize(t *testing.T) { assert.Error(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5196,7 +5180,7 @@ func TestVFolderQuotaSize(t *testing.T) { func TestMissingFile(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5207,7 +5191,7 @@ func TestMissingFile(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5219,7 +5203,7 @@ func TestOpenError(t *testing.T) { } usePubKey := false u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5264,7 +5248,7 @@ func TestOpenError(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5273,7 +5257,7 @@ func TestOpenError(t *testing.T) { func TestOverwriteDirWithFile(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5298,7 +5282,7 @@ func TestOverwriteDirWithFile(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5318,7 +5302,7 @@ func TestHashedPasswords(t *testing.T) { for pwd, clearPwd := range pwdMapping { u := getTestUser(usePubKey) u.Password = pwd - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.Password = clearPwd client, err := getSftpClient(user, usePubKey) @@ -5331,7 +5315,7 @@ func TestHashedPasswords(t *testing.T) { if !assert.Error(t, err, "login with wrong password must fail") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5355,7 +5339,7 @@ func TestPasswordsHashPbkdf2Sha256_389DS(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = pbkdf2Pwd - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) user.Password = pbkdf2ClearPwd client, err := getSftpClient(user, usePubKey) @@ -5368,7 +5352,7 @@ func TestPasswordsHashPbkdf2Sha256_389DS(t *testing.T) { if !assert.Error(t, err, "login with wrong password must fail") { client.Close() } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5381,7 +5365,7 @@ func TestPermList(t *testing.T) { dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} u.Permissions["/sub"] = []string{dataprovider.PermCreateSymlinks, dataprovider.PermListItems} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5408,7 +5392,7 @@ func TestPermList(t *testing.T) { _, err = client.ReadLink(path.Join("/sub", testFileName)) assert.Error(t, err, "read remote link without permission on targe dir should not succeed") } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5420,7 +5404,7 @@ func TestPermDownload(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5441,7 +5425,7 @@ func TestPermDownload(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5453,7 +5437,7 @@ func TestPermUpload(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5467,7 +5451,7 @@ func TestPermUpload(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5479,7 +5463,7 @@ func TestPermOverwrite(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5495,7 +5479,7 @@ func TestPermOverwrite(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5507,7 +5491,7 @@ func TestPermDelete(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5523,7 +5507,7 @@ func TestPermDelete(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5536,7 +5520,7 @@ func TestPermRename(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5554,7 +5538,7 @@ func TestPermRename(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5567,7 +5551,7 @@ func TestPermRenameOverwrite(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermRename, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5589,7 +5573,7 @@ func TestPermRenameOverwrite(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5601,7 +5585,7 @@ func TestPermCreateDirs(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5609,7 +5593,7 @@ func TestPermCreateDirs(t *testing.T) { err = client.Mkdir("testdir") assert.Error(t, err, "mkdir without permission should not succeed") } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5622,7 +5606,7 @@ func TestPermSymlink(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5640,7 +5624,7 @@ func TestPermSymlink(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5652,7 +5636,7 @@ func TestPermChmod(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChown, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5670,7 +5654,7 @@ func TestPermChmod(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5683,7 +5667,7 @@ func TestPermChown(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChtimes} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5701,7 +5685,7 @@ func TestPermChown(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5714,7 +5698,7 @@ func TestPermChtimes(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5732,7 +5716,7 @@ func TestPermChtimes(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5743,7 +5727,7 @@ func TestSubDirsUploads(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermChtimes, dataprovider.PermDownload, dataprovider.PermOverwrite} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5794,7 +5778,7 @@ func TestSubDirsUploads(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5805,7 +5789,7 @@ func TestSubDirsOverwrite(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermOverwrite, dataprovider.PermListItems} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5825,7 +5809,7 @@ func TestSubDirsOverwrite(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5836,7 +5820,7 @@ func TestSubDirsDownloads(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermChmod, dataprovider.PermUpload, dataprovider.PermListItems} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5868,7 +5852,7 @@ func TestSubDirsDownloads(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5881,7 +5865,7 @@ func TestPermsSubDirsSetstat(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermCreateDirs} u.Permissions["/subdir"] = []string{dataprovider.PermAny} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5904,7 +5888,7 @@ func TestPermsSubDirsSetstat(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5912,7 +5896,7 @@ func TestPermsSubDirsSetstat(t *testing.T) { func TestOpenUnhandledChannel(t *testing.T) { u := getTestUser(false) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) config := &ssh.ClientConfig{ @@ -5931,7 +5915,7 @@ func TestOpenUnhandledChannel(t *testing.T) { err = conn.Close() assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5943,7 +5927,7 @@ func TestPermsSubDirsCommands(t *testing.T) { u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs} u.Permissions["/subdir/otherdir"] = []string{dataprovider.PermListItems, dataprovider.PermDownload} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -5978,7 +5962,7 @@ func TestPermsSubDirsCommands(t *testing.T) { err = client.RemoveDirectory("/otherdir1") assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -5989,12 +5973,12 @@ func TestRootDirCommands(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermDownload, dataprovider.PermUpload} - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/subdir"] = []string{dataprovider.PermDownload, dataprovider.PermUpload} - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client, err := getSftpClient(user, usePubKey) @@ -6012,13 +5996,13 @@ func TestRootDirCommands(t *testing.T) { assert.NoError(t, err) user.Permissions = make(map[string][]string) user.Permissions["/"] = []string{dataprovider.PermAny} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -6487,7 +6471,7 @@ func TestGetVirtualFolderForPath(t *testing.T) { func TestSSHCommands(t *testing.T) { usePubKey := false - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) _, err = runSSHCommand("ls", user, usePubKey) assert.Error(t, err, "unsupported ssh command must fail") @@ -6515,19 +6499,19 @@ func TestSSHCommands(t *testing.T) { assert.NoError(t, err) assert.Contains(t, string(out), "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b") - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } func TestSSHFileHash(t *testing.T) { usePubKey := true - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) u := getTestUserWithCryptFs(usePubKey) u.Username = u.Username + "_crypt" - cryptUser, _, err := httpd.AddUser(u, http.StatusOK) + cryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} { client, err := getSftpClient(user, usePubKey) @@ -6541,13 +6525,13 @@ func TestSSHFileHash(t *testing.T) { assert.NoError(t, err) user.Permissions = make(map[string][]string) user.Permissions["/"] = []string{dataprovider.PermUpload} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) _, err = runSSHCommand("sha512sum "+testFileName, user, usePubKey) assert.Error(t, err, "hash command with no list permission must fail") user.Permissions["/"] = []string{dataprovider.PermAny} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) initialHash, err := computeHashForFile(sha512.New(), testFilePath) @@ -6564,11 +6548,11 @@ func TestSSHFileHash(t *testing.T) { assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(cryptUser, http.StatusOK) + _, err = httpdtest.RemoveUser(cryptUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -6608,7 +6592,7 @@ func TestSSHCopy(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testDir := "adir" testDir1 := "adir1" @@ -6644,18 +6628,18 @@ func TestSSHCopy(t *testing.T) { assert.NoError(t, err) err = client.Symlink(path.Join(testDir, testFileName), testFileName) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 4, user.UsedQuotaFiles) assert.Equal(t, 2*testFileSize+2*testFileSize1, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize) assert.Equal(t, 2, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -6678,7 +6662,7 @@ func TestSSHCopy(t *testing.T) { if assert.NoError(t, err) { assert.True(t, fi.IsDir()) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 6, user.UsedQuotaFiles) assert.Equal(t, 3*testFileSize+3*testFileSize1, user.UsedQuotaSize) @@ -6695,7 +6679,7 @@ func TestSSHCopy(t *testing.T) { if assert.NoError(t, err) { assert.True(t, fi.Mode().IsRegular()) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 7, user.UsedQuotaFiles) assert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize) @@ -6708,11 +6692,11 @@ func TestSSHCopy(t *testing.T) { if assert.NoError(t, err) { assert.True(t, fi.IsDir()) } - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 7, user.UsedQuotaFiles) assert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -6726,11 +6710,11 @@ func TestSSHCopy(t *testing.T) { assert.Equal(t, "OK\n", string(out)) _, err := client.Stat(path.Join(vdirPath2, testDir1+"copy")) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 9, user.UsedQuotaFiles) assert.Equal(t, 5*testFileSize+4*testFileSize1, user.UsedQuotaSize) - folder, _, err = httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -6778,11 +6762,11 @@ func TestSSHCopy(t *testing.T) { err = os.Remove(testFilePath1) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -6800,7 +6784,7 @@ func TestSSHCopyPermissions(t *testing.T) { dataprovider.PermListItems} u.Permissions["/dir3"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermDownload, dataprovider.PermListItems} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -6848,7 +6832,7 @@ func TestSSHCopyPermissions(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -6892,7 +6876,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -6933,18 +6917,18 @@ func TestSSHCopyQuotaLimits(t *testing.T) { assert.NoError(t, err) _, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %v", path.Join(vdirPath2, testDir)), user, usePubKey) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) assert.Equal(t, int64(0), user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, 0, f.UsedQuotaFiles) assert.Equal(t, int64(0), f.UsedQuotaSize) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -6977,7 +6961,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) { user.QuotaSize = testFileSize * 10 user.VirtualFolders[1].QuotaSize = testFileSize user.VirtualFolders[1].QuotaFiles = 10 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.Equal(t, 1, user.QuotaFiles) assert.Equal(t, testFileSize*10, user.QuotaSize) @@ -7002,11 +6986,11 @@ func TestSSHCopyQuotaLimits(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -7044,7 +7028,7 @@ func TestSSHRemove(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -7090,7 +7074,7 @@ func TestSSHRemove(t *testing.T) { assert.Equal(t, "OK\n", string(out)) _, err := client.Stat(testFileName) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3, user.UsedQuotaFiles) assert.Equal(t, testFileSize+2*testFileSize1, user.UsedQuotaSize) @@ -7100,7 +7084,7 @@ func TestSSHRemove(t *testing.T) { assert.Equal(t, "OK\n", string(out)) _, err := client.Stat(path.Join(vdirPath1, testFileName)) assert.Error(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, testFileSize1, user.UsedQuotaSize) @@ -7127,7 +7111,7 @@ func TestSSHRemove(t *testing.T) { // test remove dir with no delete perm user.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems} - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -7136,11 +7120,11 @@ func TestSSHRemove(t *testing.T) { assert.Error(t, err) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -7156,7 +7140,7 @@ func TestBasicGitCommands(t *testing.T) { } usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) repoName := "testrepo" //nolint:goconst @@ -7175,7 +7159,7 @@ func TestBasicGitCommands(t *testing.T) { assert.NoError(t, err, "unexpected error, out: %v", string(out)) user.QuotaFiles = 100000 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) out, err = pushToGitRepo(clonePath) @@ -7186,10 +7170,10 @@ func TestBasicGitCommands(t *testing.T) { out, err = addFileToGitRepo(clonePath, 131072) assert.NoError(t, err, "unexpected error, out: %v", string(out)) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) user.QuotaSize = user.UsedQuotaSize + 1 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) out, err = pushToGitRepo(clonePath) assert.Error(t, err, "git push must fail if quota is exceeded, out: %v", string(out)) @@ -7202,7 +7186,7 @@ func TestBasicGitCommands(t *testing.T) { err = os.Chmod(aDir, os.ModePerm) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) @@ -7231,7 +7215,7 @@ func TestGitQuotaVirtualFolders(t *testing.T) { }) err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err) { @@ -7263,12 +7247,12 @@ func TestGitQuotaVirtualFolders(t *testing.T) { out, err = pushToGitRepo(clonePath) assert.NoError(t, err, "unexpected error, out: %v", string(out)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(mappedPath) assert.NoError(t, err) @@ -7282,7 +7266,7 @@ func TestGitErrors(t *testing.T) { } usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) repoName := "testrepo" clonePath := filepath.Join(homeBasePath, repoName) @@ -7293,7 +7277,7 @@ func TestGitErrors(t *testing.T) { out, err := cloneGitRepo(homeBasePath, "/"+repoName, user.Username) assert.Error(t, err, "cloning a missing repo must fail, out: %v", string(out)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -7309,11 +7293,11 @@ func TestSCPBasicHandling(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.QuotaSize = 6553600 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaSize = 6553600 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -7339,7 +7323,7 @@ func TestSCPBasicHandling(t *testing.T) { } err = os.Remove(localPath) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -7349,9 +7333,9 @@ func TestSCPBasicHandling(t *testing.T) { } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -7366,11 +7350,11 @@ func TestSCPUploadFileOverwrite(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.QuotaFiles = 1000 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser(usePubKey) u.QuotaFiles = 1000 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(32760) @@ -7383,7 +7367,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) { // test a new upload that must overwrite the existing file err = scpUpload(testFilePath, remoteUpPath, true, false) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, testFileSize, user.UsedQuotaSize) assert.Equal(t, 1, user.UsedQuotaFiles) @@ -7403,14 +7387,14 @@ func TestSCPUploadFileOverwrite(t *testing.T) { defer client.Close() err = client.Symlink(testFileName, testFileName+".link") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, testFileSize, user.UsedQuotaSize) assert.Equal(t, 1, user.UsedQuotaFiles) } err = scpUpload(testFilePath, remoteUpPath+".link", true, false) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, testFileSize*2, user.UsedQuotaSize) assert.Equal(t, 2, user.UsedQuotaFiles) @@ -7424,11 +7408,11 @@ func TestSCPUploadFileOverwrite(t *testing.T) { } err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) } @@ -7437,9 +7421,9 @@ func TestSCPRecursive(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - localUser, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + localUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) - sftpUser, _, err := httpd.AddUser(getTestSFTPUser(usePubKey), http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated) assert.NoError(t, err) testBaseDirName := "test_dir" testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName) @@ -7493,11 +7477,11 @@ func TestSCPRecursive(t *testing.T) { err = os.RemoveAll(testBaseDirPath) assert.NoError(t, err) - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) } @@ -7507,7 +7491,7 @@ func TestSCPExtensionsFilter(t *testing.T) { } usePubKey := true u := getTestUser(usePubKey) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(131072) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -7525,14 +7509,14 @@ func TestSCPExtensionsFilter(t *testing.T) { DeniedExtensions: []string{}, }, } - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = scpDownload(localPath, remoteDownPath, false, false) assert.Error(t, err, "scp download must fail") err = scpUpload(testFilePath, remoteUpPath, false, false) assert.Error(t, err, "scp upload must fail") - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) @@ -7550,7 +7534,7 @@ func TestSCPUploadMaxSize(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Filters.MaxUploadFileSize = testFileSize + 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) err = createTestFile(testFilePath, testFileSize) @@ -7569,7 +7553,7 @@ func TestSCPUploadMaxSize(t *testing.T) { assert.NoError(t, err) err = os.Remove(testFilePath1) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -7591,7 +7575,7 @@ func TestSCPVirtualFolders(t *testing.T) { }) err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testBaseDirName := "test_dir" testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName) @@ -7611,9 +7595,9 @@ func TestSCPVirtualFolders(t *testing.T) { err = scpDownload(testBaseDirDownPath, remoteDownPath, true, true) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(testBaseDirPath) assert.NoError(t, err) @@ -7656,7 +7640,7 @@ func TestSCPVirtualFoldersQuota(t *testing.T) { assert.NoError(t, err) err = os.MkdirAll(mappedPath2, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testBaseDirName := "test_dir" testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName) @@ -7688,18 +7672,18 @@ func TestSCPVirtualFoldersQuota(t *testing.T) { assert.NoError(t, err) expectedQuotaFiles := 2 expectedQuotaSize := testFileSize * 2 - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) - folder, _, err := httpd.GetFolders(0, 0, mappedPath1, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath1, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] assert.Equal(t, expectedQuotaSize, f.UsedQuotaSize) assert.Equal(t, expectedQuotaFiles, f.UsedQuotaFiles) } - folder, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath2, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -7707,11 +7691,11 @@ func TestSCPVirtualFoldersQuota(t *testing.T) { assert.Equal(t, expectedQuotaFiles, f.UsedQuotaFiles) } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(testBaseDirPath) assert.NoError(t, err) @@ -7733,7 +7717,7 @@ func TestSCPPermsSubDirs(t *testing.T) { u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/somedir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) localPath := filepath.Join(homeBasePath, "scp_download.dat") subPath := filepath.Join(user.GetHomeDir(), "somedir") @@ -7761,7 +7745,7 @@ func TestSCPPermsSubDirs(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7772,7 +7756,7 @@ func TestSCPPermCreateDirs(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(32760) @@ -7795,7 +7779,7 @@ func TestSCPPermCreateDirs(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7806,7 +7790,7 @@ func TestSCPPermUpload(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65536) @@ -7819,7 +7803,7 @@ func TestSCPPermUpload(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7830,7 +7814,7 @@ func TestSCPPermOverwrite(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65536) @@ -7846,7 +7830,7 @@ func TestSCPPermOverwrite(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7857,7 +7841,7 @@ func TestSCPPermDownload(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65537) @@ -7875,7 +7859,7 @@ func TestSCPPermDownload(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7888,7 +7872,7 @@ func TestSCPQuotaSize(t *testing.T) { u := getTestUser(usePubKey) u.QuotaFiles = 1 u.QuotaSize = testFileSize + 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) err = createTestFile(testFilePath, testFileSize) @@ -7912,7 +7896,7 @@ func TestSCPQuotaSize(t *testing.T) { // now test quota limits while uploading the current file, we have 1 bytes remaining user.QuotaSize = testFileSize + 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = scpUpload(testFilePath2, remoteUpPath+".quota", true, false) assert.Error(t, err, "user is over quota scp upload must fail") @@ -7931,7 +7915,7 @@ func TestSCPQuotaSize(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7940,7 +7924,7 @@ func TestSCPEscapeHomeDir(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) @@ -7967,7 +7951,7 @@ func TestSCPEscapeHomeDir(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -7976,7 +7960,7 @@ func TestSCPUploadPaths(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) @@ -8002,7 +7986,7 @@ func TestSCPUploadPaths(t *testing.T) { assert.NoError(t, err) err = os.Remove(localPath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -8011,7 +7995,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) @@ -8026,7 +8010,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) { err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -8038,12 +8022,12 @@ func TestSCPRemoteToRemote(t *testing.T) { t.Skip("scp between remote hosts is not supported on Windows") } usePubKey := true - user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated) assert.NoError(t, err) u := getTestUser(usePubKey) u.Username += "1" u.HomeDir += "1" - user1, _, err := httpd.AddUser(u, http.StatusOK) + user1, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) @@ -8057,11 +8041,11 @@ func TestSCPRemoteToRemote(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user1.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) } @@ -8070,7 +8054,7 @@ func TestSCPErrors(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } u := getTestUser(true) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileSize := int64(524288) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -8083,7 +8067,7 @@ func TestSCPErrors(t *testing.T) { assert.NoError(t, err) user.UploadBandwidth = 512 user.DownloadBandwidth = 512 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) cmd := getScpDownloadCommand(localPath, remoteDownPath, false, false) go func() { @@ -8114,7 +8098,7 @@ func TestSCPErrors(t *testing.T) { os.Remove(localPath) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } diff --git a/sftpgo.json b/sftpgo.json index f0d295af..159c0b7a 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -121,7 +121,6 @@ "sslmode": 0, "connection_string": "", "sql_tables_prefix": "", - "manage_users": 1, "track_quota": 2, "pool_size": 0, "users_base_dir": "", @@ -153,7 +152,6 @@ "templates_path": "templates", "static_files_path": "static", "backups_path": "backups", - "auth_user_file": "", "certificate_file": "", "certificate_key_file": "" }, diff --git a/static/img/undraw_profile.svg b/static/img/undraw_profile.svg new file mode 100644 index 00000000..98023417 --- /dev/null +++ b/static/img/undraw_profile.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/static/vendor/fontawesome-free/css/all.min.css b/static/vendor/fontawesome-free/css/all.min.css deleted file mode 100644 index 3d28ab20..00000000 --- a/static/vendor/fontawesome-free/css/all.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */ -.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/static/vendor/fontawesome-free/css/fontawesome.min.css b/static/vendor/fontawesome-free/css/fontawesome.min.css new file mode 100644 index 00000000..8e36e25a --- /dev/null +++ b/static/vendor/fontawesome-free/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/static/vendor/fontawesome-free/css/solid.min.css b/static/vendor/fontawesome-free/css/solid.min.css new file mode 100644 index 00000000..f8c89c38 --- /dev/null +++ b/static/vendor/fontawesome-free/css/solid.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/calendar.svg b/static/vendor/fontawesome-free/svgs/solid/calendar.svg deleted file mode 100644 index 2d3eefe8..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/calendar.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg b/static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg deleted file mode 100644 index b22538a9..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/folder-open.svg b/static/vendor/fontawesome-free/svgs/solid/folder-open.svg deleted file mode 100644 index 57dcfa60..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/folder-open.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/folder.svg b/static/vendor/fontawesome-free/svgs/solid/folder.svg deleted file mode 100644 index c9607689..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/folder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/info-circle.svg b/static/vendor/fontawesome-free/svgs/solid/info-circle.svg deleted file mode 100644 index af2a4f4f..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/info-circle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/svgs/solid/user.svg b/static/vendor/fontawesome-free/svgs/solid/user.svg deleted file mode 100644 index 591873a5..00000000 --- a/static/vendor/fontawesome-free/svgs/solid/user.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/vendor/fontawesome-free/webfonts/fa-solid-900.svg b/static/vendor/fontawesome-free/webfonts/fa-solid-900.svg index 7742838b..313b3118 100644 --- a/static/vendor/fontawesome-free/webfonts/fa-solid-900.svg +++ b/static/vendor/fontawesome-free/webfonts/fa-solid-900.svg @@ -1,16 +1,12 @@ - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 +Created by FontForge 20200314 at Mon Oct 5 09:50:45 2020 By Robert Madole Copyright (c) Font Awesome - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +d="M470.38 446.49c3.03613 0.957031 6.26953 1.47949 9.62012 1.47949c17.6514 0 31.9834 -14.3223 32 -31.9697v-352c0 -35.3496 -43 -64 -96 -64s-96 28.6602 -96 64s43 64 96 64c11.0361 -0.0605469 21.7158 -1.4248 32 -3.92969v184.609l-256 -75v-233.68 +c0 -35.3398 -43 -64 -96 -64s-96 28.6602 -96 64s43 64 96 64c11.0352 -0.0625 21.7139 -1.42285 32 -3.91992v261.41c0.00878906 14.3125 9.43359 26.4336 22.4102 30.5098z" /> +d="M280.37 299.74c2.09082 1.68555 4.76562 2.69434 7.6582 2.69434s5.55078 -1.00879 7.6416 -2.69434l184.33 -151.74v-164c0 -8.83105 -7.16895 -16 -16 -16l-112.02 0.30957c-8.83105 0 -16.001 7.16895 -16.001 15.999c0 0.0175781 0 0.0341797 0.000976562 0.0517578 +v95.6396c0 8.83105 -7.16992 16 -16 16h-64c-8.83105 0 -16 -7.16895 -16 -16v-95.71c0 -8.80371 -7.12695 -15.9561 -15.9209 -16l-112.06 -0.290039c-8.83105 0 -16 7.16895 -16 16v163.89zM571.6 196.53c2.70703 -2.20117 4.42578 -5.56152 4.42578 -9.31836 +c0 -2.88867 -1.02246 -5.54004 -2.72559 -7.6123l-25.5 -31c-2.20117 -2.66309 -5.53418 -4.35059 -9.25684 -4.35059c-2.90332 0 -5.56641 1.0332 -7.64258 2.75098l-235.23 193.74c-2.09082 1.68555 -4.7666 2.69434 -7.6582 2.69434 +c-2.89258 0 -5.55078 -1.00879 -7.6416 -2.69434l-235.22 -193.74c-2.0752 -1.71387 -4.73926 -2.75586 -7.63867 -2.75586c-3.73242 0 -7.07031 1.70898 -9.27148 4.38574l-25.5 31c-1.71875 2.07617 -2.7627 4.74414 -2.7627 7.64648 +c0 3.72266 1.69824 7.05176 4.3623 9.25391l253.13 208.47c8.29102 6.82227 18.9668 10.9209 30.5312 10.9209s22.1787 -4.09863 30.4688 -10.9209l89.5303 -73.6602v72.6104c0 6.62305 5.37695 12 12 12h56c6.62305 0 12 -5.37695 12 -12v-138.51z" /> +d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM348.49 127c2.19531 2.73926 3.52637 6.21973 3.52637 10c0 5.05566 -2.35059 9.56738 -6.0166 12.5l-58 42.5v144c0 8.83105 -7.16895 16 -16 16h-32 +c-8.83105 0 -16 -7.16895 -16 -16v-155.55c0 -12.6338 5.8418 -23.8975 15 -31.2305l67 -49.7197v0c2.7373 -2.19043 6.21387 -3.51758 9.98926 -3.51758c5.05566 0 9.56738 2.35059 12.501 6.01758l20 25v0z" /> +c-0.00195312 0 -0.00390625 -0.0078125 -0.00488281 -0.0078125c-4.12891 0 -7.53125 -3.13477 -7.95508 -7.15234zM315.64 144c9.5 0 16.9102 8.23047 15.9102 17.6797l-5.06934 48c-0.860352 8.14062 -7.7207 14.3203 -15.9102 14.3203h-45.1504 +c-8.18945 0 -15.0498 -6.17969 -15.9102 -14.3203l-5.06934 -48c-1 -9.44922 6.40918 -17.6797 15.9092 -17.6797h55.29z" /> +d="M567.938 204.092c5.07422 -7.61035 8.06152 -16.7998 8.06152 -26.625v-129.467c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v129.467c0 9.8252 2.9873 19.0146 8.06152 26.625l105.689 158.534c8.60742 12.9102 23.2725 21.374 39.9385 21.374 +h268.621c16.667 0 31.332 -8.46387 39.9395 -21.374zM162.252 320l-85.334 -128h123.082l32 -64h112l32 64h123.082l-85.333 128h-251.497z" /> +d="M500.33 448c6.62305 0 12 -5.37695 12 -12v-200.34c0 -6.62305 -5.37695 -12 -12 -12h-200.33c-6.62305 0 -12 5.37695 -12 12v47.4102c0 0.00390625 -0.00878906 0.00878906 -0.00878906 0.0136719c0 6.62305 5.37695 12 12 12 +c0.194336 0 0.386719 -0.00488281 0.579102 -0.0136719l101.529 -4.87012c-31.6084 47.0322 -85.1172 77.8594 -145.992 77.8594c-97.1367 0 -176 -78.8633 -176 -176s78.8633 -176 176 -176c44.502 0 85.168 16.5518 116.173 43.8301 +c2.10938 1.84375 4.87793 2.96582 7.89746 2.96582c3.31055 0 6.31055 -1.34375 8.48242 -3.51562l34 -34c2.17383 -2.17188 3.52246 -5.17285 3.52246 -8.48633c0 -3.55176 -1.54688 -6.74512 -4.00293 -8.94336c-43.8477 -39.6924 -102.079 -63.9102 -165.824 -63.9102 +h-0.355469c-136.9 0 -247.9 110.93 -248 247.81c-0.0996094 136.66 111.34 248.19 248 248.19c0.0927734 0 0.116211 0.140625 0.208984 0.140625c75.5918 0 143.312 -33.9727 188.711 -87.4707l-4 82.7598c-0.00878906 0.192383 -0.0136719 0.375977 -0.0136719 0.570312 +c0 6.62305 5.37695 12 12 12h0.0136719h47.4102z" /> +d="M440.65 435.43c-0.00976562 0.192383 -0.0136719 0.375977 -0.0136719 0.570312c0 6.62109 5.37305 11.9961 11.9932 12h47.3701c6.62305 0 12 -5.37695 12 -12v-200.35c0 -6.62305 -5.37695 -12 -12 -12h-200.22c-6.62305 0 -12 5.37695 -12 12v47.4092 +c0 0.00488281 -0.00878906 0.00976562 -0.00878906 0.0136719c0 6.62305 5.37695 12 12 12c0.194336 0 0.386719 -0.00390625 0.578125 -0.0136719l101.46 -4.85938c-31.5938 46.9941 -85.1406 77.6738 -145.973 77.6738c-82.8662 0 -152.428 -57.4229 -171.027 -134.614 +c-1.24219 -5.29688 -5.99707 -9.25391 -11.6699 -9.25977h-49.0498c-6.62305 0 -12 5.36719 -12 11.9893c0 0.748047 0.0693359 1.48047 0.200195 2.19043c21.6201 114.9 122.44 201.82 243.54 201.82c0.0966797 0 0.123047 0.141602 0.219727 0.141602 +c75.5615 0 143.248 -33.9814 188.601 -87.4814zM255.83 16c0.015625 0 0.0185547 0.0898438 0.0332031 0.0898438c82.8701 0 152.43 57.4434 170.997 134.65c1.24219 5.29688 5.99707 9.25391 11.6699 9.25977h49.0498c6.62305 0 12 -5.36719 12 -11.9893 +c0 -0.748047 -0.0693359 -1.48047 -0.200195 -2.19043c-21.6201 -114.9 -122.439 -201.82 -243.55 -201.82c-0.0800781 0 -0.0908203 -0.140625 -0.170898 -0.140625c-75.4814 0 -143.106 33.9082 -188.459 87.3105l4.14941 -82.5703 +c0.0107422 -0.201172 0.015625 -0.395508 0.015625 -0.599609c0 -6.62305 -5.37695 -12 -12 -12h-0.015625h-47.3496c-6.62305 0 -12 5.37695 -12 12v200.33c0 6.62305 5.37695 12 12 12h200.2c6.62305 0 12 -5.37695 12 -12v-47.4004 +c0 -0.00390625 0.0078125 -0.00878906 0.0078125 -0.0136719c0 -6.62305 -5.37695 -12 -12 -12c-0.193359 0 -0.386719 0.00488281 -0.578125 0.0136719l-101.8 4.87012c31.5254 -47.0088 85.0449 -77.7998 145.847 -77.7998h0.15332z" /> +c-60.5791 0 -109.917 48.0967 -111.928 108.187l-14.3828 7.19141c-10.502 5.25098 -17.6895 16.0908 -17.6895 28.6221v48c0 141.504 114.52 256 256 256z" /> +d="M215 377c15 15 41 4.46973 41 -17v-336c0 -21.4697 -26 -32 -41 -17l-88.9404 89h-102.06c-13.2461 0 -24 10.7539 -24 24v144c0 13.2461 10.7539 24 24 24h102z" /> +d="M0 195.882v204.118c0 26.5098 21.4902 48 48 48h204.118c13.2461 0 25.252 -5.37012 33.9404 -14.0586l211.883 -211.883c18.7441 -18.7441 18.7441 -49.1367 0 -67.8818l-204.118 -204.118c-18.7451 -18.7441 -49.1377 -18.7441 -67.8818 0l-211.883 211.883 +c-8.68848 8.68848 -14.0586 20.6943 -14.0586 33.9404zM112 384c-26.5098 0 -48 -21.4902 -48 -48s21.4902 -48 48 -48s48 21.4902 48 48s-21.4902 48 -48 48z" /> +d="M497.941 222.059c18.7441 -18.7441 18.7441 -49.1367 0 -67.8818l-204.118 -204.118c-18.7461 -18.7451 -49.1387 -18.7441 -67.8818 0l-211.883 211.883c-8.68848 8.68848 -14.0586 20.6943 -14.0586 33.9404v204.118c0 26.5098 21.4902 48 48 48h204.118 +c13.2461 0 25.252 -5.37012 33.9404 -14.0586zM112 288c26.5098 0 48 21.4902 48 48s-21.4902 48 -48 48s-48 -21.4902 -48 -48s21.4902 -48 48 -48zM625.941 154.177l-204.118 -204.118c-18.7451 -18.7441 -49.1377 -18.7441 -67.8818 0l-0.360352 0.360352 +l174.059 174.059c16.999 16.999 26.3604 39.6006 26.3604 63.6406s-9.3623 46.6406 -26.3604 63.6396l-196.242 196.242h48.7207c13.2461 0 25.252 -5.37012 33.9404 -14.0586l211.883 -211.883c18.7441 -18.7441 18.7441 -49.1367 0 -67.8818z" /> d="M512 304v-288c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h88l12.2998 32.9004c7 18.6992 24.9004 31.0996 44.9004 31.0996h125.5c20 0 37.8994 -12.4004 44.8994 -31.0996l12.4004 -32.9004h88c26.5 0 48 -21.5 48 -48zM376 160 c0 66.2002 -53.7998 120 -120 120s-120 -53.7998 -120 -120s53.7998 -120 120 -120s120 53.7998 120 120zM344 160c0 -48.5 -39.5 -88 -88 -88s-88 39.5 -88 88s39.5 88 88 88s88 -39.5 88 -88z" /> +d="M333.49 210c34.4395 -27.54 55.5693 -71.1504 50.8301 -119.6c-6.86035 -70.6504 -70.2002 -122.4 -141 -122.4h-209.32c-8.83105 0 -16 7.16895 -16 16v48c0 8.83105 7.16895 16 16 16h31.8701v288h-31.8701c-8.83105 0 -16 7.16895 -16 16v48 +c0 8.83105 7.16895 16 16 16h199.42c74.5801 0 134.45 -64.4902 127.07 -140.79c-2.43945 -24.5273 -12.1992 -47.1309 -27 -65.21zM145.66 336v-96h87.7598c26.4922 0 48 21.5078 48 48s-21.5078 48 -48 48h-87.7598zM233.42 48c30.9072 0 56 25.0928 56 56 +s-25.0928 56 -56 56h-87.7598v-112h87.7598z" /> +d="M320 400v-32c0 -8.83105 -7.16895 -16 -16 -16h-62.7598l-80 -320h46.7598c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-192c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h62.7598l80 320h-46.7598 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h192c8.83105 0 16 -7.16895 16 -16z" /> +d="M432 416c8.83105 0 16 -7.16895 16 -16v-80c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v16h-120v-112h24c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h24v112h-120v-16c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v80c0 8.83105 7.16895 16 16 16h416zM363.31 155.31l80 -80c2.89453 -2.89551 4.68555 -6.89844 4.68555 -11.3115 +c0 -4.41406 -1.79102 -8.41211 -4.68555 -11.3076l-80 -80c-10 -10.0205 -27.3096 -3 -27.3096 11.3096v48h-224v-48c0 -15.6396 -18 -20.6396 -27.3096 -11.3096l-80 80c-2.89453 2.89551 -4.68555 6.89844 -4.68555 11.3115c0 4.41406 1.79102 8.41211 4.68555 11.3076 +l80 80c10 10.0107 27.3096 3 27.3096 -11.3096v-48h224v48c0 15.6396 18 20.6396 27.3096 11.3096z" /> +d="M12.8301 96c-7.07617 0 -12.8301 5.74414 -12.8301 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h262.34h0.00976562c7.07617 0 12.8203 -5.74414 12.8203 -12.8193v-0.0107422v-38.3398v-0.00976562 +c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-262.34zM12.8301 352c-7.07617 0 -12.8301 5.74414 -12.8301 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h262.34h0.00976562 +c7.07617 0 12.8203 -5.74414 12.8203 -12.8193v-0.0107422v-38.3398v-0.00976562c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-262.34zM432 288c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h416zM432 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416z" /> +d="M432 288c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416zM432 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16 +v32c0 8.83105 7.16895 16 16 16h416zM108.1 352c-6.67773 0 -12.0996 5.42188 -12.0996 12.0996v39.8105c0 6.67285 5.41699 12.0898 12.0898 12.0898h0.00976562h231.811c6.67285 0 12.0898 -5.41699 12.0898 -12.0898v-39.8105v-0.00976562 +c0 -6.67285 -5.41699 -12.0898 -12.0898 -12.0898h-231.811zM339.91 96h-231.811c-6.67773 0 -12.0996 5.42188 -12.0996 12.0996v39.8105c0 6.67285 5.41699 12.0898 12.0898 12.0898h0.00976562h231.811c6.67285 0 12.0898 -5.41699 12.0898 -12.0898v-39.8105 +v-0.00976562c0 -6.67285 -5.41699 -12.0898 -12.0898 -12.0898z" /> +d="M16 224c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416zM432 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16 +v32c0 8.83105 7.16895 16 16 16h416zM435.17 416c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-262.34h-0.00976562c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193 +v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h262.34zM435.17 160c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-262.34h-0.00976562 +c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h262.34z" /> +d="M432 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416zM432 160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16 +v32c0 8.83105 7.16895 16 16 16h416zM432 288c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416zM432 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16 +h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416z" /> +d="M80 80c8.83105 0 16 -7.16895 16 -16v-64c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v64c0 8.83105 7.16895 16 16 16h64zM80 400c8.83105 0 16 -7.16895 16 -16v-64c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v64 +c0 8.83105 7.16895 16 16 16h64zM80 240c8.83105 0 16 -7.16895 16 -16v-64c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v64c0 8.83105 7.16895 16 16 16h64zM496 64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 384c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 224c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320z" /> +d="M100.69 84.71l-96 95.9805c-2.89453 2.89551 -4.68555 6.89844 -4.68555 11.3115c0 4.41406 1.79102 8.41211 4.68555 11.3076l96 96c9.97949 10 27.3096 3.01074 27.3096 -11.3096v-191.98c0 -14.2393 -17.3096 -21.3096 -27.3096 -11.3096zM432 32 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416zM435.17 160c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562 +c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-230.34h-0.00976562c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h230.34zM435.17 288 +c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-230.34h-0.00976562c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193v0.0107422v38.3398v0.00976562 +c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h230.34zM432 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416z" /> +d="M27.3096 84.7002c-9.97949 -10 -27.3096 -3.00977 -27.3096 11.2998v192c0 14.2197 17.2695 21.3398 27.3096 11.3203l96 -96c2.89453 -2.89648 4.68555 -6.89941 4.68555 -11.3125s-1.79102 -8.41211 -4.68555 -11.3076zM432 32c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416zM435.17 160c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422 +h-230.34h-0.00976562c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h230.34zM435.17 288c7.07617 0 12.8301 -5.74414 12.8301 -12.8193v-0.0107422v-38.3398v-0.00976562 +c0 -7.07617 -5.74414 -12.8203 -12.8193 -12.8203h-0.0107422h-230.34h-0.00976562c-7.07617 0 -12.8203 5.74414 -12.8203 12.8193v0.0107422v38.3398v0.00976562c0 7.07617 5.74414 12.8203 12.8193 12.8203h0.0107422h230.34zM432 416c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416z" /> @@ -375,18 +674,18 @@ c63.4004 0 118.9 33.5996 149.9 87.5c6.69922 11.7998 22.6992 11.2998 28.2998 -1.2 d="M216 424.14c0 -103.14 168 -125.85 168 -296.14c0 -105.87 -86.1299 -192 -192 -192s-192 86.1299 -192 192c0 58.6699 27.7998 106.84 54.5703 134.96c14.96 15.7305 41.4297 5.2002 41.4297 -16.5v-85.5098c0 -35.1699 27.9805 -64.4902 63.1504 -64.9404 c35.7393 -0.469727 64.8496 28.3604 64.8496 63.9902c0 88 -176 96.1504 -52.1504 277.18c13.5 19.7305 44.1504 10.7607 44.1504 -13.04z" /> +d="M572.52 206.6c2.21387 -4.37793 3.46094 -9.38965 3.46094 -14.626c0 -5.2373 -1.24707 -10.1855 -3.46094 -14.5635c-54.1992 -105.771 -161.59 -177.41 -284.52 -177.41s-230.29 71.5898 -284.52 177.4c-2.21387 4.37793 -3.46094 9.38965 -3.46094 14.626 +c0 5.2373 1.24707 10.1855 3.46094 14.5635c54.1992 105.771 161.59 177.41 284.52 177.41s230.29 -71.5898 284.52 -177.4zM288 48c0.0234375 0 0.0458984 -0.000976562 0.0703125 -0.000976562c79.4365 0 143.93 64.4922 143.93 143.93v0.0712891 +c0 79.4756 -64.5244 144 -144 144s-144 -64.5244 -144 -144s64.5244 -144 144 -144zM288 288c0.0761719 0 0.160156 -0.0273438 0.237305 -0.0273438c52.8623 0 95.7803 -42.917 95.7803 -95.7793s-42.918 -95.7803 -95.7803 -95.7803s-95.7803 42.918 -95.7803 95.7803 +c0 8.68945 1.16016 17.1104 3.33301 25.1162c7.93164 -5.83594 17.7432 -9.26758 28.3359 -9.26758c26.4092 0 47.8496 21.4404 47.8496 47.8496c0 10.5938 -3.44922 20.3867 -9.28516 28.3184c8.0459 2.34277 16.541 3.66797 25.3096 3.79004z" /> +d="M320 48c8.91309 0.0830078 17.542 0.976562 26 2.61035l51.8896 -40.1504c-25.0195 -6.45996 -50.9795 -10.46 -77.8896 -10.46c-122.93 0 -230.29 71.5898 -284.52 177.4c-2.21387 4.37793 -3.46094 9.38965 -3.46094 14.626c0 5.2373 1.24707 10.1855 3.46094 14.5635 +c10.2393 20 22.9297 38.29 36.7197 55.5898l104.899 -81.0693c5.65039 -74.4004 67.0508 -133.11 142.9 -133.11zM633.82 -10.0996c3.76855 -2.92871 6.17676 -7.50977 6.17676 -12.6475c0 -3.69238 -1.25293 -7.09375 -3.35742 -9.80273l-19.6396 -25.2705 +c-2.92871 -3.76855 -7.50879 -6.17578 -12.6465 -6.17578c-3.69727 0 -7.10254 1.25684 -9.81348 3.36621l-588.36 454.729c-3.76562 2.92871 -6.1709 7.50781 -6.1709 12.6426c0 3.69434 1.25488 7.09863 3.36133 9.80762l19.6299 25.2705 +c2.92871 3.76855 7.50879 6.17578 12.6465 6.17578c3.69727 0 7.10254 -1.25684 9.81348 -3.36621l127.22 -98.3301c43.6846 23.8564 94.0967 37.6357 147.32 37.7002c122.93 0 230.29 -71.5898 284.52 -177.4c2.21387 -4.37793 3.46094 -9.38965 3.46094 -14.626 +c0 -5.2373 -1.24707 -10.1855 -3.46094 -14.5635c-20.2109 -39.3887 -47.6904 -73.7881 -81.25 -102.07zM450.1 131.9c8.61035 18.3203 13.9004 38.4697 13.9004 60.0996c0 0.0273438 0.00195312 0.0527344 0.00195312 0.0800781c0 79.4316 -64.4883 143.92 -143.92 143.92 +h-0.0820312c-34.6328 -0.0253906 -66.4756 -12.4902 -91.1504 -33.1104l73.6104 -56.8896c0.857422 3.20508 1.38867 6.5625 1.54004 10c-0.0185547 10.5391 -3.49023 20.3242 -9.30957 28.21c8.43164 2.46191 17.3359 3.82031 26.5576 3.82031 +c52.2998 0 94.7607 -42.46 94.7607 -94.7598c0 -0.423828 -0.00292969 -0.847656 -0.00878906 -1.27051c-0.138672 -10.377 -1.97559 -20.4014 -5.2002 -29.7197z" /> +l43.2002 -57.5996h102.86l-49.0303 171.61c-2.91992 10.2197 4.75 20.3896 15.3799 20.3896h65.5c5.95117 0 11.1396 -3.23633 13.9004 -8.05957l105.1 -183.94h114.29z" /> +d="M504.971 88.9707c9.37305 -9.37305 9.37305 -24.5684 0 -33.9404l-80 -79.9844c-15.0098 -15.0098 -40.9707 -4.49023 -40.9707 16.9707v39.9834h-58.7852c-3.46094 0 -6.58105 1.46484 -8.77246 3.81152l-70.5566 75.5967l53.333 57.1426l52.7812 -56.5508h32v39.9814 +c0 21.4375 25.9434 31.9971 40.9707 16.9707zM12 272c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h110.785c3.46094 0 6.58203 -1.46484 8.77246 -3.81152l70.5566 -75.5967l-53.333 -57.1426l-52.7812 56.5508h-84zM384 272h-32l-220.442 -236.188 +c-2.26953 -2.43066 -5.44629 -3.81152 -8.77246 -3.81152h-110.785c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h84l220.442 236.188c2.19141 2.34668 5.31152 3.81152 8.77246 3.81152h58.7852v39.9814c0 21.4365 25.9434 31.9971 40.9707 16.9697 +l80 -79.9814c9.37305 -9.37207 9.37305 -24.5674 0 -33.9404l-80 -79.9844c-15.0098 -15.0088 -40.9707 -4.48926 -40.9707 16.9707v39.9844z" /> +d="M164.07 299.9h-152.07c-6.62305 0 -12 5.37695 -12 12v80c0 19.8682 16.1309 36 36 36h104c19.8691 0 36 -16.1318 36 -36v-80c0 -0.0380859 0.000976562 -0.0751953 0.000976562 -0.112305c0 -6.5625 -5.32812 -11.8906 -11.8906 -11.8906 +c-0.0136719 0 -0.0263672 0.00292969 -0.0400391 0.00292969zM512 311.9c0 -0.00390625 0.00195312 -0.0078125 0.00195312 -0.0107422c0 -6.5625 -5.32715 -11.8906 -11.8896 -11.8906c-0.0380859 0 -0.0751953 0.000976562 -0.112305 0.000976562h-152 +c-6.62305 0 -12 5.37695 -12 12v80c0 19.8691 16.1309 36 36 36h104c19.8691 0 36 -16.1309 36 -36v-80.0996zM348 267.9h151.85c6.62305 0 12.001 -5.37598 12.001 -11.998c0 -0.0341797 0 -0.0683594 -0.000976562 -0.102539 +c-0.199219 -20.2002 -0.599609 -40.3994 0 -53.2002c0 -150.699 -134.42 -246.699 -255 -246.699s-256.75 96 -256.75 246.6c0.600586 13 0.100586 31.9004 0 53.2998v0.100586c0 6.62305 5.37695 12 12 12h151.9c6.62305 0 12 -5.37695 12 -12v-52 +c0 -127.9 160 -128.101 160 0v52c0 6.62305 5.37695 12 12 12z" /> @@ -422,10 +722,10 @@ d="M207.029 66.5244l-194.344 194.344c-9.37207 9.37305 -9.37207 24.5684 0 33.9404 c9.37207 -9.37305 9.37207 -24.5684 0 -33.9404l-194.343 -194.344c-9.37305 -9.37207 -24.5684 -9.37207 -33.9414 0z" /> +l-40.416 42.792v-182.119h187.548c6.62305 0 12.627 -2.68457 16.9707 -7.0293z" /> +d="M400 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h352zM94 32c160.055 0 290 129.708 290 290c0 7.11621 -4.97559 13.0801 -11.6279 14.6143l-65 14.998 +c-1.08691 0.250977 -2.20312 0.394531 -3.36621 0.394531c-6.18457 0 -11.501 -3.75195 -13.7939 -9.10156l-30 -69.998c-0.775391 -1.81055 -1.22266 -3.81055 -1.22266 -5.90332c0 -4.68066 2.14844 -8.86328 5.51172 -11.6152l37.8857 -30.9971 +c-22.4834 -47.9219 -61.8369 -87.8164 -110.78 -110.779l-30.9971 37.8848c-2.75195 3.36328 -6.94043 5.49414 -11.6211 5.49414c-2.09277 0 -4.08691 -0.429688 -5.89746 -1.20508l-69.998 -29.999c-5.34961 -2.29297 -9.08984 -7.59375 -9.08984 -13.7783 +c0 -1.16309 0.131836 -2.29492 0.382812 -3.38184l14.998 -65c1.55957 -6.75391 7.58301 -11.627 14.6162 -11.627z" /> @@ -512,11 +812,11 @@ c-8.41406 0 -15.4707 6.49023 -16.0176 14.8867c-7.29883 112.07 -96.9404 201.488 - M447.99 -15.4971c0.324219 -9.03027 -6.97168 -16.5029 -16.0049 -16.5039h-48.0684c-8.62598 0 -15.6455 6.83496 -15.999 15.4531c-7.83789 191.148 -161.286 344.626 -352.465 352.465c-8.61816 0.354492 -15.4531 7.37402 -15.4531 15.999v48.0684 c0 9.03418 7.47266 16.3301 16.5029 16.0059c234.962 -8.43555 423.093 -197.667 431.487 -431.487z" /> +d="M576 144v-96c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v96c0 26.5098 21.4902 48 48 48h480c26.5098 0 48 -21.4902 48 -48zM528 224h-480c-0.0234375 0 -0.0996094 -0.0361328 -0.124023 -0.0361328 +c-10.8613 0 -21.2168 -2.18066 -30.6533 -6.12891l96.5283 144.791c8.60742 12.9102 23.2725 21.374 39.9385 21.374h268.621c16.667 0 31.332 -8.46387 39.9395 -21.374l96.5273 -144.791c-9.43652 3.94824 -19.8447 6.16504 -30.7061 6.16504h-0.0712891zM480 128 +c-17.6729 0 -32 -14.3271 -32 -32s14.3271 -32 32 -32s32 14.3271 32 32s-14.3271 32 -32 32zM384 128c-17.6729 0 -32 -14.3271 -32 -32s14.3271 -32 32 -32s32 14.3271 32 32s-14.3271 32 -32 32z" /> @@ -572,13 +872,14 @@ d="M507.73 338.9c11.7891 -47.4102 -0.84082 -99.6602 -37.9102 -136.73c-39.9004 -3 c-16.5 50.1006 -5.58984 107.561 34.0498 147.2c37.0303 37.0195 89.2002 49.6699 136.58 37.9297c9.08984 -2.25977 12.2803 -13.54 5.66016 -20.1602l-74.3604 -74.3594l11.3105 -67.8799l67.8799 -11.3105l74.3604 74.3604 c6.58008 6.58008 17.8799 3.51953 20.1201 -5.50977zM64 -24c13.25 0 24 10.75 24 24c0 13.2598 -10.75 24 -24 24s-24 -10.7402 -24 -24c0 -13.25 10.75 -24 24 -24z" /> +d="M139.61 412.5l17 -16.5c2.13281 -2.18066 3.44922 -5.16797 3.44922 -8.45605c0 -3.33496 -1.35352 -6.35547 -3.54004 -8.54395l-72.1992 -72.1904l-15.5898 -15.6191c-2.29297 -2.17969 -5.39941 -3.51758 -8.80859 -3.51758 +c-3.41016 0 -6.50977 1.33789 -8.80176 3.51758l-47.5898 47.3994c-2.18066 2.17383 -3.53125 5.18262 -3.53125 8.50195c0 3.31836 1.35059 6.3252 3.53125 8.49805l15.7002 15.7197c2.17285 2.18164 5.18164 3.53125 8.50098 3.53125s6.3252 -1.34961 8.49902 -3.53125 +l22.6992 -22.1191l63.6807 63.3096c2.17285 2.18066 5.18262 3.53125 8.50098 3.53125c3.31934 0 6.3252 -1.35059 8.49902 -3.53125zM139.61 253.31l16.9795 -17c2.125 -2.16504 3.43652 -5.13574 3.43652 -8.40625c0 -3.31641 -1.34863 -6.32031 -3.52637 -8.49316 +l-72.2002 -72.2197l-15.7002 -15.6904c-2.29004 -2.17871 -5.39551 -3.5166 -8.80273 -3.5166c-3.4082 0 -6.50586 1.33789 -8.79688 3.5166l-47.4697 47.5c-2.18066 2.17285 -3.53125 5.18262 -3.53125 8.50195c0 3.31836 1.35059 6.3252 3.53125 8.49805l15.7002 15.6904 +c2.17285 2.18066 5.18164 3.53125 8.50098 3.53125s6.3252 -1.35059 8.49902 -3.53125l22.6992 -22.1006l63.6807 63.7197c2.17285 2.18164 5.18262 3.53125 8.50098 3.53125c3.31934 0 6.3252 -1.34961 8.49902 -3.53125zM64 80c26.4922 0 48 -21.5078 48 -48 +s-21.5078 -48 -48 -48c-26.4697 0 -48.5898 21.5 -48.5898 48s22.0996 48 48.5898 48zM496 64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-288c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h288zM496 384 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-288c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h288zM496 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-288c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h288z" /> @@ -598,11 +899,11 @@ c-40.2998 -22.1006 -68.8994 -62 -75.1992 -109.4h-65.9004c-17.7002 0 -32 14.2998 +c-13.2549 0 -24 10.7451 -24 24v368c0 13.2549 10.7451 24 24 24h168v-104zM440.971 375.029c4.34473 -4.34473 7.0293 -10.3477 7.0293 -16.9707v-6.05859h-96v96h6.05859c6.62305 0 12.626 -2.68457 16.9707 -7.0293z" /> @@ -634,30 +935,30 @@ d="M400 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 - d="M16 316c-8.83691 0 -16 7.16309 -16 16v40c0 8.83691 7.16309 16 16 16h416c8.83691 0 16 -7.16309 16 -16v-40c0 -8.83691 -7.16309 -16 -16 -16h-416zM16 156c-8.83691 0 -16 7.16309 -16 16v40c0 8.83691 7.16309 16 16 16h416c8.83691 0 16 -7.16309 16 -16v-40 c0 -8.83691 -7.16309 -16 -16 -16h-416zM16 -4c-8.83691 0 -16 7.16309 -16 16v40c0 8.83691 7.16309 16 16 16h416c8.83691 0 16 -7.16309 16 -16v-40c0 -8.83691 -7.16309 -16 -16 -16h-416z" /> +d="M48 400c26.4922 0 48 -21.5078 48 -48s-21.5078 -48 -48 -48s-48 21.5078 -48 48s21.5078 48 48 48zM48 240c26.4922 0 48 -21.5078 48 -48s-21.5078 -48 -48 -48s-48 21.5078 -48 48s21.5078 48 48 48zM48 80c26.4922 0 48 -21.5078 48 -48s-21.5078 -48 -48 -48 +s-48 21.5078 -48 48s21.5078 48 48 48zM496 64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 384c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16 +h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320z" /> +c4.76758 -1.95996 10.0107 -3.07617 15.4707 -3.11914c10.1602 0 14.3594 3.5 14.3594 8.21973c0 6.64941 -5.60938 9.08984 -15.9395 9.08984h-4.73047c-5.95996 0 -9.25 2.12012 -12.25 7.87988l-1.0498 1.92969c-2.4502 4.75 -1.2002 9.81055 2.7998 14.8809l5.61035 7 +c3.47461 4.32422 7.0957 8.37695 11 12.3096h-22.8301c-4.41504 0 -8 3.58496 -8 8v16c0 4.41504 3.58496 8 8 8h57c7.5 0 11.3398 -4 11.3398 -11.3496v-3.31055c0.0136719 -0.299805 0.0175781 -0.595703 0.0175781 -0.899414 +c0 -5.10449 -1.9248 -9.76367 -5.08789 -13.29zM496 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 384c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM496 64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-320c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h320zM16 288 +c-4.41504 0 -8 3.58496 -8 8v16c0 4.41504 3.58496 8 8 8h16v64h-8c-4.4082 0.0078125 -7.98145 3.59766 -7.98145 8.00781c0 1.2832 0.303711 2.49707 0.841797 3.57227l8 16c1.31055 2.62012 4.01367 4.41406 7.13965 4.41992h24c4.41504 0 8 -3.58496 8 -8v-88h16 +c4.41504 0 8 -3.58496 8 -8v-16c0 -4.41504 -3.58496 -8 -8 -8h-64zM12.0898 128c-7.00977 0 -12.0898 4 -12.0898 11.4102v4c0 47.2803 51 56.3994 50.9697 69.1201c0 7.18945 -5.9502 8.75 -9.2793 8.75c-0.0185547 0 -0.0380859 0.000976562 -0.0566406 0.000976562 +c-3.65918 0 -6.97949 -1.46582 -9.40332 -3.84082c-5.12012 -4.91016 -10.5107 -7 -16.1201 -2.44043l-8.58008 6.87988c-5.7998 4.53027 -7.16992 9.78027 -2.7998 15.3701c6.65918 8.75 19.0996 18.75 40.46 18.75c19.4697 0 44.4697 -10.5 44.4697 -39.5596 +c0 -37.7607 -45.0498 -46.1504 -48.3398 -56.4404h38.6797c4.41504 0 8 -3.58496 8 -8v-16c0 -4.41504 -3.58496 -8 -8 -8h-67.9102z" /> +d="M496 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-480c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h102.29c-11.6797 16.5303 -19.7803 35.4697 -21.7803 56.3604c-0.385742 3.97559 -0.577148 7.88281 -0.577148 11.96 +c0 68.2266 55.3633 123.624 123.577 123.68h68c50.1416 0 93.5244 -28.7686 114.521 -70.7998l0.529297 -1c1.07324 -2.14844 1.70215 -4.57715 1.70215 -7.13965c0 -6.26562 -3.61035 -11.6953 -8.86133 -14.3203l-42.9404 -21.4707 +c-2.14941 -1.07324 -4.5791 -1.70312 -7.14355 -1.70312c-6.2627 0 -11.6895 3.60645 -14.3164 8.85352c-8.18652 16.374 -25.0859 27.5801 -44.623 27.5801h-0.0371094h-66.79c-24.0352 -0.000976562 -43.5479 -19.5059 -43.5479 -43.541 +c0 -19.5742 12.9414 -36.1494 30.7285 -41.6289l87.1699 -26.8301h202.1zM315.76 128h94.3906c2.6084 -7.7373 4.44434 -15.9834 5.33984 -24.3604c0.385742 -3.97559 0.577148 -7.88281 0.577148 -11.96c0 -68.2266 -55.3633 -123.624 -123.577 -123.68h-68 +c-50.1416 0 -93.5244 28.7686 -114.521 70.7998l-0.529297 1c-1.07324 2.14844 -1.70215 4.57715 -1.70215 7.13965c0 6.26562 3.61035 11.6953 8.86133 14.3203l42.9404 21.4707c2.14941 1.07324 4.5791 1.70312 7.14355 1.70312 +c6.2627 0 11.6895 -3.60645 14.3164 -8.85352c8.18652 -16.374 25.0859 -27.5801 44.623 -27.5801h0.0371094h66.79c24.0254 0.0224609 43.5273 19.5244 43.5498 43.5498c-0.0117188 15.3828 -8.07227 28.8594 -20.2402 36.4502z" /> +d="M32 384c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h144c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32v-160c0 -44.1533 35.8467 -80 80 -80s80 35.8467 80 80v160h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h144c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32v-160c0 -88.2197 -71.7803 -160 -160 -160s-160 71.7803 -160 160v160h-32zM432 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h416z" /> +M448 198.059v-6.05859h-96v96h6.05859c6.62305 0 12.626 -2.68457 16.9707 -7.0293l65.9404 -65.9404c4.34473 -4.34473 7.03027 -10.3477 7.03027 -16.9717z" /> @@ -764,7 +1065,7 @@ d="M544 224c96 -21.333 96 -26.583 96 -32s0 -10.667 -96 -32l-128 -16l-48 -16h-24l l64 8v2.66602h-48v16h-8v69.333l10.667 10.667h34.666l66.667 -80h48v164h-16v12h114.667c11.666 0 21.333 -2.625 21.333 -6s-9.66699 -6 -21.333 -6h-39.5088l116.842 -148h24l48 -16z" /> +c15.4062 13.3047 39.6865 2.50293 39.6865 -18.1641v-15.8174l-108.607 -93.7861c-11.8906 -10.2637 -19.3926 -25.4307 -19.3926 -42.3564v-0.0234375c0 -0.0078125 -0.0292969 -0.00292969 -0.0292969 -0.0117188c0 -16.9268 7.53125 -32.1084 19.4229 -42.373 +l108.606 -93.7852v-15.8184c0 -20.7002 -24.2998 -31.4531 -39.6865 -18.1641z" /> +d="M496 288c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v96h-16c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32 +c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-144h16zM336 384c8.83105 0 16 -7.16895 16 -16v-48c0 -8.83105 -7.16895 -16 -16 -16h-33.4805l-77.8096 -112l77.8096 -112h33.4805c8.83105 0 16 -7.16895 16 -16v-48 +c0 -8.83105 -7.16895 -16 -16 -16h-67c-5.41113 0.0273438 -10.1836 2.73047 -13.0596 6.87012l-79.9004 115l-79.9004 -115c-2.89062 -4.16016 -7.69531 -6.87012 -13.1396 -6.87012h-67c-8.83105 0 -16 7.16895 -16 16v48c0 8.83105 7.16895 16 16 16h33.4805l77.8096 112 +l-77.8096 112h-33.4805c-8.83105 0 -16 7.16895 -16 16v48c0 8.83105 7.16895 16 16 16h67c5.41113 -0.0273438 10.1836 -2.73047 13.0596 -6.87012l79.9004 -115l79.9004 115c2.89062 4.16016 7.69531 6.87012 13.1396 6.87012h67z" /> +d="M496 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v96h-16c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32 +c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-144h16zM336 384c8.83105 0 16 -7.16895 16 -16v-48c0 -8.83105 -7.16895 -16 -16 -16h-33.4805l-77.8096 -112l77.8096 -112h33.4805c8.83105 0 16 -7.16895 16 -16v-48 +c0 -8.83105 -7.16895 -16 -16 -16h-67c-5.41113 0.0273438 -10.1836 2.73047 -13.0596 6.87012l-79.9004 115l-79.9004 -115c-2.89062 -4.16016 -7.69531 -6.87012 -13.1396 -6.87012h-67c-8.83105 0 -16 7.16895 -16 16v48c0 8.83105 7.16895 16 16 16h33.4805l77.8096 112 +l-77.8096 112h-33.4805c-8.83105 0 -16 7.16895 -16 16v48c0 8.83105 7.16895 16 16 16h67c5.41113 -0.0273438 10.1836 -2.73047 13.0596 -6.87012l79.9004 -115l79.9004 115c2.89062 4.16016 7.69531 6.87012 13.1396 6.87012h67z" /> +c0.136719 3.79004 1.03223 7.43164 2.51562 10.7031l49.4355 98.8125c7.33008 14.6094 26.5391 26.4688 42.8867 26.4844h104.215c46.2168 72.7969 108.122 128 211.354 128c25.0996 0 50.3086 0 82.5059 -6.90625c5.54883 -1.1875 11.0176 -6.65625 12.207 -12.1875z +M384.04 280c22.0752 0.0078125 39.9971 17.9258 40.0098 40c0 22.0762 -17.9229 40 -40 40c-22.0762 0 -40 -17.9238 -40 -40c0 -22.0732 17.918 -39.9951 39.9902 -40z" /> @@ -987,12 +1288,12 @@ c2.2998 -2.30078 6.09961 -2.30078 8.5 0l23.0996 23.0996c9.2998 9.2998 9.2998 24. +c3.36816 -0.485352 6.75977 -0.711914 10.2607 -0.711914c8.3877 0 16.4424 1.44043 23.9287 4.08887c7.81348 2.76367 16.0107 -3.01465 16.0107 -11.3027v-88.8057c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48 +h121.033c12.5508 0 16.6748 -16.8301 5.54492 -22.6309c-18.7773 -9.78613 -36.0615 -22.1084 -51.0137 -37.6758c-2.18164 -2.27637 -5.25098 -3.69141 -8.64844 -3.69336h-50.916v-320h320v68.8721z" /> +M374.14 291.95c7.61035 16.6494 -9.54004 33.7998 -26.1895 26.2002l-144.34 -65.9707c-6.98438 -3.19238 -12.5781 -8.78516 -15.7705 -15.7695l-65.9795 -144.351c-7.61035 -16.6494 9.5498 -33.8096 26.1992 -26.1992l144.341 65.9697 +c6.9834 3.19238 12.5771 8.78613 15.7695 15.7695z" /> @@ -1006,8 +1307,8 @@ c-7.56055 7.56055 -20.4854 2.20605 -20.4854 -8.48438v-246.06c0 -10.6904 12.9258 d="M310.706 34.2354l8.81836 -44.4902c1.23828 -6.24902 -2.62109 -12.3623 -8.78809 -13.957c-12.5391 -3.24414 -34.8008 -7.78809 -61.1016 -7.78809c-104.371 0 -182.496 65.3076 -207.521 155.64h-30.1143c-6.62695 0 -12 5.37305 -12 12v28.3604 c0 6.62695 5.37305 12 12 12h21.3877c-1 12.958 -0.828125 28.6377 0.181641 42.2451h-21.5693c-6.62695 0 -12 5.37305 -12 12v29.7549c0 6.62695 5.37305 12 12 12h33.0752c28.9551 83.748 107.376 144 204.56 144c21.0752 0 40.582 -2.91211 52.6865 -5.20703 c6.86035 -1.30078 11.1475 -8.17578 9.32617 -14.917l-11.9912 -44.3682c-1.65527 -6.125 -7.78613 -9.89062 -14.002 -8.62305c-9.28711 1.89551 -23.3652 4.14551 -37.8516 4.14551c-54.9287 0 -96.9854 -30.0391 -117.619 -75.0303h138.278 -c7.66211 0 13.3613 -7.08203 11.7227 -14.5664l-6.51172 -29.7549c-1.13965 -5.20703 -6.3916 -9.43359 -11.7227 -9.43359v0h-146.593c-1.55176 -13.958 -1.34766 -27.917 -0.137695 -42.2451h134.237c7.68945 0 13.3936 -7.12891 11.708 -14.6309l-6.37305 -28.3604 -c-1.16211 -5.17188 -6.40723 -9.36914 -11.708 -9.36914h-113.689c19.5322 -50.6582 64.6982 -85.4482 121.462 -85.4482c18.0039 0 34.7334 2.97363 45.4258 5.41211c6.58887 1.50391 13.1094 -2.73828 14.4238 -9.36816z" /> +c7.66211 0 13.3613 -7.08203 11.7227 -14.5664l-6.51172 -29.7549c-1.17969 -5.3877 -5.9834 -9.43359 -11.7227 -9.43359h-146.593c-1.55176 -13.958 -1.34766 -27.917 -0.137695 -42.2451h134.237c7.68945 0 13.3936 -7.12891 11.708 -14.6309l-6.37305 -28.3604 +c-1.20312 -5.35547 -5.99121 -9.36914 -11.708 -9.36914h-113.689c19.5322 -50.6582 64.6982 -85.4482 121.462 -85.4482c18.0039 0 34.7334 2.97363 45.4258 5.41211c6.58887 1.50391 13.1094 -2.73828 14.4238 -9.36816z" /> +d="M176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555l-80 96c-10.0703 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32 +c8.83105 0 16 -7.16895 16 -16v-304h48zM416 160c8.83105 0 16 -7.16895 16 -16v-17.6299c0 -9.51074 -4.14355 -18.0566 -10.7402 -23.9199l-61.2598 -70.4502h56c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128c-8.83105 0 -16 7.16895 -16 16 +v17.6299c0 9.51074 4.14355 18.0566 10.7402 23.9199l61.2598 70.4502h-56c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128zM447.06 245.38c0.600586 -1.67969 0.931641 -3.49512 0.931641 -5.37988c0 -8.82812 -7.16406 -15.9951 -15.9912 -16h-24.8398 +c-0.015625 0 -0.0263672 -0.00195312 -0.0419922 -0.00195312c-7.11426 0 -13.1514 4.6543 -15.2285 11.082l-4.40918 12.9199h-71l-4.4209 -12.9199c-2.07617 -6.42773 -8.10938 -11.0801 -15.2246 -11.0801h-0.00488281h-24.8301 +c-8.82715 0.00488281 -15.9863 7.17773 -15.9863 16.0049c0 1.88574 0.326172 3.69531 0.926758 5.375l59.2695 160c2.20996 6.19043 8.125 10.6201 15.0703 10.6201h41.4395c6.94531 0 12.8604 -4.42969 15.0703 -10.6201zM335.61 304h32.7793l-16.3896 48z" /> +d="M304 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h64zM176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96 +c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555l-80 96c-10.0801 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32c8.83105 0 16 -7.16895 16 -16v-304h48zM432 288 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-192c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h192zM368 160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h128zM496 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-256c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h256z" /> +d="M304 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h64zM16 288c-14.2305 0 -21.3496 17.2598 -11.3096 27.3096l80 96c2.89551 2.89453 6.89844 4.68555 11.3115 4.68555 +c4.41406 0 8.41211 -1.79102 11.3076 -4.68555l80 -96c10.0703 -10.0693 2.90039 -27.3096 -11.3096 -27.3096h-48v-304c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v304h-48zM432 288c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-192c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h192zM368 160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128zM496 416 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-256c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h256z" /> +d="M304 352c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-112h16c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v64h-16zM330.15 189.09c53.4502 14.25 101.85 -25.8799 101.869 -77.0898v-10.7695c0 -70.3906 -28.25 -107.24 -86.25 -132 +c-8.36914 -3.58008 -18.0293 1.2793 -20.8994 9.90918l-9.90039 20c-2.62012 7.87012 0.610352 16.9404 8.18066 20.3408c7.59961 3.28516 14.6064 7.64258 20.8496 12.9092c-47.6396 4.76074 -83.0996 51.4805 -68.8496 102.53c7.62793 26.2793 28.5596 46.9287 55 54.1699 +zM352 92c11.0381 0 20 8.96191 20 20s-8.96191 20 -20 20s-20 -8.96191 -20 -20s8.96191 -20 20 -20zM176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555 +l-80 96c-10.0703 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32c8.83105 0 16 -7.16895 16 -16v-304h48z" /> +c7.59961 3.28516 14.6064 7.64258 20.8496 12.9092c-47.6396 4.76074 -83.0996 51.4805 -68.8301 102.53c7.62891 26.2793 28.5596 46.9287 55 54.1699zM352 92c11.0381 0 20 8.96191 20 20s-8.96191 20 -20 20s-20 -8.96191 -20 -20s8.96191 -20 20 -20zM304 352 +c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-112h16c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-96 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v64h-16zM107.31 411.31l80 -96c10.0703 -10.0693 2.90039 -27.3096 -11.3096 -27.3096h-48v-304c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v304h-48 +c-14.2197 0 -21.3496 17.2598 -11.3096 27.3096l80 96c2.89551 2.89453 6.89844 4.68555 11.3115 4.68555c4.41406 0 8.41211 -1.79102 11.3076 -4.68555z" /> +c-20.1826 0 -29.4854 39.293 -33.9307 57.7949c-5.20605 21.666 -10.5889 44.0703 -25.3936 58.9023c-32.4688 32.5234 -49.5029 73.9668 -89.1172 113.11c-2.19727 2.17285 -3.55762 5.19043 -3.55762 8.52148v213.77c0 6.54102 5.24316 11.8779 11.7832 11.998 +c15.8311 0.290039 36.6934 9.0791 52.6504 16.1787c31.7549 14.127 71.2744 31.708 119.561 31.7246h2.84375c42.7773 0 93.3633 -0.413086 113.774 -29.7373c8.3916 -12.0566 10.4453 -27.0342 6.14746 -44.6318c16.3125 -17.0527 25.0635 -48.8633 16.3818 -74.7568 +c17.5439 -23.4316 19.1436 -56.1318 9.30859 -79.4688l0.109375 -0.110352c11.8936 -11.9492 19.5234 -31.2588 19.4395 -49.1973c-0.15625 -30.3516 -26.1572 -58.0977 -59.5527 -58.0977h-101.725c7.30762 -28.3398 33.2773 -52.1318 33.2773 -94.5479 +c0 -73.4521 -48 -81.4521 -72 -81.4521z" /> +c-15.6172 0 -27.0654 14.6953 -23.2832 29.8213l48 192c2.6084 10.4316 12.0488 18.1787 23.2832 18.1787h11.3604c23.6895 -10.8936 50.5684 -10.4434 73.2793 0h11.3604c11.2344 0 20.6748 -7.74707 23.2832 -18.1787z" /> @@ -1105,7 +1405,7 @@ l-100.399 33.5l-47.2998 -94.7002c-6.40039 -12.7998 -24.6006 -12.7998 -31 0l-47.3 c-4.59961 13.5 8.2998 26.4004 21.9004 21.9004l100.5 -33.5l47.2998 94.7002c6.40039 12.7998 24.5996 12.7998 31 0l47.4004 -94.8008l100.399 33.5c13.5 4.60059 26.4004 -8.2998 21.9004 -21.8994l-33.5 -100.4zM346.5 101.5c49.9004 49.9004 49.9004 131.1 0 181 s-131.1 49.9004 -181 0s-49.9004 -131.1 0 -181s131.1 -49.9004 181 0z" /> +c0 5.72656 4.02734 10.5205 9.39746 11.7139l54.6025 12.1338v30.4395l-49.3975 -10.9775c-7.49316 -1.66602 -14.6025 4.03711 -14.6025 11.7139v40.9766c0 5.72656 4.02734 10.5205 9.39746 11.7139l54.6025 12.1338v68.9971c0 6.62695 5.37305 12 12 12h56 +c6.62695 0 12 -5.37305 12 -12v-51.2188l129.397 28.7539c7.49316 1.66602 14.6025 -4.03711 14.6025 -11.7139v-40.9756c0 -5.72656 -4.02734 -10.5205 -9.39746 -11.7139l-134.603 -29.9121v-30.4385l129.397 28.7539c7.49316 1.66602 14.6025 -4.03711 14.6025 -11.7139 +v-40.9766c0 -5.72656 -4.02734 -10.5205 -9.39746 -11.7139l-134.603 -29.9121v-159.219c86.1787 0 168 48 168 148.754c0 6.33398 5.63965 11.2461 11.9746 11.2461h48.0195z" /> +c-4.41504 0 -8 -3.58496 -8 -8v-64c0 -4.41504 3.58496 -8 8 -8z" /> +d="M496 320v-16c0 -4.41504 -3.58496 -8 -8 -8h-24v-12c0 -6.62695 -5.37305 -12 -12 -12h-392c-6.62695 0 -12 5.37305 -12 12v12h-24c-4.41504 0 -8 3.58496 -8 8v16c0 3.33398 2.03906 6.19141 4.94141 7.3916l232 88 +c0.94043 0.389648 1.97168 0.605469 3.05371 0.605469c1.08105 0 2.12305 -0.21582 3.06348 -0.605469l232 -88c2.90234 -1.2002 4.94141 -4.05762 4.94141 -7.3916zM472 16c13.2549 0 24 -10.7451 24 -24v-16c0 -4.41504 -3.58496 -8 -8 -8h-464 +c-4.41504 0 -8 3.58496 -8 8v16c0 13.2549 10.7451 24 24 24h432zM96 256h64v-192h64v192h64v-192h64v192h64v-192h36c6.62695 0 12 -5.37305 12 -12v-20h-416v20c0 6.62695 5.37305 12 12 12h36v192z" /> +l9.40039 -31.9004c1.4668 -4.96582 6.06152 -8.5957 11.5 -8.59961h22.8994c8.2998 0 14 8.09961 11.4004 15.9004l-57.5 169.1c-1.7002 4.7998 -6.2998 8.09961 -11.4004 8.09961h-32.5c-5.2002 0 -9.7002 -3.19922 -11.3994 -8.09961z" /> +d="M480 288c17.6611 0 32 -14.3389 32 -32v-288c0 -17.6611 -14.3389 -32 -32 -32h-320c-17.6611 0 -32 14.3389 -32 32v448c0 17.6611 14.3389 32 32 32h242.75c8.82715 -0.000976562 16.8291 -3.58008 22.6201 -9.37012l45.25 -45.25 +c5.7959 -5.79199 9.37891 -13.7979 9.37988 -22.6299v-82.75zM288 16v32c0 8.83105 -7.16895 16 -16 16h-32c-8.83105 0 -16 -7.16895 -16 -16v-32c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16zM288 144v32c0 8.83105 -7.16895 16 -16 16h-32 +c-8.83105 0 -16 -7.16895 -16 -16v-32c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16zM416 16v32c0 8.83105 -7.16895 16 -16 16h-32c-8.83105 0 -16 -7.16895 -16 -16v-32c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16zM416 144v32 +c0 8.83105 -7.16895 16 -16 16h-32c-8.83105 0 -16 -7.16895 -16 -16v-32c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16zM416 256v64h-48c-8.83105 0 -16 7.16895 -16 16v48h-160v-128h224zM64 320c17.6611 0 32 -14.3389 32 -32v-320 +c0 -17.6611 -14.3389 -32 -32 -32h-32c-17.6611 0 -32 14.3389 -32 32v320c0 17.6611 14.3389 32 32 32h32z" /> +d="M384 326.059v-6.05859h-128v128h6.05859c6.36523 0 12.4707 -2.5293 16.9717 -7.0293l97.9404 -97.9404c4.34375 -4.34473 7.0293 -10.3486 7.0293 -16.9717zM248 288h136v-328c0 -13.2549 -10.7451 -24 -24 -24h-336c-13.2549 0 -24 10.7451 -24 24v464 +c0 13.2549 10.7451 24 24 24h200v-136c0 -13.2002 10.7998 -24 24 -24zM123.206 47.4951l19.5791 20.8838c0.905273 0.96582 1.46289 2.26562 1.46289 3.69238c0 1.61426 -0.709961 3.06445 -1.83496 4.05469l-40.7627 35.874l40.7627 35.874 +c1.125 0.990234 1.83203 2.44043 1.83203 4.05566c0 1.42676 -0.554688 2.72559 -1.45996 3.69141l-19.5791 20.8848c-0.985352 1.05176 -2.3877 1.70703 -3.94141 1.70703c-1.42676 0 -2.72559 -0.555664 -3.69141 -1.46094l-64.8662 -60.8115 +c-1.05078 -0.986328 -1.70801 -2.38672 -1.70801 -3.93945c0 -1.55371 0.657227 -2.9541 1.70801 -3.94043l64.8662 -60.8115c0.96582 -0.905273 2.26562 -1.46289 3.69336 -1.46289c1.55273 0 2.9541 0.657227 3.93945 1.70898zM174.501 -2.98438 +c0.478516 -0.138672 0.982422 -0.212891 1.50488 -0.212891c2.45801 0 4.53418 1.64551 5.18555 3.89453l61.4395 211.626c0.138672 0.478516 0.213867 0.982422 0.213867 1.50488c0 2.45801 -1.64551 4.53418 -3.89355 5.18652l-27.4521 7.9707 +c-0.477539 0.138672 -0.981445 0.212891 -1.50391 0.212891c-2.45801 0 -4.53516 -1.64551 -5.18848 -3.89453l-61.4395 -211.626c-0.138672 -0.477539 -0.212891 -0.981445 -0.212891 -1.50293c0 -2.45898 1.64551 -4.53516 3.89355 -5.18848zM335.293 108.061 +c1.05176 0.986328 1.70898 2.38672 1.70898 3.94043c0 1.55273 -0.657227 2.95312 -1.70801 3.93945l-64.8662 60.8115c-0.96582 0.905273 -2.26562 1.46289 -3.69336 1.46289c-1.55273 0 -2.9541 -0.657227 -3.93945 -1.70898l-19.5801 -20.8848 +c-0.905273 -0.96582 -1.46289 -2.26562 -1.46289 -3.69238c0 -1.61426 0.709961 -3.06445 1.83496 -4.05469l40.7627 -35.874l-40.7637 -35.873c-1.125 -0.990234 -1.83203 -2.44043 -1.83203 -4.05566c0 -1.42676 0.554688 -2.72559 1.45996 -3.69141l19.5801 -20.8848 +c0.985352 -1.05176 2.3877 -1.70703 3.94141 -1.70703c1.42676 0 2.72559 0.555664 3.69141 1.46094z" /> +d="M448 352v-320h32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-160c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32v128h-192v-128h32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-160 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32v320h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32v-128h192v128h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32z" /> +d="M448 400v-32c0 -8.83105 -7.16895 -16 -16 -16h-48v-368c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v368h-32v-368c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v112h-32c-88.3066 0 -160 71.6934 -160 160 +s71.6934 160 160 160h240c8.83105 0 16 -7.16895 16 -16z" /> +d="M352 128c53.0186 0 96 -42.9814 96 -96s-42.9814 -96 -96 -96s-96 42.9814 -96 96c0 0.00976562 0.00292969 -0.0429688 0.00292969 -0.0332031c0 7.16699 0.785156 14.1523 2.27344 20.874l-102.486 64.0537c-16.4033 -13.0752 -37.1816 -20.8945 -59.79 -20.8945 +c-53.0186 0 -96 42.9814 -96 96s42.9814 96 96 96c22.6084 0 43.3867 -7.81934 59.79 -20.8945l102.486 64.0537c-1.48633 6.71094 -2.27637 13.6826 -2.27637 20.8408c0 53.0186 42.9814 96 96 96s96 -42.9814 96 -96s-42.9814 -96 -96 -96 +c-22.6084 0 -43.3867 7.81934 -59.79 20.8965l-102.486 -64.0547c1.48828 -6.73145 2.27344 -13.6025 2.27344 -20.7793s-0.785156 -14.1719 -2.27344 -20.9033l102.486 -64.0537c16.4033 13.0752 37.1816 20.8945 59.79 20.8945z" /> +c-1.13281 -4.44141 -1.73535 -9.09375 -1.73535 -13.8857c0 -0.0117188 -0.00488281 0 -0.00488281 -0.0117188c0 -30.9277 25.0723 -56 56 -56s56 25.0723 56 56c-0.000976562 30.9287 -25.0732 56.001 -56.001 56.001z" /> +d="M320 416v-96h-64v96c0 17.6611 14.3389 32 32 32s32 -14.3389 32 -32zM368 288c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-16v-32c-0.0117188 -77.3096 -55.0684 -141.886 -128 -156.8v-99.2002h-64v99.2002 +c-72.9316 14.9141 -127.988 79.4902 -128 156.8v32h-16c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h352zM128 416v-96h-64v96c0 17.6611 14.3389 32 32 32s32 -14.3389 32 -32z" /> @@ -1358,16 +1658,17 @@ c5.41992 6.97949 15.4805 8.22949 22.46 2.80957l144.96 -112.04c22.9307 31.5 57.26 c0 -102.3 36.1504 -133.529 55.4697 -154.29c6 -6.43945 8.66016 -14.1602 8.61035 -21.71c0 -1.39941 -0.610352 -2.67969 -0.799805 -4.05957zM157.23 196.46l212.789 -164.46h-241.92c-19.1191 0 -31.9893 15.5996 -32.0996 32 c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c16.21 17.4199 44.0098 42.79 52.6201 110.75zM320 -64c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" /> +d="M432 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h120l9.40039 18.7002c3.85547 7.88574 11.9434 13.2998 21.3066 13.2998h0.0927734h114.3 +c0.00585938 0 -0.00195312 0.0234375 0.00390625 0.0234375c9.41113 0 17.5645 -5.42871 21.4961 -13.3232l9.40039 -18.7002h120zM53.2002 -19l-21.2002 339h384l-21.2002 -339c-1.57031 -25.0762 -22.4316 -44.9971 -47.8994 -45h-245.801 +c-25.4678 0.00292969 -46.3291 19.9238 -47.8994 45z" /> +c5.45996 -5.05566 14.1846 -3.97168 18.2334 2.29492l22.3799 34.6553c1.20996 1.87305 1.91895 4.12109 1.91895 6.51465c0 3.125 -1.19727 5.97168 -3.15625 8.1084c-1.45703 1.58887 -36.4658 38.9043 -103.423 38.9043c-81.7578 0 -143.762 -62.0986 -143.762 -143.401 +c0 -82.3066 59.792 -145.567 144.484 -145.567c70.0752 0 108.259 43.8643 109.851 45.7314z" /> @@ -1402,13 +1703,13 @@ c-70.751 0 -128 -57.2588 -128 -128zM384 64c70.751 0 128 57.2598 128 128c0 70.751 d="M384 384c106 0 192 -86 192 -192s-86 -192 -192 -192h-192c-106 0 -192 86 -192 192s86 192 192 192h192zM384 64c70.7002 0 128 57.2002 128 128c0 70.7002 -57.2002 128 -128 128c-70.7002 0 -128 -57.2002 -128 -128c0 -70.7002 57.2002 -128 128 -128z" /> +s35.8877 -80 80 -80zM290.632 144l74.2861 120h-127.547l-24.7461 -39.9736c22.8271 -20.1328 38.4229 -48.2705 42.3828 -80.0264h35.624zM507.689 48.1143c46.0605 -2.43164 84.3115 34.3447 84.3125 79.8848c0 44.1123 -35.8877 80 -80 80 +c-0.0136719 0 0.00585938 -0.0078125 -0.00683594 -0.0078125c-6.85156 0 -13.5029 -0.864258 -19.8516 -2.48926l44.4688 -71.6426c4.66113 -7.50879 2.35156 -17.3721 -5.15625 -22.0322l-13.5938 -8.4375c-7.50879 -4.65918 -17.3721 -2.35156 -22.0322 5.15625 +l-44.4326 71.5859c-12.7021 -14.7451 -20.1475 -34.1416 -19.3359 -55.2627c1.57812 -41.0635 34.5918 -74.5898 75.6279 -76.7549z" /> +c22.7783 -7.32129 29.7354 -36.1914 12.8359 -53.0918zM192 320v-87.5312l118.208 37.9951c3.08594 0.992188 6.38086 1.52832 9.79492 1.52832c3.41309 0 6.70312 -0.536133 9.78906 -1.52832l118.208 -37.9951v87.5312h-256z" /> +c9.69238 24.6738 37.5537 36.8174 62.2275 27.124l190.342 -74.7646l24.8721 31.0898c12.3066 15.3809 33.9785 19.5146 51.0811 9.74121l112 -64c12.0605 -6.89355 20.1533 -19.8564 20.1533 -34.7305v-240c0 -18.5615 -12.7695 -34.6855 -30.8379 -38.9365l-136 -32 +c-2.94824 -0.694336 -6.00391 -1.06348 -9.16211 -1.06348h-80c-22.0908 0 -40 17.9082 -40 40z" /> +d="M384 -32v61.4609c0 8.5332 -4.4375 16.0166 -11.1543 20.2734l-111.748 70.8105c-7.41895 4.70215 -16.2656 7.45508 -25.6914 7.45508h-147.406c-13.2549 0 -24 10.7451 -24 24v8c0 35.3457 28.6543 64 64 64h123.648c13.3086 0 24.7158 8.12109 29.5371 19.6924 +l21.4102 51.3848c4.94141 11.8555 -3.77051 24.9229 -16.6143 24.9229h-229.981c-30.9277 0 -56 25.0723 -56 56v16c0 13.2549 10.7451 24 24 24h333.544c17.0908 0 32.0781 -8.90137 40.583 -22.3682l163.04 -258.146c9.35645 -14.8145 14.833 -32.4619 14.833 -51.2637 +v-116.222h-192z" /> +d="M510.9 302.729l-68.2969 -286.823c-10.8975 -45.7705 -52.0801 -79.9062 -101.166 -79.9062h-127.363c-36.0293 0 -68.8447 14.0459 -93.1855 36.9531l-108.298 101.92c-7.72754 7.29297 -12.5537 17.6299 -12.5537 29.084c0 22.0723 17.9199 39.9922 39.9922 39.9922 +c10.5742 0 20.2188 -4.11426 27.374 -10.8262l60.5928 -57.0254v0c0 27.958 -4.1084 54.9473 -11.6699 80.4668l-42.6885 144.075c-1.06738 3.60254 -1.63965 7.41699 -1.63965 11.3633c0 22.0801 17.9258 40.0059 40.0049 40.0059 +c18.1338 0 33.4512 -12.0977 38.3525 -28.6504l37.1543 -125.395c1.02148 -3.44629 4.21387 -5.96387 7.99023 -5.96387c4.59766 0 8.33105 3.7334 8.33105 8.33105c0 0.717773 -0.09375 1.41016 -0.264648 2.07422l-50.3047 195.641 +c-0.821289 3.19238 -1.25879 6.53711 -1.25879 9.98438c0 22.0742 17.9219 39.9961 39.9971 39.9961c18.6279 0 34.291 -12.793 38.7305 -30.043l56.0947 -218.158c1.15527 -4.49512 5.23926 -7.82129 10.0928 -7.82129c5.03125 0 9.23438 3.57715 10.207 8.32227 +l37.6826 183.704c3.76074 18.2139 19.9043 31.9248 39.2256 31.9248c4.20703 0 8.26562 -0.629883 12.0771 -1.83496c19.8604 -6.2998 30.8623 -27.6738 26.6758 -48.085l-33.8389 -164.967c-0.101562 -0.492188 -0.154297 -1.00098 -0.154297 -1.52344 +c0 -4.16797 3.38379 -7.55176 7.55176 -7.55176c3.56445 0 6.55566 2.48535 7.34668 5.80859l29.3975 123.459c4.19141 17.6016 20.0312 30.708 38.9082 30.708c22.0732 0 39.9941 -17.9209 39.9941 -39.9941c0 -3.19727 -0.380859 -6.26465 -1.09082 -9.24512v0z" /> +c13.2549 0 24 10.7451 24 24v71.6631h25.5566l44.1289 -82.9375c4.03516 -7.58301 12.0049 -12.7266 21.1875 -12.7266h24.4639c18.2617 0.000976562 29.8291 19.5908 21.0186 35.5869z" /> +d="M592 448c26.4922 0 48 -21.5078 48 -48v-320c0 -26.4922 -21.5078 -48 -48 -48h-240v-32h176c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h176v32h-240 +c-26.4922 0 -48 21.5078 -48 48v320c0 26.4922 21.5078 48 48 48h544zM576 96v288h-512v-288h512z" /> +d="M0 330.34c0.00292969 13.4697 8.32617 24.9932 20.1201 29.71l139.88 55.9502v-384l-138.06 -62.8398c-10.5107 -4.2002 -21.9404 3.54004 -21.9404 14.8594v346.32zM192 32v384l192 -64v-384zM554.06 414.84c10.5107 4.2002 21.9404 -3.54004 21.9404 -14.8594v-346.32 +c0 -13.4707 -8.32422 -24.9951 -20.1201 -29.71l-139.88 -55.9502v384z" /> +d="M440.667 265.891c-1.00195 -5.61328 -5.91309 -9.89062 -11.8135 -9.89062h-79.0957l-22.8564 -128h74.8096c7.4707 0 13.126 -6.75391 11.8135 -14.1094l-7.14355 -40c-1.00195 -5.61328 -5.91309 -9.89062 -11.8125 -9.89062h-79.0967l-15.377 -86.1094 +c-1.00195 -5.61328 -5.91309 -9.89062 -11.8125 -9.89062h-40.6318c-7.47266 0 -13.127 6.75391 -11.8135 14.1094l14.623 81.8906h-98.6338l-15.3779 -86.1094c-1.00195 -5.61328 -5.91309 -9.89062 -11.8135 -9.89062h-40.6318 +c-7.4707 0 -13.126 6.75391 -11.8125 14.1094l14.623 81.8906h-74.8105c-7.4707 0 -13.126 6.75391 -11.8125 14.1094l7.14258 40c1.00195 5.61328 5.91309 9.89062 11.8135 9.89062h79.0957l22.8564 128h-74.8096c-7.4707 0 -13.126 6.75391 -11.8135 14.1094l7.14355 40 +c1.00195 5.61328 5.91309 9.89062 11.8125 9.89062h79.0967l15.377 86.1094c1.00195 5.61328 5.91309 9.89062 11.8125 9.89062h40.6318c7.47266 0 13.127 -6.75391 11.8135 -14.1094l-14.623 -81.8906h98.6348l15.377 86.1094 +c1.00195 5.61328 5.91309 9.89062 11.8135 9.89062h40.6318c7.4707 0 13.126 -6.75391 11.8125 -14.1094l-14.623 -81.8906h74.8105c7.4707 0 13.126 -6.75391 11.8125 -14.1094zM261.889 128l22.8574 128h-98.6338l-22.8574 -128h98.6338z" /> +c-4.33203 -6.17773 -11.4912 -10.1973 -19.6006 -10.2002l-33.3994 -0.100586c-19.5 0 -30.9004 21.9004 -19.7002 37.8008l368 463.699c4.5 6.40039 11.7998 10.2002 19.5996 10.2002z" /> @@ -1756,14 +2057,14 @@ d="M290.547 258.961c-20.2949 10.1494 -44.1465 11.1992 -64.7393 3.88965c42.6064 0 c-14.7246 -30.8457 -46.123 -50.8535 -80.2979 -50.8535c-0.556641 0 -94.4707 8.61426 -94.4707 8.61426l-66.4062 -33.3467c-9.38379 -4.69336 -19.8145 -0.378906 -23.8945 7.78125l-44.4561 88.9248c-4.16699 8.61523 -1.11133 18.8975 6.94531 23.6211l58.0723 33.0693 l41.1221 74.1953c6.38965 57.2451 34.7314 109.768 79.7432 146.727c11.3906 9.44824 28.3408 7.78125 37.5098 -3.61328c9.44629 -11.3936 7.78027 -28.0674 -3.6123 -37.5156c-12.5029 -10.5596 -23.6172 -22.5098 -32.5088 -35.5703 c21.6719 14.7285 46.6787 24.7324 74.1865 28.0674c14.7246 1.94434 28.0625 -8.33594 29.7295 -23.0654c1.94531 -14.7275 -8.33594 -28.0674 -23.0615 -29.7344c-16.1162 -1.94434 -31.1201 -7.50293 -44.1787 -15.2832c26.1143 5.71289 58.7119 3.1377 88.0791 -11.1152 -c13.3359 -6.66895 18.8936 -22.5088 12.2246 -35.8486c-6.38965 -13.0596 -22.5039 -18.6162 -35.5645 -12.2256zM263.318 189.489c-6.1123 12.5049 -18.3379 20.2861 -32.2314 20.2861h-0.105469c-19.5732 0 -35.46 -15.8867 -35.46 -35.46 -c0 -0.0302734 0 -0.0800781 0.000976562 -0.110352c0 -21.4277 17.8076 -35.5703 35.5645 -35.5703c13.8936 0 26.1191 7.78125 32.2314 20.2861c4.44531 9.44922 13.6133 15.0059 23.3389 15.2842c-9.72559 0.277344 -18.8936 5.83496 -23.3389 15.2842zM638.139 226.726 -c4.16797 -8.61426 1.11133 -18.8965 -6.94531 -23.6201l-58.0713 -33.0693l-41.1221 -74.1963c-6.38965 -57.2451 -34.7314 -109.767 -79.7432 -146.726c-10.9316 -9.1123 -27.7988 -8.14453 -37.5098 3.6123c-9.44629 11.3945 -7.78027 28.0674 3.61328 37.5166 -c12.5029 10.5586 23.6162 22.5088 32.5078 35.5703c-21.6719 -14.7295 -46.6787 -24.7324 -74.1865 -28.0674c-10.0205 -2.50586 -27.5518 5.64258 -29.7295 23.0645c-1.94531 14.7285 8.33594 28.0674 23.0615 29.7344c16.1162 1.94629 31.1201 7.50293 44.1787 15.2842 -c-26.1143 -5.71289 -58.7119 -3.1377 -88.0791 11.1152c-13.3359 6.66895 -18.8936 22.5088 -12.2246 35.8477c6.38965 13.0605 22.5049 18.6191 35.5654 12.2266c20.2949 -10.1484 44.1465 -11.1982 64.7393 -3.88965c-42.6064 0 -71.208 20.4746 -85.5781 50.5762 -c-8.57617 17.8984 5.14746 38.0713 23.6172 38.0713c-18.4297 0 -32.2109 20.1357 -23.6172 38.0703c14.0332 29.3965 44.0391 50.8877 81.9658 50.8545l92.8027 -8.61523l66.4062 33.3467c9.4082 4.7041 19.8281 0.354492 23.8936 -7.78027zM408.912 245.344 -c-13.8936 0 -26.1191 -7.78027 -32.2314 -20.2861c-4.44531 -9.44824 -13.6133 -15.0059 -23.3389 -15.2832c9.72559 -0.27832 18.8936 -5.83594 23.3389 -15.2842c6.1123 -12.5049 18.3379 -20.2861 32.2314 -20.2861h0.105469c19.5732 0 35.46 15.8857 35.46 35.46 -c0 0.0302734 0 0.0791016 -0.000976562 0.110352c0 21.4287 -17.8076 35.5693 -35.5645 35.5693z" /> +c13.3359 -6.66895 18.8936 -22.5088 12.2246 -35.8486c-6.38965 -13.0596 -22.5039 -18.6162 -35.5645 -12.2256zM263.318 189.489c-6.1123 12.5049 -18.3379 20.2861 -32.2314 20.2861h-0.107422c-19.5703 0 -35.46 -15.8896 -35.46 -35.46 +c0 -0.0380859 0.00195312 -0.0732422 0.00292969 -0.110352c0 -21.4277 17.8076 -35.5703 35.5645 -35.5703c13.8936 0 26.1191 7.78125 32.2314 20.2861c4.44531 9.44922 13.6133 15.0059 23.3389 15.2842c-9.72559 0.277344 -18.8936 5.83496 -23.3389 15.2842z +M638.139 226.726c4.16797 -8.61426 1.11133 -18.8965 -6.94531 -23.6201l-58.0713 -33.0693l-41.1221 -74.1963c-6.38965 -57.2451 -34.7314 -109.767 -79.7432 -146.726c-10.9316 -9.1123 -27.7988 -8.14453 -37.5098 3.6123 +c-9.44629 11.3945 -7.78027 28.0674 3.61328 37.5166c12.5029 10.5586 23.6162 22.5088 32.5078 35.5703c-21.6719 -14.7295 -46.6787 -24.7324 -74.1865 -28.0674c-10.0205 -2.50586 -27.5518 5.64258 -29.7295 23.0645c-1.94531 14.7285 8.33594 28.0674 23.0615 29.7344 +c16.1162 1.94629 31.1201 7.50293 44.1787 15.2842c-26.1143 -5.71289 -58.7119 -3.1377 -88.0791 11.1152c-13.3359 6.66895 -18.8936 22.5088 -12.2246 35.8477c6.38965 13.0605 22.5049 18.6191 35.5654 12.2266c20.2949 -10.1484 44.1465 -11.1982 64.7393 -3.88965 +c-42.6064 0 -71.208 20.4746 -85.5781 50.5762c-8.57617 17.8984 5.14746 38.0713 23.6172 38.0713c-18.4297 0 -32.2109 20.1357 -23.6172 38.0703c14.0332 29.3965 44.0391 50.8877 81.9658 50.8545l92.8027 -8.61523l66.4062 33.3467 +c9.4082 4.7041 19.8281 0.354492 23.8936 -7.78027zM408.912 245.344c-13.8936 0 -26.1191 -7.78027 -32.2314 -20.2861c-4.44531 -9.44824 -13.6133 -15.0059 -23.3389 -15.2832c9.72559 -0.27832 18.8936 -5.83594 23.3389 -15.2842 +c6.1123 -12.5049 18.3379 -20.2861 32.2314 -20.2861h0.107422c19.5703 0 35.46 15.8887 35.46 35.46c0 0.0371094 -0.00195312 0.0722656 -0.00292969 0.110352c0 21.4287 -17.8076 35.5693 -35.5645 35.5693z" /> +d="M569.344 216.369c4.20996 -7.13086 6.62598 -15.5469 6.62598 -24.4199c0 -8.87402 -2.41699 -17.1875 -6.62695 -24.3193c-31.9746 -54.2607 -79.6484 -98.3232 -136.81 -126.301l0.00683594 -0.00878906l43.1201 -58.377 +c7.60156 -10.8594 4.95996 -25.8252 -5.90039 -33.4268l-13.1133 -9.17773c-10.8594 -7.59863 -25.8223 -4.95801 -33.4238 5.90039l-251.836 356.544c-13.5234 -6.16211 -26.5166 -13.3994 -38.7764 -21.5635l189.979 -271.399 +c-11.4863 -1.21191 -22.4707 -1.83301 -34.2754 -1.83301c-15.1465 0 -30.0566 1.02344 -44.6641 3.00293l-40.6309 58.04h-0.00976562l-119.399 170.58c-10.457 -11.1943 -19.8271 -23.0791 -28.2939 -35.9121l124.19 -177.417 +c-73.1172 25.4863 -134.358 76.0166 -172.858 141.349c-8.96484 15.2109 -8.76562 33.8643 0 48.7393c0.0107422 0.0166016 0.0234375 0.0332031 0.0332031 0.0498047c33.5459 56.8984 82.7676 99.8506 136.79 126.242l-43.1309 58.3945 +c-7.60156 10.8604 -4.95996 25.8252 5.90039 33.4268l13.1143 9.17773c10.8584 7.59961 25.8213 4.95801 33.4229 -5.90039l52.7705 -72.1689c26.3496 6.79004 53.9834 10.4092 82.4512 10.4092c119.81 0 224.96 -63.9492 281.344 -159.631zM390.026 102.06 +c21.1406 23.9658 33.9736 55.4365 33.9736 89.9404c0 75.1738 -60.8379 136 -136 136c-17.5117 0 -34.2422 -3.30566 -49.6084 -9.32324l19.0684 -27.2363c25.9883 7.96289 54.7598 5.56836 79.5098 -7.68066h-0.0292969c-23.6504 0 -42.8203 -19.1699 -42.8203 -42.8193 +c0 -23.4717 18.9922 -42.8203 42.8203 -42.8203c23.6494 0 42.8193 19.1699 42.8193 42.8203v0.0292969c18.9111 -35.3271 15.8818 -79.1123 -8.7998 -111.68z" /> +c-3.63867 2.68848 -8.77637 1.82129 -11.3389 -1.90625l-9.07227 -13.1963c-0.884766 -1.28711 -1.40332 -2.8457 -1.40332 -4.52539c0 -2.63867 1.26953 -4.98438 3.24219 -6.44141c22.8877 -16.8994 55.4541 -40.6904 105.304 -76.8682 +c20.2734 -14.7812 56.5234 -47.8135 92.2637 -47.5732c35.7236 -0.242188 71.9609 32.7715 92.2627 47.5732c49.8506 36.1787 82.418 59.9697 105.304 76.8682c1.97266 1.45703 3.25391 3.79883 3.25391 6.4375c0 1.67969 -0.530273 3.24219 -1.41504 4.5293z" /> +d="M304 128c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM336 224c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM368 160c-8.83105 0 -16 7.16895 -16 16s7.16895 16 16 16 +s16 -7.16895 16 -16s-7.16895 -16 -16 -16zM336 128c-8.83105 0 -16 7.16895 -16 16s7.16895 16 16 16s16 -7.16895 16 -16s-7.16895 -16 -16 -16zM304 192c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM432 224 +c-8.83105 0 -16 7.16895 -16 16s7.16895 16 16 16s16 -7.16895 16 -16s-7.16895 -16 -16 -16zM384 208c0 8.83105 7.16895 16 16 16s16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16zM368 256c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16 +s-16 7.16895 -16 16s7.16895 16 16 16zM464 224c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM496 256c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM432 192 +c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM400 160c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM336 96c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16 +s-16 7.16895 -16 16s7.16895 16 16 16zM304 64c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM368 128c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM389.65 346.35 +c2.89648 -2.89551 4.68945 -6.90039 4.68945 -11.3164s-1.79297 -8.41699 -4.68945 -11.3135l-169.381 -169.37c-2.89551 -2.89648 -6.90039 -4.68945 -11.3164 -4.68945s-8.41699 1.79297 -11.3135 4.68945l-11.2998 11.3105 +c-2.89355 2.89551 -4.68457 6.89844 -4.68457 11.3125c0 4.41309 1.79102 8.41113 4.68457 11.3076l5.66016 5.66992c-19.7871 20.0811 -31.9951 47.6602 -32 78.0498c0 19.2402 5.2998 37.0801 13.9297 52.8604l-10 10c-10.5723 10.6055 -25.1416 17.167 -41.2861 17.167 +c-2.58984 0 -5.1416 -0.169922 -7.64355 -0.49707c-30 -3.73047 -51 -31.7803 -51 -61.9307v-305.6c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v303.15c0 67.9395 55.4902 129.35 123.44 128.85 +c33.4453 -0.166992 63.7471 -13.835 85.6592 -35.8496l10 -10c15.8203 8.5498 33.6602 13.8496 52.9004 13.8496c30.3916 -0.000976562 57.9707 -12.21 78.0498 -32l5.66992 5.66016c2.89648 2.89648 6.90137 4.68945 11.3174 4.68945s8.41699 -1.79297 11.3125 -4.68945z +" /> +d="M32 64v48h448v-48c-0.0576172 -28.2656 -12.3916 -53.6514 -32 -71.0898v-40.9102c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v16h-256v-16c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v40.9102 +c-19.6084 17.4385 -31.9424 42.8242 -32 71.0898zM496 192c8.83105 0 16 -7.16895 16 -16v-16c0 -8.83105 -7.16895 -16 -16 -16h-480c-8.83105 0 -16 7.16895 -16 16v16c0 8.83105 7.16895 16 16 16h16v186.75c0 38.2197 31.0391 69.2656 69.2598 69.2656 +c19.1113 0 36.4248 -7.75879 48.96 -20.2959l19.2607 -19.2695c29.8994 13.1299 59.1094 7.60938 79.7295 -8.62012l0.169922 0.169922c2.89551 2.89355 6.89941 4.68457 11.3125 4.68457s8.41211 -1.79102 11.3076 -4.68457l11.3096 -11.3096 +c2.89746 -2.89648 4.69043 -6.90137 4.69043 -11.3174s-1.79297 -8.41699 -4.69043 -11.3135l-105.369 -105.369c-2.89648 -2.89746 -6.90137 -4.69043 -11.3174 -4.69043s-8.41699 1.79297 -11.3135 4.69043l-11.3096 11.3096 +c-2.88477 2.89453 -4.66992 6.8916 -4.66992 11.2969c0 4.40625 1.78516 8.39844 4.66992 11.293l0.169922 0.169922c-16.2295 20.6201 -21.75 49.8506 -8.62012 79.7305l-19.2695 19.2598c-3.84766 3.84082 -9.16016 6.21289 -15.0205 6.21289 +c-11.7178 0 -21.2344 -9.50098 -21.2598 -21.2129v-186.75h416z" /> +d="M32 -16v336h384v-336c0 -26.4922 -21.5078 -48 -48 -48h-288c-26.4922 0 -48 21.5078 -48 48zM304 240v-224c0 -8.83105 7.16895 -16 16 -16s16 7.16895 16 16v224c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16zM208 240v-224c0 -8.83105 7.16895 -16 16 -16 +s16 7.16895 16 16v224c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16zM112 240v-224c0 -8.83105 7.16895 -16 16 -16s16 7.16895 16 16v224c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16zM432 416c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h120l9.40039 18.7002c3.85547 7.88574 11.9434 13.2998 21.3066 13.2998h0.0927734h114.3c0.00585938 0 -0.00195312 0.0234375 0.00390625 0.0234375 +c9.41113 0 17.5645 -5.42871 21.4961 -13.3232l9.40039 -18.7002h120z" /> d="M88 281.941h-46.0576c-21.3828 0 -32.0908 25.8516 -16.9717 40.9707l86.0596 86.0586c9.37207 9.37305 24.5674 9.37305 33.9404 0l86.0596 -86.0586c15.1191 -15.1201 4.41113 -40.9707 -16.9717 -40.9707h-46.0586v-301.941c0 -6.62695 -5.37305 -12 -12 -12h-56 c-6.62695 0 -12 5.37305 -12 12v301.941z" /> +d="M448 104v-112v-0.0615234c0 -13.2129 -10.7275 -23.9395 -23.9395 -23.9395c-0.0205078 0 -0.0400391 0.000976562 -0.0605469 0.000976562h-112c-21.3896 0 -32.0898 25.9004 -17 41l36.2002 36.2002l-107.2 107.2l-107.23 -107.301l36.2305 -36.0996 +c15.0898 -15.0996 4.38965 -41 -17 -41h-112h-0.0615234c-13.2129 0 -23.9395 10.7275 -23.9395 23.9395c0 0.0205078 0.000976562 0.0400391 0.000976562 0.0605469v112c0 21.4004 25.8896 32.0996 41 17l36.1904 -36.2002l107.27 107.2l-107.28 107.3l-36.1797 -36.2998 +c-15.0996 -15.0996 -41 -4.40039 -41 17v112v0.0615234c0 13.2129 10.7275 23.9395 23.9395 23.9395c0.0205078 0 0.0400391 -0.000976562 0.0605469 -0.000976562h112c21.3896 0 32.0898 -25.9004 17 -41l-36.2002 -36.2002l107.2 -107.2l107.23 107.301l-36.2305 36.0996 +c-15.0898 15.0996 -4.38965 41 17 41h112h0.0615234c13.2129 0 23.9395 -10.7275 23.9395 -23.9395c0 -0.0205078 -0.000976562 -0.0400391 -0.000976562 -0.0605469v-112c0 -21.4004 -25.8896 -32.0996 -41 -17l-36.1904 36.2002l-107.27 -107.2l107.28 -107.3 +l36.1797 36.2002c15.0996 15.1992 41 4.5 41 -16.9004z" /> @@ -1994,9 +2297,9 @@ v-70.9004h-116c-6.59961 0 -12 -5.40039 -12 -12v-64c0 -6.59961 5.40039 -12 12 -12 d="M8 192c0 137 111 248 248 248s248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248zM300 76v116h70.9004c10.6992 0 16.0996 13 8.5 20.5l-114.9 114.3c-4.7002 4.7002 -12.2002 4.7002 -16.9004 0l-115 -114.3c-7.59961 -7.59961 -2.19922 -20.5 8.5 -20.5 h70.9004v-116c0 -6.59961 5.40039 -12 12 -12h64c6.59961 0 12 5.40039 12 12z" /> +d="M432 128c8.83105 0 16 -7.16895 16 -16v-128c0 -26.4922 -21.5078 -48 -48 -48h-352c-26.4922 0 -48 21.5078 -48 48v352c0 26.4922 21.5078 48 48 48h160c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-144v-320h320v112 +c0 8.83105 7.16895 16 16 16h32zM488 448c13.2461 0 24 -10.7539 24 -24v-128c0 -21.5 -26 -32 -41 -17l-35.7197 35.6797l-243.61 -243.68c-4.34668 -4.36133 -10.3652 -7.0625 -17.0029 -7.0625s-12.6504 2.70117 -16.9971 7.0625l-22.6699 22.6299 +c-4.36133 4.34668 -7.0625 10.3652 -7.0625 17.0029c0 6.63867 2.70117 12.6504 7.0625 16.9971l243.73 243.64l-35.7305 35.7305c-15.0498 15.0898 -4.37012 41 17 41h128z" /> @@ -2015,11 +2318,11 @@ c-6.2002 6.2002 -16.3994 6.2002 -22.5996 0l-105.4 -105.4c-10.0996 -10.0996 -3 -2 d="M485.5 448l90.5 -160h-101.1l-69.2002 160h79.7998zM357.5 448l69.2002 -160h-277.4l69.2002 160h139zM90.5 448h79.7998l-69.2002 -160h-101.1zM0 256h100.7l123 -251.7c1.5 -3.09961 -2.7002 -5.89941 -5 -3.2998zM148.2 256h279.6l-137 -318.2 c-1 -2.39941 -4.5 -2.39941 -5.5 0zM352.3 4.2998l123 251.7h100.7l-218.7 -254.9c-2.2998 -2.69922 -6.5 0.100586 -5 3.2002z" /> +d="M313.553 328.331c14.2646 -15.3623 3.29102 -40.3311 -17.5869 -40.3311h-63.9658v-328c0 -13.2549 -10.7451 -24 -24 -24h-195.976c-10.6914 0 -16.0459 12.9258 -8.48535 20.4854l56 56c2.17188 2.17188 5.17383 3.51465 8.48535 3.51465h83.9756v272h-63.9746 +c-20.9639 0 -31.793 25.0312 -17.5869 40.3311l103.975 112.003c9.49805 10.2295 25.6885 10.2139 35.1738 0z" /> @@ -2035,8 +2338,8 @@ c0 -4.41992 4.78027 -8 10.6699 -8h85.3301v-32h-85.3301c-5.88965 0 -10.6699 -3.58 d="M272 448c26.5 0 48 -21.5 48 -48v-416c0 -26.5 -21.5 -48 -48 -48h-224c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h224zM160 -32c17.7002 0 32 14.2998 32 32s-14.2998 32 -32 32s-32 -14.2998 -32 -32s14.2998 -32 32 -32zM272 76v312 c0 6.59961 -5.40039 12 -12 12h-200c-6.59961 0 -12 -5.40039 -12 -12v-312c0 -6.59961 5.40039 -12 12 -12h200c6.59961 0 12 5.40039 12 12z" /> +c5.67578 2.35449 11.96 3.6543 18.4824 3.6543c6.52148 0 12.7432 -1.2998 18.418 -3.6543zM256.1 1.7002c93.7002 46.5996 172.5 156.3 175.801 307.7l-175.9 73.2998z" /> @@ -2096,47 +2399,48 @@ l46.2998 -46.2998l-157.9 -157.9c-35 42.4004 -53.5 93.6006 -56.0996 145.5c63.9004 d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM120 256c17.7002 0 32 14.2998 32 32s-14.2998 32 -32 32s-32 -14.2998 -32 -32s14.2998 -32 32 -32zM184 352c0 -17.7002 14.2998 -32 32 -32s32 14.2998 32 32 s-14.2998 32 -32 32s-32 -14.2998 -32 -32zM232 208c17.7002 0 32 14.2998 32 32s-14.2998 32 -32 32s-32 -14.2998 -32 -32s14.2998 -32 32 -32z" /> +d="M74 240l-33.9102 90.3799c-0.655273 1.74707 -1.01953 3.64551 -1.01953 5.62012c0 8.83105 7.16895 16 16 16h0.0195312h56.9102v32h-24c-4.41504 0 -8 3.58496 -8 8v16c0 4.41504 3.58496 8 8 8h24v24c0 4.41504 3.58496 8 8 8h16c4.41504 0 8 -3.58496 8 -8v-24h24 +c4.41504 0 8 -3.58496 8 -8v-16c0 -4.41504 -3.58496 -8 -8 -8h-24v-32h56.8896c0.00683594 0 0.0078125 -0.00683594 0.0146484 -0.00683594c8.83008 0 16 -7.16992 16 -16c0 -1.97461 -0.359375 -3.86621 -1.01465 -5.61328l-33.8896 -90.3799h10 +c8.83105 0 16 -7.16895 16 -16v-16c0 -8.83105 -7.16895 -16 -16 -16h-15.9404c0.142578 -44.1934 5.69141 -86.9287 15.9404 -128h-128c10.249 41.0713 15.7979 83.8066 15.9404 128h-15.9404c-8.83105 0 -16 7.16895 -16 16v16c0 8.83105 7.16895 16 16 16h10z +M247.16 -11.5801c5.24805 -2.62598 8.83984 -8.0459 8.83984 -14.3096v-22.1104c0 -8.83105 -7.16895 -16 -16 -16h-224c-8.83105 0 -16 7.16895 -16 16v22.1104c0.000976562 6.26562 3.59668 11.6855 8.84961 14.3096l23.1504 11.5801v16c0 8.83105 7.16895 16 16 16h160 +c8.83105 0 16 -7.16895 16 -16v-16zM339.93 146.2l-24.5693 20.7998c-6.94434 5.86133 -11.3438 14.6143 -11.3604 24.4004v58.5996c0 3.31152 2.68848 6 6 6h26.3896c3.31152 0 6 -2.68848 6 -6v-26h24.71v26c0 3.31152 2.68848 6 6 6h53.8105c3.31152 0 6 -2.68848 6 -6 +v-26h24.71v26c0 3.31152 2.68848 6 6 6h26.3799c3.31152 0 6 -2.68848 6 -6v-58.54c0 -0.0107422 0.0185547 -0.0126953 0.0185547 -0.0234375c0 -9.79297 -4.40918 -18.5645 -11.3486 -24.4365l-24.5996 -20.79l3.29004 -82.21h-126.721zM384 144v-32h32v32 +c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16zM503.16 -11.5801c5.24805 -2.62598 8.83984 -8.0459 8.83984 -14.3096v-22.1104c0 -8.83105 -7.16895 -16 -16 -16h-192c-8.83105 0 -16 7.16895 -16 16v22.1104c0.000976562 6.26562 3.59668 11.6855 8.84961 14.3096 +l23.1504 11.5801v16c0 8.83105 7.16895 16 16 16h128c8.83105 0 16 -7.16895 16 -16v-16z" /> +d="M8 160.12c0 73.3799 59.8096 181.08 112.6 225.37c-14 3.41992 -24.5996 15.5098 -24.5996 30.5098c0 17.6611 14.3389 32 32 32h64c17.6611 0 32 -14.3389 32 -32c0 -15.0498 -10.5996 -27.0898 -24.5996 -30.5098c24.3994 -20.4902 50.0693 -54.6807 70.8691 -92.5898 +l-107.89 -107.931c-1.44727 -1.44727 -2.3418 -3.44922 -2.3418 -5.65625c0 -2.20605 0.894531 -4.20508 2.3418 -5.65332l11.3105 -11.3105c1.44727 -1.44629 3.44922 -2.3418 5.65527 -2.3418c2.20703 0 4.20605 0.895508 5.6543 2.3418l100.31 100.33 +c15.96 -35.46 26.6904 -71.9492 26.6904 -102.56c0 -51.6006 -22.1396 -73.8301 -56 -84.6006v-43.5195h-192v43.5195c-33.8604 10.7705 -56 32.9609 -56 84.6006zM304 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-288 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h288z" /> +d="M400 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-352c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h352zM416 288c17.6494 -0.0136719 31.9688 -14.3477 31.9688 -32.001c0 -3.32129 -0.507812 -6.52539 -1.44922 -9.53906 +l-73.0791 -214.46h-298.881l-73.0791 214.46c-0.941406 3.01367 -1.45508 6.21875 -1.45508 9.54004c0 17.6533 14.3252 31.9863 31.9746 32h160v48h-40c-4.41504 0 -8 3.58496 -8 8v48c0 4.41504 3.58496 8 8 8h40v40c0 4.41504 3.58496 8 8 8h48 +c4.41504 0 8 -3.58496 8 -8v-40h40c4.41504 0 8 -3.58496 8 -8v-48c0 -4.41504 -3.58496 -8 -8 -8h-40v-48h160z" /> +d="M19 175.53c-11.2041 4.98145 -19 16.1963 -19 29.2393v0.0205078v137.21c0 0.0195312 -0.00292969 0.0419922 -0.00292969 0.0625c0 6.60742 2.67578 12.5957 7.00293 16.9375l9 9l-14.21 28.4199c-1.13867 2.27344 -1.79004 4.85547 -1.79004 7.56934v0.0107422 +c0 6.62305 5.37695 12 12 12h147.94c106 0 191.92 -86 191.92 -192v-192h-319.86v14.5195c0 0.0078125 -0.078125 -0.03125 -0.078125 -0.0244141c0 31.3145 18.0312 58.4512 44.2686 71.585l57.2197 28.6504c15.751 7.87695 26.5303 24.1348 26.5303 42.9297v0.00976562 +v50.3301l-22.1201 -11.0801c-6.19238 -3.09668 -10.8369 -8.78906 -12.5508 -15.6504l-9.21973 -30.6494c-2.81152 -9.35645 -9.77051 -16.9043 -18.7598 -20.5l-12.7803 -5.12012c-3.66895 -1.46777 -7.7168 -2.27246 -11.9082 -2.27246 +c-4.61621 0 -9.00586 0.979492 -12.9717 2.74219zM52 320c-11.0381 0 -20 -8.96191 -20 -20s8.96191 -20 20 -20s20 8.96191 20 20s-8.96191 20 -20 20zM368 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-352c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h352z" /> +d="M105.1 224c-29.3896 18.3799 -49.0996 50.7803 -49.0996 88c0 57.3994 46.6006 104 104 104s104 -46.6006 104 -104c0 -37.2197 -19.71 -69.6201 -49.0996 -88h25.0996c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-16v-5.49023 +c0 -44 4.11035 -86.5996 24 -122.51h-176c19.8604 35.9102 24 78.5098 24 122.51v5.49023h-16c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h25.0996zM304 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-288 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h288z" /> +d="M256 336c-30.9072 0 -56 25.0928 -56 56s25.0928 56 56 56s56 -25.0928 56 -56s-25.0928 -56 -56 -56zM432 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-352c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h352zM504.87 263.84 +c4.30566 -2.86816 7.11914 -7.77344 7.11914 -13.3311c0 -2.56445 -0.604492 -4.98926 -1.67969 -7.13867l-102.55 -211.37h-303.52l-102.55 211.33c-1.0752 2.14941 -1.70508 4.58008 -1.70508 7.14453c0 5.55762 2.83887 10.457 7.14453 13.3252l28.5703 16 +c7.35938 4.91016 16.8096 2.5498 22.0898 -4.54004c8.6543 -11.709 22.4922 -19.2686 38.1572 -19.2686c1.13672 0 2.26562 0.0400391 3.38281 0.119141c25.6699 1.73926 44.6699 24.7998 44.6699 50.4893c0 7.39648 6.00391 13.4004 13.4004 13.4004h38.7695 +c6.04004 0 11.6104 -3.99023 12.8604 -9.91016c4.57715 -21.7363 23.8789 -38.0752 46.9688 -38.0752s42.3936 16.3389 46.9707 38.0752c1.25 5.91016 6.86035 9.91016 12.8604 9.91016h38.7695c7.39648 0 13.4004 -6.00391 13.4004 -13.4004 +c0 -23.5293 15.7002 -45.46 38.8398 -49.75c2.95898 -0.576172 5.9541 -0.918945 9.08105 -0.918945c15.6064 0 29.4688 7.5293 38.1494 19.1494c5.37988 7.13965 14.8496 9.67969 22.29 4.67969z" /> +d="M368 416c8.83105 0 16 -7.16895 16 -16v-176l-64 -32c0 -47.7197 1.54004 -95 13.21 -160h-282.42c11.6699 65 13.21 111.67 13.21 160l-64 32v176c0 8.83105 7.16895 16 16 16h56.0996c8.83105 0 16 -7.16895 16 -16v-48h47.9004v48c0 8.83105 7.16895 16 16 16h80 +c8.83105 0 16 -7.16895 16 -16v-48h48v48c0 8.83105 7.16895 16 16 16h56zM224 128v64c0 17.6611 -14.3389 32 -32 32s-32 -14.3389 -32 -32v-64h64zM368 0c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-352c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h352z" /> +l272 113.3c5.6748 2.35449 11.959 3.6543 18.4814 3.6543s12.7432 -1.2998 18.4189 -3.6543z" /> +d="M271.06 303.7c-24.0596 6.39941 -43.4297 24.7002 -46.5693 47.7002c-4.33984 32 20.6201 59.3994 53.5098 63v17.5996c0 8.7998 7.82031 16 17.3701 16h17.3701c9.5498 0 17.3701 -7.2002 17.3701 -16v-17.7197c12.457 -1.28516 24.2842 -5.35938 34.5195 -11.5 +c3.32227 -2.00098 5.52734 -5.64746 5.52734 -9.80469c0 -2.18945 -0.617188 -4.23633 -1.6875 -5.97559c-0.592773 -0.978516 -1.31836 -1.8457 -2.16992 -2.59961l-19 -17.5c-4.01953 -3.7002 -10.0693 -4.2002 -15.2998 -2 +c-3.46289 1.4043 -7.2666 2.19336 -11.2305 2.19922h-35.5996c-5.03027 0 -9.12012 -3.7998 -9.12012 -8.39941c0.12207 -3.94727 2.91699 -7.23145 6.62988 -8.10059l54.2705 -14.2998c24.0996 -6.39941 43.4102 -24.7002 46.5596 -47.7002 +c4.33984 -32 -20.5693 -59.3994 -53.5 -63v-17.5996c0 -8.7998 -7.83008 -16 -17.3799 -16h-17.3701c-9.54004 0 -17.3701 7.2002 -17.3701 16v17.7002c-12.4541 1.28516 -24.2773 5.35938 -34.5098 11.5c-3.33008 1.99609 -5.54199 5.64551 -5.54199 9.80762 +c0 2.17188 0.607422 4.20312 1.66211 5.93262c0.617188 1.00098 1.36914 1.88867 2.25 2.65918l19 17.5c4.01953 3.7002 10.0596 4.2002 15.2998 2c3.45117 -1.39941 7.24121 -2.18848 11.1904 -2.19922h35.5996c5.03027 0 9.12012 3.7998 9.12012 8.39941 +c-0.121094 3.94727 -2.91602 7.23145 -6.62988 8.10059zM565.27 119.9c6.5918 -5.86328 10.7656 -14.3916 10.7656 -23.8984c0 -10.1123 -4.70117 -19.1357 -12.0352 -25.002l-151.23 -121c-10.9443 -8.74512 -24.8633 -14 -39.9482 -14h-0.0517578h-356.77 +c-8.83105 0 -16 7.16895 -16 16v96c0 8.83105 7.16895 16 16 16h55.4004l46.5 37.71c20.2197 16.4053 46.0596 26.2822 74.0996 26.29h160c17.6406 0 31.9668 -14.3066 32 -31.9404c0 -0.0410156 0.000976562 -0.0507812 0.000976562 -0.0917969 +c0 -1.83008 -0.158203 -3.62402 -0.460938 -5.36816c-2.54004 -15.6992 -17.3496 -26.5996 -33.25 -26.5996h-78.29c-8.83105 0 -16 -7.16895 -16 -16s7.16895 -16 16 -16h118.27c0.0605469 0 0.161133 0.0234375 0.22168 0.0234375 +c15.0459 0 28.8799 5.23242 39.7783 13.9766l92.4004 73.9004c12.4004 10 30.7998 10.6992 42.5996 0z" /> +d="M224 192c-70.6455 0 -128 57.3545 -128 128s57.3545 128 128 128s128 -57.3545 128 -128s-57.3545 -128 -128 -128zM320 128v-160c0.0791016 -11.6504 3.3418 -22.6367 8.90039 -32h-280.9c-26.4922 0 -48 21.5078 -48 48v41.5996 +c0.0166016 74.1699 60.2305 134.384 134.4 134.4h16.6992c22.1426 -10.2109 47.085 -15.9072 73.0498 -15.9072c25.9639 0 50.6084 5.69629 72.751 15.9072h16.6992c5 0 9.7002 -1 14.5 -1.5c-5.06641 -9.00684 -8.02539 -19.4561 -8.09961 -30.5zM608 160 +c17.6611 0 32 -14.3389 32 -32v-160c0 -17.6611 -14.3389 -32 -32 -32h-224c-17.6611 0 -32 14.3389 -32 32v160c0 17.6611 14.3389 32 32 32h32v80c0 44.1533 35.8467 80 80 80s80 -35.8467 80 -80v-80h32zM496 16c17.6611 0 32 14.3389 32 32s-14.3389 32 -32 32 +s-32 -14.3389 -32 -32s14.3389 -32 32 -32zM528 160v80c0 17.6611 -14.3389 32 -32 32s-32 -14.3389 -32 -32v-80h64z" /> @@ -2571,8 +2876,8 @@ c12.9697 -4.20996 26.6006 -6.91016 40.9502 -6.91016s27.9805 2.7002 40.9404 6.910 c26.4697 0 48 -22.25 48 -49.5898v-316.82c0 -27.3398 -21.5303 -49.5898 -48 -49.5898h-244.55c-6.57031 25.2695 -20.5898 47.3096 -39.6904 64h76.2402v64h128v-64h64v288h-352v-49.7998c-18.9004 11.0195 -40.5801 17.7998 -64 17.7998v46.4102 c0 27.3398 21.5303 49.5898 48 49.5898h384z" /> +d="M446.53 350.57c0 0 58.4297 -19.0605 98.9893 -41.2803c18.7607 -10.2803 30.4805 -29.8301 30.4805 -51.2305c0 -21.793 -11.9512 -40.7695 -29.71 -50.7295l-154.44 -86.6504l98.5205 -104.68h53.6299c17.6699 0 32 -14.3301 32 -32c0 -8.83984 -7.16016 -16 -16 -16 +h-90.3799l-118.53 125.94c5.07031 54.1494 -29.9297 85.0596 -40.7998 93.21c-36.8496 27.6191 -88.29 27.6592 -125.13 0l-34.7803 -26.0908c-7.07031 -5.2998 -8.49023 -15.3291 -3.18945 -22.4092c5.31934 -7.10059 15.3496 -8.5 22.4092 -3.19043l32.7607 24.5898 +c20.6895 15.5303 48.3496 20.8105 72.2393 10.8799c44.0605 -18.3193 57.8506 -70.3701 33.71 -106.6l-35.7998 -48.3301h79.4902c17.6699 0 32 -14.3301 32 -32c0 -8.83984 -7.16016 -16 -16 -16h-304c-34.9199 0 -63.8896 28.0996 -64 63.0195 +c-0.5 166.86 126.75 304.021 289.46 319.44c6.82031 37.25 39.3096 65.54 78.54 65.54c39.1904 0 71.6699 -28.2305 78.5303 -65.4297zM368 312c13.25 0 24 10.75 24 24c0 13.2598 -10.75 24 -24 24c-13.2598 0 -24 -10.7402 -24 -24c0 -13.25 10.7402 -24 24 -24z" /> @@ -2675,7 +2979,7 @@ c-5.32031 -0.449219 -10.5605 -1.17969 -16 -1.17969c-16.6006 0 -32.6406 2.2998 -4 c-0.0800781 145.76 129.3 182.88 147.31 186.94c57.1709 12.9199 111.221 0.259766 153.21 -28.7002c43.4902 -29.9902 94.9209 -46.2402 147.74 -46.2402h9.37012c60.6504 0 115.01 -45.4102 118.18 -105.98zM463.97 200c13.25 0 24 10.75 24 24 c0 13.2598 -10.75 24 -24 24s-24 -10.7402 -24 -24c0 -13.25 10.75 -24 24 -24zM543.97 46.75v99.0596c-11.1299 -11.3799 -24.7393 -20.1494 -39.8594 -25.9795z" /> +d="M358.4 444.8c10.5996 7.90039 25.5996 0.400391 25.5996 -12.7998v-480c0 -13.2002 -15.0996 -20.7002 -25.5996 -12.7998l-38.4004 44.7998l-54.4004 -44.7998c-2.66602 -2.01953 -6.01367 -3.21777 -9.6123 -3.21777c-3.59961 0 -6.9209 1.19824 -9.58691 3.21777 +l-54.4004 44.7998l-54.4004 -44.7998c-2.66602 -2.01953 -6.01367 -3.21777 -9.6123 -3.21777c-3.59961 0 -6.9209 1.19824 -9.58691 3.21777l-54.4004 44.7998l-38.4004 -44.7998c-10.5996 -7.90039 -25.5996 -0.400391 -25.5996 12.7998v480 +c0 13.2002 15 20.7002 25.5996 12.7998l38.4004 -44.7998l54.4004 44.7998c2.66602 2.01953 6.01367 3.21777 9.6123 3.21777c3.59961 0 6.9209 -1.19824 9.58691 -3.21777l54.4004 -44.7998l54.4004 44.7998c2.66602 2.01953 6.01367 3.21777 9.6123 3.21777 +c3.59961 0 6.9209 -1.19824 9.58691 -3.21777l54.4004 -44.7998zM320 88v16c0 4.40039 -3.59961 8 -8 8h-240c-4.40039 0 -8 -3.59961 -8 -8v-16c0 -4.40039 3.59961 -8 8 -8h240c4.40039 0 8 3.59961 8 8zM320 184v16c0 4.40039 -3.59961 8 -8 8h-240 +c-4.40039 0 -8 -3.59961 -8 -8v-16c0 -4.40039 3.59961 -8 8 -8h240c4.40039 0 8 3.59961 8 8zM320 280v16c0 4.40039 -3.59961 8 -8 8h-240c-4.40039 0 -8 -3.59961 -8 -8v-16c0 -4.40039 3.59961 -8 8 -8h240c4.40039 0 8 3.59961 8 8z" /> +d="M32 224h32v-192h-32h-0.0390625c-17.6406 0 -31.9619 14.3213 -31.9619 31.9619c0 0.0126953 0.000976562 0.0253906 0.000976562 0.0380859v128v0.0390625c0 17.6406 14.3213 31.9619 31.9619 31.9619c0.0126953 0 0.0253906 -0.000976562 0.0380859 -0.000976562z +M544 272v-272c-0.0351562 -35.3066 -28.6934 -63.9648 -64 -64h-320c-35.3066 0.0351562 -63.9648 28.6934 -64 64v272v0.0263672c0 44.1387 35.835 79.9746 79.9736 79.9746c0.00878906 0 0.0175781 -0.000976562 0.0263672 -0.000976562h112v64 +c0 17.6611 14.3389 32 32 32s32 -14.3389 32 -32v-64h112h0.0263672c44.1387 0 79.9746 -35.835 79.9746 -79.9736c0 -0.00878906 -0.000976562 -0.0175781 -0.000976562 -0.0263672zM264 192c0 22.0762 -17.9238 40 -40 40s-40 -17.9238 -40 -40s17.9238 -40 40 -40 +c22.0752 0 40 17.9248 40 40zM256 64h-64v-32h64v32zM352 64h-64v-32h64v32zM456 192c0 22.0762 -17.9238 40 -40 40s-40 -17.9238 -40 -40s17.9238 -40 40 -40c22.0752 0 40 17.9248 40 40zM448 64h-64v-32h64v32zM640 192v-128v-0.0390625 +c0 -17.6406 -14.3213 -31.9619 -31.9619 -31.9619c-0.0126953 0 -0.0253906 0.000976562 -0.0380859 0.000976562h-32v192h32h0.0390625c17.6406 0 31.9619 -14.3213 31.9619 -31.9619c0 -0.0126953 -0.000976562 -0.0253906 -0.000976562 -0.0380859z" /> +c9.50977 2.5498 17.8701 7.44043 25.4297 13.3203zM263 108c-13.2305 -13.4697 -33.8398 -15.8799 -49.7305 -5.82031c-6.13867 3.89746 -13.5029 6.15527 -21.3066 6.15527s-15.084 -2.25781 -21.2227 -6.15527c-15.9004 -10.0596 -36.5098 -7.64941 -49.7402 5.82031 +c-14.7305 15 -16.4004 14.04 -38.7803 20.1396c-13.8896 3.79004 -24.75 14.8408 -28.4697 28.9805c-7.48047 28.3994 -5.54004 24.9697 -25.9502 45.75c-10.1699 10.3604 -14.1396 25.4502 -10.4199 39.5898c7.48047 28.4199 7.46973 24.46 0 52.8203 +c-3.72949 14.1396 0.25 29.2295 10.4199 39.5801c20.4102 20.7793 18.4805 17.3594 25.9502 45.75c3.71973 14.1396 14.5801 25.1895 28.4697 28.9795c27.8906 7.61035 24.5303 5.62988 44.9404 26.4102c10.1699 10.3604 25 14.4004 38.8896 10.6104 +c27.9199 -7.61035 24.0303 -7.60059 51.9004 0c13.8896 3.79004 28.7197 -0.260742 38.8896 -10.6104c20.4297 -20.79 17.0703 -18.7998 44.9502 -26.4102c13.8896 -3.79004 24.75 -14.8398 28.4697 -28.9795c7.48047 -28.3906 5.54004 -24.9707 25.9502 -45.75 +c10.1699 -10.3506 14.1396 -25.4404 10.4199 -39.5801c-7.47949 -28.4102 -7.46973 -24.4502 0 -52.8301c3.71973 -14.1406 -0.25 -29.2305 -10.4199 -39.5801c-20.4102 -20.7803 -18.4697 -17.3506 -25.9502 -45.75c-3.71973 -14.1396 -14.5801 -25.1904 -28.4697 -28.9805 +c-21.7598 -5.92969 -23.5098 -4.58984 -38.79 -20.1396zM97.6602 272.04c0 -53.0303 42.2402 -96.0205 94.3398 -96.0205s94.3398 42.9902 94.3398 96.0205s-42.2402 96.0195 -94.3398 96.0195s-94.3398 -42.9893 -94.3398 -96.0195z" /> @@ -2884,12 +3187,12 @@ v16c0 4.41992 -3.58008 8 -8 8h-176c-4.41992 0 -8 -3.58008 -8 -8zM112 48c17.6699 h112c17.6699 0 32 14.3301 32 32v96c0 17.6699 -14.3301 32 -32 32h-112v-160zM400 48c17.6699 0 32 14.3301 32 32s-14.3301 32 -32 32s-32 -14.3301 -32 -32s14.3301 -32 32 -32z" /> +d="M431.34 325.95c44.9004 -16.3398 80.6602 -42.7803 80.6602 -86.1006v-160.229c0 -30.2705 -27.5 -57.6797 -72 -77.8604v101.9c0 13.2461 -10.7539 24 -24 24s-24 -10.7539 -24 -24v-118.93c-33.0498 -9.11035 -71.0703 -15.0605 -112 -16.7305v103.61 +c0 13.2461 -10.7539 24 -24 24s-24 -10.7539 -24 -24v-103.61c-40.9297 1.66992 -78.9502 7.62012 -112 16.7305v118.93c0 13.2461 -10.7539 24 -24 24s-24 -10.7539 -24 -24v-101.9c-44.5 20.1807 -72 47.5898 -72 77.8604v160.229c0 107.601 219.55 112.15 256 112.15 +c15.2197 0 62.4297 -0.910156 112.19 -9.69043l110.06 71c2.53711 1.69238 5.59082 2.7041 8.86621 2.7041c5.55664 0 10.4551 -2.83887 13.3242 -7.14355l8.86914 -13.3105c1.69238 -2.53711 2.7041 -5.58984 2.7041 -8.86523 +c0 -5.55664 -2.83887 -10.4561 -7.14355 -13.3242zM256 175.76c114.87 0 208 28.6904 208 64.0898c0 21.3105 -33.9102 40.1504 -85.8604 51.75l-118.64 -76.5195c-2.53711 -1.69141 -5.59082 -2.7041 -8.86621 -2.7041c-5.55664 0 -10.4551 2.83887 -13.3242 7.14355 +l-8.86914 13.3105c-1.69434 2.53809 -2.70703 5.59277 -2.70703 8.87012c0 5.55371 2.83594 10.4502 7.13672 13.3193l72.8096 47c-15.9492 1.2002 -32.5293 1.91016 -49.6797 1.91016c-114.88 0 -208 -28.6797 -208 -64.0801c0 -35.3994 93.1201 -64.0898 208 -64.0898z +" /> +c-8.83984 0 -16 -7.16016 -16 -16s7.16016 -16 16 -16h12.3896c18.6201 0 35.1104 11.8701 41 29.5303l10.6104 31.8799l16.8301 -50.46c2.03027 -6.14062 7.58008 -10.4404 14.0303 -10.8906c0.389648 -0.0292969 0.759766 -0.0498047 1.13965 -0.0498047 +c0.00390625 0 -0.00292969 -0.015625 0.000976562 -0.015625c6.26074 0 11.6865 3.60742 14.3086 8.85547l7.6709 15.3408c2.7998 5.59961 7.93945 6.18945 10.0195 6.18945s7.21973 -0.599609 10.1699 -6.51953c7.37012 -14.7207 22.1904 -23.8604 38.6396 -23.8604 +h47.1904c8.83984 0 16 7.16016 16 16s-7.16016 16 -16 16h-47.1904zM377 343c4.5 -4.5 7 -10.5996 7 -16.9004v-6.09961h-128v128h6.09961c6.40039 0 12.5 -2.5 17 -7z" /> +l0.00488281 0.00195312c4.27637 0 8.15039 -1.73633 10.9551 -4.54199l6.91992 -6.91992c2.91016 -2.91016 6.85059 -4.54004 10.96 -4.54004h10.0908c8.55957 0 15.5 -6.93945 15.5 -15.5c0 -6.66992 -4.27051 -12.5898 -10.6006 -14.7002l-47.3096 -15.7695 +c-3.90039 -1.2998 -8.15039 -1 -11.8301 0.839844l-14.7207 7.36035c-7.5791 3.7998 -15.9492 5.76953 -24.4297 5.76953h-0.889648c-12.2734 -0.00292969 -23.6533 -4.08594 -32.7803 -10.9297l-27.5801 -20.6904c-13.75 -10.3193 -21.8496 -26.5098 -21.8496 -43.6992 +v-14.0605c0.00292969 -15.0742 6.11328 -28.7393 16 -38.6299c10.25 -10.2402 24.1396 -16 38.6299 -16h25.8799c8.55957 0 15.5 -6.94043 15.5 -15.5v-29.8896c0 -12.6504 3.0293 -24.6885 8.33008 -35.29c4.7002 -9.40039 14.3096 -15.3398 24.8203 -15.3398 +c9.63477 0.000976562 18.1133 4.89551 23.0898 12.3594l13.0293 19.5498c7.18359 10.7715 15.4854 20.4473 25 29.1602c2.4707 2.27051 4.14062 5.27051 4.76074 8.56055l4.2998 22.8301c0.439453 2.3291 1.41016 4.5293 2.83008 6.42969l18.7402 24.9795 +c2.00977 2.68066 3.09961 5.9502 3.09961 9.30078v11.3398c0 8.55957 -6.94043 15.5 -15.5 15.5h-8.20996c-5.17969 0 -10.0205 2.58984 -12.8896 6.89941l-13.2402 19.8604c-5.66992 8.50977 -1.70996 20.0703 7.99023 23.2998l2.64941 0.879883 +c1.53906 0.511719 3.20312 0.78418 4.91309 0.78418c3.17383 0 6.12695 -0.955078 8.58691 -2.59375l18.21 -12.1396c2.45801 -1.6416 5.44043 -2.59863 8.61523 -2.59863c2.48438 0 4.83301 0.585938 6.91504 1.62793l15.3896 7.7002 +c5.25 2.62012 8.57031 7.99023 8.57031 13.8604v6.92969z" /> +c1.08008 8.37988 1.82031 16.8701 1.82031 25.54c0 32.1299 -7.7998 62.4102 -21.3203 89.3301l-12.9795 -6.49023c-3.74023 -1.85938 -6.91992 -4.67969 -9.24023 -8.14941l-19.5898 -29.3809c-2.54004 -3.80371 -4.02051 -8.4209 -4.02051 -13.334 +c0 -4.91211 1.48047 -9.48145 4.02051 -13.2852l17.9795 -26.9707c3.31055 -4.96973 8.36035 -8.51953 14.1504 -9.96973z" /> +c-0.490234 -1.7002 -2.06055 -2.87988 -3.83984 -2.87988h-3.80078c-1.66211 0.000976562 -3.08691 1.01465 -3.68945 2.45996l-5.35059 12.8496c-1.23926 2.99023 -4.15918 4.93066 -7.38965 4.93066h-12.0898 +c-0.00390625 0 -0.0146484 -0.00488281 -0.0185547 -0.00488281c-1.72168 0 -3.31738 -0.545898 -4.62109 -1.47559l-23.71 -16.8896c-1.73047 -1.23047 -3.61035 -2.25977 -5.59082 -3.0498l-39.3398 -15.7402c-3.04004 -1.21973 -5.0293 -4.16016 -5.0293 -7.42969 +v-10.2002l-0.00195312 -0.00390625c0 -2.20703 0.895508 -4.20703 2.3418 -5.65625l11.9102 -11.9102c3 -3 7.06934 -4.68945 11.3096 -4.68945h10.3398c1.31055 0 2.61035 0.15918 3.87988 0.479492l21.2705 5.32031c2.08203 0.520508 4.25391 0.802734 6.49707 0.802734 +c7.38574 0 14.0771 -2.99805 18.9229 -7.84277l13.0098 -13.0098c3 -3 7.07031 -4.69043 11.3096 -4.69043h15.1602c4.24023 0 8.31055 1.69043 11.3105 4.69043l9.56934 9.56934c3 3 4.69043 7.07031 4.69043 11.3105z" /> +c-18.4697 11.9805 -28.6396 33.3701 -28.6396 55.3906v62.3096c0 4.41992 3.58008 8 8 8h48c4.41992 0 8 -3.58008 8 -8v-62.3096c0 -6.82031 3.61035 -12.9805 9.28027 -16.7803zM360.89 95.9502c0.0371094 0 0.0556641 0.0351562 0.0927734 0.0351562 +c19.4336 0 36.8535 -8.68652 48.5879 -22.3857l117.949 -137.6h-88.4492c-19.4385 0 -36.8506 8.65137 -48.5898 22.3496l-117.801 137.431c1.40039 0.0195312 53.8105 0.109375 88.21 0.169922zM616 96c13.25 0 24 -10.7402 24 -24v-112c0 -13.25 -10.75 -24 -24 -24 +h-17.4199c-19.4375 0 -36.8506 8.65137 -48.5898 22.3496l-117.99 137.65h184z" /> +c0 -13.4707 -8.32422 -24.9951 -20.1201 -29.71l-139.88 -55.9502v288z" /> +c0.00292969 13.4697 8.32617 24.9932 20.1201 29.71zM288 88.3301c14.0703 0 27.3799 6.17969 36.5098 16.9502c19.6699 23.2002 40.5703 49.6299 59.4902 76.7197v-245.99l-192 64v182c18.9199 -27.0996 39.8301 -53.5195 59.4902 -76.7197 +c9.12988 -10.7803 22.4395 -16.96 36.5098 -16.96zM554.06 286.84c10.5107 4.2002 21.9404 -3.54004 21.9404 -14.8594v-250.32c0 -13.4707 -8.32422 -24.9951 -20.1201 -29.71l-139.88 -55.9502v288z" /> @@ -3263,11 +3567,11 @@ c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c0 2.7998 0.900391 5.40039 1. c0 24.2998 -13.7002 45.2002 -33.5996 56c0.699219 -2.59961 1.59961 -5.2002 1.59961 -8c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c0 2.7998 0.900391 5.40039 1.59961 8c-19.8994 -10.7998 -33.5996 -31.7002 -33.5996 -56c0 -35.2998 28.7002 -64 64 -64z " /> +c-26.4404 -7.36035 -54.5205 -5.85059 -81 1.35938l-287.601 78.3506c-9.58496 2.61621 -18.2998 7.45605 -25.4697 13.9297z" /> +c2.41504 1.22461 5.18066 1.91504 8.07227 1.91504c2.875 0 5.59277 -0.682617 7.99805 -1.89551l72.3496 -36.4697l103.21 52.3799l-156.22 98.0996c-8.08008 8.87988 -5.5 23.1201 5.16992 28.5303l65.75 33.3701c2.41504 1.22559 5.18164 1.91699 8.07324 1.91699 +c3.67383 0 7.08984 -1.11621 9.92676 -3.02734l218.7 -82.0596l98.5098 49.9902c26.7402 13.5596 56.4297 21.4199 86.2803 19.4795c33.5098 -2.17969 51.04 -12.8799 58.25 -27.4502c7.22949 -14.5596 5.23926 -35.1699 -13.0703 -63.6494 +c-16.3096 -25.3701 -40.2803 -44.7402 -67.0205 -58.3105l-290.96 -147.649c-8.88574 -4.51562 -19.001 -7.10645 -29.6396 -7.12012l-130.54 -0.180664c-9.22949 -0.00976562 -18.0498 3.87012 -24.3301 10.7109z" /> +d="M434.66 280.29c5.77344 -5.79004 9.34473 -13.7861 9.34473 -22.5996c0 -8.81445 -3.57129 -16.8008 -9.34473 -22.5908l-210.66 -211.1v271.12l75.4297 75.5195l0.0703125 0.0703125v0c5.75781 5.73633 13.707 9.28516 22.4688 9.28516 +c8.79883 0 16.7676 -3.57715 22.5312 -9.35547l90.1602 -90.3496v0zM480 128c17.6611 0 32 -14.3389 32 -32v-128c0 -17.6611 -14.3389 -32 -32 -32h-300c2.17969 1.91016 4.62012 3.41992 6.67969 5.49023l186.41 186.51h106.91zM192 416v-384 +c0 -52.9834 -43.0166 -96 -96 -96s-96 43.0166 -96 96v384c0 17.6611 14.3389 32 32 32h128c17.6611 0 32 -14.3389 32 -32zM96 8c13.2461 0 24 10.7539 24 24s-10.7539 24 -24 24s-24 -10.7539 -24 -24s10.7539 -24 24 -24zM128 192v64h-64v-64h64zM128 320v64h-64v-64h64z +" /> +c38.6895 0 70.0498 -29.4199 70.0498 -65.71v-60.1104l32.8799 21.9199c4.4502 2.9707 7.12012 7.95996 7.12012 13.3105v170.59c0 8.83984 7.16016 16 16 16h16c8.83984 0 16 -7.16016 16 -16v-170.59c0 -5.55273 2.81934 -10.4414 7.12012 -13.3105l32.8799 -21.9199 +v60.1104c0 36.29 31.3604 65.71 70.0498 65.71c43.9805 0 57.9307 -28.5596 80.0498 -63.1299c45.9707 -71.8701 80.3408 -149.72 102.011 -231.021z" /> +d="M128 192c70.6455 0 128 -57.3545 128 -128s-57.3545 -128 -128 -128s-128 57.3545 -128 128s57.3545 128 128 128zM507 246.86c14.2402 -24.3799 -3.58008 -54.8604 -32.0898 -54.8604h-213.82c-28.5098 0 -46.3301 30.4805 -32.0898 54.8604l106.93 182.85 +c6.48828 10.9688 18.3906 18.3311 32.0469 18.3311c13.6553 0 25.6055 -7.3623 32.0938 -18.3311zM480 160c17.6611 0 32 -14.3389 32 -32v-160c0 -17.6611 -14.3389 -32 -32 -32h-160c-17.6611 0 -32 14.3389 -32 32v160c0 17.6611 14.3389 32 32 32h160z" /> +c0 26.5 21.5 48 48 48h416zM250.58 96c11 0 18.7197 10.8496 15.1104 21.25l-53.6904 154.62c-3.25586 9.3877 -12.1758 16.1299 -22.666 16.1299h-0.00390625h-26.6602l0.00292969 0.00585938c-10.4873 0 -19.4131 -6.74219 -22.6729 -16.126l-53.7002 -154.63 +c-3.60938 -10.4004 4.11035 -21.25 15.1201 -21.25h16.9404c0.00195312 0 -0.000976562 -0.00390625 0.000976562 -0.00390625c6.99316 0 12.9453 4.49609 15.1191 10.7539l7.37988 21.25h70.29l7.36914 -21.25c2.24023 -6.42969 8.31055 -10.75 15.1201 -10.75h16.9404z +M424 112v160c0 8.83984 -7.16016 16 -16 16h-16c-8.83984 0 -16 -7.16016 -16 -16v-36.4199c-7.54004 2.68945 -15.54 4.41992 -24 4.41992c-39.7002 0 -72 -32.2998 -72 -72s32.2998 -72 72 -72c9.92969 0 19.4004 2.01953 28.0195 5.67969 +c2.94043 -3.41016 7.13086 -5.67969 11.9805 -5.67969h16c8.83984 0 16 7.16016 16 16z" /> @@ -3611,19 +3916,19 @@ c-6.62988 0 -12 -5.37012 -12 -12v-40c0 -6.62988 5.37012 -12 12 -12h40c6.62988 0 c0 6.62988 -5.37012 12 -12 12h-40c-6.62988 0 -12 -5.37012 -12 -12v-40c0 -6.62988 5.37012 -12 12 -12h40c6.62988 0 12 5.37012 12 12zM576 44v40c0 6.62988 -5.37012 12 -12 12h-40c-6.62988 0 -12 -5.37012 -12 -12v-40c0 -6.62988 5.37012 -12 12 -12h40 c6.62988 0 12 5.37012 12 12zM576 140v40c0 6.62988 -5.37012 12 -12 12h-40c-6.62988 0 -12 -5.37012 -12 -12v-40c0 -6.62988 5.37012 -12 12 -12h40c6.62988 0 12 5.37012 12 12z" /> +d="M256 416c141.38 0 256 -93.1201 256 -208s-114.62 -208 -256 -208c-38.4102 0 -74.71 7.07031 -107.4 19.3799c-24.6094 -19.6299 -74.3398 -51.3799 -140.6 -51.3799l-0.00195312 0.00195312c-4.41309 0 -7.99512 3.58301 -7.99512 7.99512 +c0 2.13184 0.835938 4.06934 2.19727 5.50293c0.5 0.530273 42.2598 45.4502 54.8193 95.7598c-35.6094 35.7305 -57.0195 81.1807 -57.0195 130.74c0 114.88 114.62 208 256 208zM280 113.56c30.29 3.62012 53.3701 30.9805 49.3203 63.04 +c-2.90039 22.96 -20.6602 41.3105 -42.9102 47.6699l-50.0703 14.3008c-3.59961 1.0293 -6.12012 4.35938 -6.12012 8.10938c0 4.64062 3.78027 8.41992 8.44043 8.41992h32.7803c0.0214844 0 0.0634766 -0.0126953 0.0859375 -0.0126953 +c3.62891 0 7.07422 -0.790039 10.1738 -2.20703c4.7998 -2.20996 10.3701 -1.70996 14.1094 2.03027l17.5205 17.5195c5.26953 5.27051 4.66992 14.2705 -1.5498 18.3799c-9.5 6.27051 -20.3604 10.1104 -31.7803 11.46v17.7305c0 8.83984 -7.16016 16 -16 16h-16 +c-8.83984 0 -16 -7.16016 -16 -16v-17.5498c-30.29 -3.62012 -53.3701 -30.9805 -49.3203 -63.0498c2.90039 -22.96 20.6602 -41.3203 42.9102 -47.6699l50.0703 -14.3008c3.59961 -1.0293 6.12012 -4.35938 6.12012 -8.10938 +c0 -4.64062 -3.78027 -8.41992 -8.44043 -8.41992h-32.7803c-3.59961 0 -7.0791 0.759766 -10.2598 2.21973c-4.7998 2.20996 -10.3701 1.70996 -14.1094 -2.03027l-17.5205 -17.5195c-5.26953 -5.27051 -4.66992 -14.2705 1.5498 -18.3799 +c9.5 -6.27051 20.3604 -10.1104 31.7803 -11.46v-17.7305c0 -8.83984 7.16016 -16 16 -16h16c8.83984 0 16 7.16016 16 16v17.5596z" /> +d="M464 320c26.4922 0 48 -21.5078 48 -48v-224c0 -26.4922 -21.5078 -48 -48 -48h-416c-26.4922 0 -48 21.5078 -48 48v288c0 26.4922 21.5078 48 48 48h160l64 -64h192zM359.5 152v16c0 8.83105 -7.16895 16 -16 16h-64v64c0 8.83105 -7.16895 16 -16 16h-16 +c-8.83105 0 -16 -7.16895 -16 -16v-64h-64c-8.83105 0 -16 -7.16895 -16 -16v-16c0 -8.83105 7.16895 -16 16 -16h64v-64c0 -8.83105 7.16895 -16 16 -16h16c8.83105 0 16 7.16895 16 16v64h64c8.83105 0 16 7.16895 16 16z" /> +d="M535.953 96c-42.6406 -94.1719 -137.641 -160 -247.984 -160c-4.26562 0 -8.54688 0.0986328 -12.8447 0.296875c-103.969 4.76562 -193.859 69.4688 -235.109 159.703h39.9219l-58.6094 58.5938c-2.65332 12.8242 -4.38672 25.9951 -5.10938 39.4219 +c-0.133789 3.5166 -0.202148 7.05078 -0.202148 10.5996c0 6.65527 0.234375 12.8477 0.702148 19.3848h47.2188l-41.3906 41.375c14.7842 66.6123 53.959 124.015 107.969 162.078c2.61426 1.87109 5.82812 2.98535 9.28125 3 +c5.62793 -0.03125 10.5791 -2.89355 13.5 -7.25c1.76367 -2.57422 2.7959 -5.68848 2.7959 -9.04199c0 -2.13086 -0.414062 -4.19141 -1.1709 -6.05176c-6.31445 -15.834 -9.84375 -33.1904 -9.84375 -51.2656c0 -45.1094 21.0469 -86.5781 57.7188 -113.734 +c4.07324 -2.96484 6.72266 -7.76855 6.72266 -13.1865c0 -4.86133 -2.13965 -9.2168 -5.51953 -12.2041c-26.5469 -23.9844 -41.1719 -56.5 -41.1719 -91.5781c0 -60.0312 42.9531 -110.281 99.8906 -121.922l2.5 65.2656l-27.1562 -18.4844 +c-1.29688 -0.832031 -2.83887 -1.31445 -4.49219 -1.31445c-2.10352 0 -4.04004 0.777344 -5.50781 2.06445c-1.55078 1.46387 -2.51953 3.53809 -2.51953 5.83691c0 1.49414 0.416992 2.90234 1.12891 4.10059l20.125 33.7656l-42.0625 8.73438 +c-3.64062 0.744141 -6.38379 3.96777 -6.38379 7.82812s2.74316 7.08398 6.38379 7.82812l42.0625 8.71875l-20.1094 33.7344c-0.724609 1.20312 -1.1416 2.61133 -1.1416 4.11719c0 4.41016 3.58105 7.99121 7.99121 7.99121c1.67188 0 3.22656 -0.510742 4.50977 -1.38965 +l30.3906 -20.6562l11.5166 287.969c0.15918 4.25879 3.66797 7.66699 7.96484 7.66699c0.0117188 0 0.0234375 0.00488281 0.0351562 0.00488281h0.046875c4.29004 -0.0332031 7.78418 -3.44629 7.95312 -7.70312l11.5312 -287.922l30.3906 20.6719 +c1.28223 0.855469 2.82227 1.35449 4.47852 1.35449c2.12793 0 4.07715 -0.820312 5.52148 -2.16699c1.54785 -1.45898 2.51465 -3.52832 2.51465 -5.82129c0 -1.48828 -0.415039 -2.89062 -1.12402 -4.08496l-20.1406 -33.7656l42.0781 -8.73438 +c3.63379 -0.750977 6.36914 -3.97266 6.36914 -7.82812s-2.73535 -7.07715 -6.36914 -7.82812l-42.0781 -8.71875l20.1094 -33.7344c0.730469 -1.20508 1.15039 -2.61719 1.15039 -4.12793c0 -2.27637 -0.947266 -4.33984 -2.47852 -5.79395 +c-1.46484 -1.32227 -3.4043 -2.12793 -5.53125 -2.12793c-1.6543 0 -3.20801 0.492188 -4.5 1.33105l-27.1719 18.4688l2.5 -65.3438c48.4844 9.40625 87.5781 48.1562 97.3125 96.5c1.68066 8.11816 2.56445 16.5254 2.56445 25.1387 +c0 36.5547 -15.8574 69.3145 -41.127 91.9395c-3.38867 2.98926 -5.52734 7.3623 -5.52734 12.2314c0 5.42578 2.64844 10.2256 6.73047 13.1904c36.6562 27.1719 57.6875 68.6094 57.6875 113.734v0.0859375c0 18.0664 -3.53613 35.4062 -9.85938 51.2266 +c-0.763672 1.86523 -1.18555 3.90625 -1.18555 6.0459c0 3.34668 1.0332 6.47949 2.79492 9.04785c2.9248 4.35059 7.875 7.20605 13.5 7.23438c3.44043 -0.0136719 6.64355 -1.12305 9.25 -2.98438c53.9287 -38.2227 93.0518 -95.6611 107.906 -162.281l-41.25 -41.2344 +h46.9531c0.359375 -5.76562 1.04688 -11.4531 1.04688 -17.2656c-0.0332031 -17.8086 -1.7959 -35.0137 -5.125 -51.8594l-58.8906 -58.875h39.9688z" /> +c1.67383 -1.4668 2.73047 -3.62012 2.73047 -6.01758c0 -4.41309 -3.58398 -7.99414 -7.99609 -7.99805h-0.015625c-1.97363 0.0996094 -3.79785 0.828125 -5.25 1.98438l-23.5938 20.6406c11.5469 -49.5781 55.7656 -86.625 108.859 -86.625 +s97.3125 37.0469 108.875 86.625l-23.5938 -20.6406c-1.40918 -1.22461 -3.25391 -1.96875 -5.26562 -1.96875h-0.015625c-2.34766 0.129883 -4.46777 1.14551 -6.01562 2.71875c-1.1543 1.45996 -1.88184 3.28809 -1.98438 5.26562 +c0.128906 2.35059 1.15137 4.47266 2.73438 6.01562l37.1094 32.4688c0.015625 0.53125 0.15625 1 0.15625 1.51562c0 11.0469 -2.09375 21.5156 -5.0625 31.5938l-21.2656 -21.25c-1.44922 -1.4502 -3.45117 -2.34863 -5.66211 -2.34863 +c-4.41797 0 -8.00488 3.58691 -8.00488 8.00488c0 2.20605 0.892578 4.20801 2.33887 5.65625l26.4219 26.4062c-10.0342 20.8945 -26.1904 38.0244 -46.3594 49.2656c6.05371 -9.67676 9.55469 -21.1123 9.55469 -33.3584c0 -19.916 -9.17383 -37.7295 -23.6172 -49.2822 +c9.69336 -10.0459 15.6592 -23.7119 15.6592 -38.7598c0 -26.875 -19.0703 -49.3535 -44.3779 -54.6621l-1.42188 34.2812l12.6719 -8.625c0.635742 -0.432617 1.40234 -0.685547 2.22852 -0.685547c0.00585938 0 0.015625 -0.00195312 0.0214844 -0.00195312h0.0263672 +c2.19727 0 3.98047 1.7832 3.98047 3.98047c0 0.748047 -0.209961 1.45215 -0.569336 2.05078l-8.53125 14.3125l17.9062 3.71875c1.81738 0.379883 3.18457 1.99219 3.18457 3.92188s-1.36719 3.54199 -3.18457 3.92188l-17.9062 3.71875l8.53125 14.3125 +c0.359375 0.598633 0.566406 1.29883 0.566406 2.04688c0 2.19629 -1.7832 3.98047 -3.98047 3.98047c-0.00878906 0 -0.0146484 0.00390625 -0.0234375 0.00390625c-0.817383 -0.0322266 -1.58984 -0.275391 -2.25 -0.671875l-14.1875 -9.65625l-4.6875 112.297 +c-0.09375 2.12695 -1.84961 3.8252 -4 3.8252s-3.90625 -1.69824 -4 -3.8252l-4.625 -110.812l-12 8.15625c-0.639648 0.43457 -1.41211 0.688477 -2.24316 0.688477c-2.20996 0 -4.00293 -1.79395 -4.00293 -4.00391c0 -0.745117 0.203125 -1.44629 0.558594 -2.04395 +l8.53125 -14.3125l-17.9062 -3.71875c-1.81738 -0.375977 -3.18457 -1.98633 -3.18457 -3.91406s1.36719 -3.53809 3.18457 -3.91406l17.9062 -3.73438l-8.53125 -14.2969c-0.330078 -0.611328 -0.532227 -1.31152 -0.5625 -2.04688 +c0.0615234 -1.12109 0.525391 -2.14062 1.25 -2.90625c0.717773 -0.677734 1.68652 -1.09277 2.75 -1.09375c0.830078 0.00390625 1.60645 0.257812 2.25 0.6875l10.3594 7.04688l-1.35938 -32.7188c-25.3086 5.31836 -44.335 27.79 -44.335 54.6709 +c0 15.0518 5.92285 28.7324 15.6162 38.7822c-14.4434 11.5508 -23.7012 29.3193 -23.7012 49.2334c0 12.2559 3.59082 23.7412 9.6543 33.4229c-20.1709 -11.2451 -36.3311 -28.374 -46.375 -49.2656l26.4219 -26.4219c1.43945 -1.44727 2.33008 -3.44043 2.33008 -5.64062 +c0 -4.41504 -3.58496 -8 -7.99902 -8c-2.2002 0 -4.19629 0.888672 -5.64355 2.32812l-21.2656 21.2656c-2.98438 -10.0938 -5.07812 -20.5625 -5.0625 -31.625z" /> +c-3.47949 -0.950195 -5.88965 -4.11035 -5.88965 -7.71973v-16.5801c0 -5.28027 5.01953 -9.11035 10.1104 -7.7207l96 26.1807c3.47949 0.950195 5.88965 4.10938 5.88965 7.71973zM448 234.47v-16.5801c0 -0.00195312 0.00195312 -0.00195312 0.00195312 -0.00390625 +c0 -3.68359 2.49609 -6.78906 5.8877 -7.71582l80 -21.8203c5.09082 -1.38965 10.1104 2.44043 10.1104 7.7207v16.5801c0 3.60938 -2.41016 6.76953 -5.88965 7.71973l-80 21.8203c-5.09082 1.38965 -10.1104 -2.44043 -10.1104 -7.7207zM304 273.74v-16.5801 +c0 -0.00195312 0.00195312 -0.00292969 0.00195312 -0.00488281c0 -3.68359 2.49609 -6.78906 5.8877 -7.71484l96 -26.1807c5.09082 -1.38965 10.1104 2.44043 10.1104 7.7207v16.5791c0 3.61035 -2.41016 6.77051 -5.88965 7.7207l-96 26.1797 +c-5.09082 1.38965 -10.1104 -2.44043 -10.1104 -7.71973z" /> +d="M501.62 355.89c6.24023 -2.33984 10.3799 -8.30957 10.3799 -14.9795v-36.9102c0 -8.83984 -7.16016 -16 -16 -16h-480c-8.83984 0 -16 7.16016 -16 16v36.9102c0.000976562 6.85547 4.31445 12.7041 10.3799 14.9795l234.39 90.0703 +c3.49219 1.31152 7.30176 2.02832 11.25 2.02832c3.94727 0 7.72852 -0.716797 11.2207 -2.02832zM64 256h64v-160h96v160h64v-160h96v160h64v-160h16c8.83984 0 16 -7.16016 16 -16v-48h-448v48c0 8.83984 7.16016 16 16 16h16v160zM496 0c8.83984 0 16 -7.16016 16 -16 +v-32c0 -8.83984 -7.16016 -16 -16 -16h-480c-8.83984 0 -16 7.16016 -16 16v32c0 8.83984 7.16016 16 16 16h480z" /> +d="M272 256.09c17.5996 0 32 -14.3994 32 -32v-128c0 -51.8896 -34.8398 -98.0801 -84.75 -112.35l-179.19 -46.6201c-2.64941 -0.69043 -5.36914 -1.03027 -8.05957 -1.03027c-23.4805 0 -32 21.1797 -32 32v96 +c0 0.00390625 -0.00488281 -0.000976562 -0.00488281 0.00292969c0 14.1221 9.1748 26.1182 21.8848 30.3477l90.1201 30.04v80.2295c0 18.9805 5.55957 37.3896 16.1201 53.2305l117.26 175.899c0.169922 0.270508 0.589844 0.25 0.790039 0.480469 +c9.58008 13.5098 27.8496 17.8799 42.2998 9.20996c15.1602 -9.10059 20.0605 -28.75 10.9707 -43.9102l-77.75 -129.59c-8.9707 -14.9199 -13.6904 -32 -13.6904 -49.3906v-76.5498c0 -8.83984 7.16016 -16 16 -16s16 7.16016 16 16v80c0 17.6006 14.4004 32 32 32z +M618.12 94.3604c13.0703 -4.36035 21.8799 -16.5801 21.8799 -30.3506v-96c0 -10.8193 -8.51953 -32 -32 -32c-2.67969 0 -5.40039 0.339844 -8.05957 1.03027l-179.19 46.6201c-49.9102 14.2598 -84.75 60.4502 -84.75 112.34v128c0 17.5996 14.4004 32 32 32 +s32 -14.4004 32 -32v-80c0 -8.83984 7.16016 -16 16 -16s16 7.16016 16 16v76.5498c0 17.3906 -4.71973 34.4697 -13.6904 49.3906l-77.75 129.59c-9.08984 15.1602 -4.18945 34.8193 10.9707 43.9102c14.4502 8.66992 32.7197 4.2998 42.2998 -9.20996 +c0.200195 -0.240234 0.610352 -0.210938 0.790039 -0.480469l117.26 -175.89c10.5605 -15.8408 16.1201 -34.25 16.1201 -53.2305v-80.2295z" /> +c-52.3096 0 -94.8594 42.5596 -94.8594 94.8594c0 52.3105 42.5498 94.8604 94.8594 94.8604c1.04004 0 3.45996 -0.209961 4.13086 -0.209961c0.738281 -0.276367 1.54004 -0.429688 2.375 -0.429688c3.73926 0 6.77441 3.03516 6.77441 6.77441 +c0 3.7373 -3.0332 6.77246 -6.76953 6.77539c-13.1201 4.91992 -26.71 7.41016 -40.3799 7.41016zM380.8 0v64h-284.8c-16 0 -32 -12.7998 -32 -32s12.7998 -32 32 -32h284.8z" /> +v-208c0 -41.8877 -20.0566 -79.043 -51.2002 -102.4l-115.2 -86.3994c-17.2695 -12.9502 -37.4893 -19.2002 -57.5195 -19.2002c-32.8105 0 -65.1699 16.75 -83.4199 48.3301c-24.6504 42.6396 -10.1904 97.5 29.21 127.06z" /> +c0 13.2598 10.75 24 24 24h81.4697c12.0801 -0.00292969 22.584 -6.67871 28.0303 -16.5703l58.4102 -106.1l84.79 322.8c3.68945 14.0703 16.4102 23.8701 30.9502 23.8701h244.35z" /> +l18.46 -30.8203h-36.8496zM382.45 136.5l18.4102 30.7998l18.4492 -30.7998h-36.8594zM128 -16v416h384v-416h-384zM194.77 262.13c-1.7627 -3.04492 -2.77148 -6.62402 -2.77148 -10.3936c0 -3.92969 1.09668 -7.60547 3.00195 -10.7363l29.3604 -49l-29.21 -48.8398 +c-1.91211 -3.17578 -3.02637 -6.91699 -3.02637 -10.8906c0 -11.6504 9.45898 -21.1094 21.1104 -21.1094h0.015625h59.5l29.25 -48.8799c3.61816 -6.12793 10.2754 -10.2207 17.9004 -10.2207h0.0996094c7.7373 0.0166016 14.4912 4.17676 18.1602 10.4004l29.1299 48.7002 +h59.4697c0.0078125 0 0.00195312 -0.0224609 0.00878906 -0.0224609c7.90723 0 14.8115 4.32812 18.4717 10.7422c1.75879 3.04199 2.76562 6.61621 2.76562 10.3799c0 3.93164 -1.09863 7.6084 -3.00586 10.7402l-29.3701 49l29.2402 48.8496 +c1.90723 3.17383 3.01758 6.91113 3.01758 10.8809c0 11.6553 -9.46191 21.1182 -21.1182 21.1191h-59.5195l-29.25 48.8604c-3.6123 6.12207 -10.2617 10.21 -17.8779 10.21h-0.0722656c-0.0117188 0 -0.00976562 0.0224609 -0.0214844 0.0224609 +c-7.74316 0 -14.5186 -4.17383 -18.1982 -10.3926l-29.1299 -48.71h-59.4502c-0.015625 0 -0.0166016 0.0224609 -0.0322266 0.0224609c-7.89844 0 -14.7939 -4.32422 -18.4482 -10.7324zM592 448c26.5098 0 48 -14.3301 48 -32v-448c0 -17.6699 -21.4902 -32 -48 -32 +s-48 14.3301 -48 32v448c0 17.6699 21.4902 32 48 32zM320 302.47l17.6797 -29.6201h-35.46zM257.55 247.47l-18.3701 -30.7998l-18.4395 30.7998h36.8096zM287.13 136.47l-33.2295 55.5303l33.1699 55.5195h65.79l33.2295 -55.5195l-33.1699 -55.5303h-65.79z" /> +d="M298.06 224l149.94 -53.5498v-218.45c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v112h-160v-112c0 -8.83105 -7.16895 -16 -16 -16h-64c-8.83105 0 -16 7.16895 -16 16v213.91c-37.1602 13.25 -64 48.4297 -64 90.0898 +c0 17.6611 14.3389 32 32 32s32 -14.3389 32 -32c0.0332031 -17.6455 14.3545 -31.9668 32 -32h170.06zM544 336v-32c0 -35.3223 -28.6777 -64 -64 -64h-32v-35.5801l-128 45.71v149.87c0 14.25 17.2197 21.3896 27.3096 11.3096l27.2803 -27.3096h53.6299 +c10.9102 0 23.75 -7.91992 28.6201 -17.6904l7.16016 -14.3096h64c8.83105 0 16 -7.16895 16 -16zM432 336c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16s7.16895 -16 16 -16s16 7.16895 16 16z" /> +c10.0703 0 19.5498 -4.7002 25.6006 -12.7598l74.5293 -99.3799c4.00781 -5.3457 6.37988 -12.042 6.37988 -19.2305c0 -5.12988 -1.20996 -9.98047 -3.35938 -14.2803l-14.3105 -28.6191c-5.25 -10.502 -16.0889 -17.6895 -28.6191 -17.6904h-30.9707 +c-8.48926 0 -16.6299 3.37012 -22.6299 9.37012l-28.0898 22.6299h-64v-36.6904c0.00195312 -18.791 10.7812 -35.0459 26.5303 -42.9199zM489.18 381.75c-4.33008 -17.1396 8.56055 -28.96 21.5205 -29.6699c11.6602 -0.629883 21.3799 7.34961 24.1299 18.2598z" /> +d="M462.8 398.43c34.3203 -34.2793 50.4307 -79.5996 49.1299 -124.56c-41.9795 22.6602 -94.3594 17.5596 -128.739 -16.7998c-40.8809 -40.8398 -40.6904 -107.181 -1.05078 -151.07c-18.9736 -6.45312 -39.3203 -10.0049 -60.4648 -10.0049 +c-0.475586 0 -0.950195 0.000976562 -1.4248 0.00488281h-85.8896l-40.6104 -40.5596c-9.71973 -9.75 -11.0898 -24.0205 -6 -36.75c2.77051 -6.92383 4.3125 -14.5234 4.3125 -22.4316c0 -33.3086 -27.042 -60.3506 -60.3496 -60.3506 +c-16.7041 0 -31.8311 6.80078 -42.7627 17.7822c-15.2803 15.2695 -19.6006 36.5 -15.1006 56.0996c-19.6094 -4.49023 -40.8496 -0.179688 -56.1191 15.0703c-10.9395 10.9229 -17.668 26.002 -17.668 42.666c0 33.2979 27.0332 60.3301 60.3301 60.3301 +c7.88965 0 15.4277 -1.51758 22.3379 -4.27637c12.7793 -5.07031 27.0791 -3.69043 36.7793 6l40.6201 40.5898v85.8301c0 64 27.6904 107 63.1699 142.43c30.666 30.6338 73.0479 49.5889 119.774 49.5889s89.0605 -18.9551 119.726 -49.5889z" /> @@ -3957,10 +4264,10 @@ c0 -4.41992 3.58008 -8 8 -8h12.2695zM256 184c0 4.41992 -3.58008 8 -8 8h-16c-4.41 c23.4004 25.1992 36.2803 58.6094 36.2803 94.0898v20.7998c0 4.41992 -3.58008 8 -8 8h-16c-4.41992 0 -8 -3.58008 -8 -8v-20.7998c0 -20.2705 -5.7002 -40.1807 -16 -56.8799c-10.2998 16.71 -16 36.6094 -16 56.8799v20.7998zM377 343c4.5 -4.5 7 -10.5996 7 -16.9004 v-6.09961h-128v128h6.09961c6.40039 0 12.5 -2.5 17 -7z" /> @@ -3984,14 +4291,14 @@ M176 320c-13.25 0 -24 11.9502 -24 26.6699s24 53.3301 24 53.3301s24 -38.5996 24 - c0 -14.7295 -10.75 -26.6699 -24 -26.6699zM400 320c-13.25 0 -24 11.9502 -24 26.6699s24 53.3301 24 53.3301s24 -38.5996 24 -53.3301c0 -14.7295 -10.75 -26.6699 -24 -26.6699zM464 320c-13.25 0 -24 11.9502 -24 26.6699s24 53.3301 24 53.3301 s24 -38.5996 24 -53.3301c0 -14.7295 -10.75 -26.6699 -24 -26.6699zM528 320c-13.25 0 -24 11.9502 -24 26.6699s24 53.3301 24 53.3301s24 -38.5996 24 -53.3301c0 -14.7295 -10.75 -26.6699 -24 -26.6699z" /> +d="M496 0c8.83984 0 16 -7.16016 16 -16v-32c0 -8.83984 -7.16016 -16 -16 -16h-480c-8.83984 0 -16 7.16016 -16 16v32c0 8.83984 7.16016 16 16 16h480zM192 64l16 -32h-144l110.96 249.66c11.1211 25.0264 29.8379 45.6514 53.46 59.1494l187.58 107.19l-56.2998 -168.92 +c-2.12207 -6.35938 -3.25781 -13.2188 -3.25781 -20.2881c0 -8.93164 1.83496 -17.4375 5.14746 -25.1621l86.4102 -201.63h-208l16 32l64 32l-64 32l-32 64l-32 -64l-64 -32zM256 288l-32 -16l32 -16l16 -32l16 32l32 16l-32 16l-16 32z" /> +d="M575.92 371.4l0.0605469 -77.71c0 -0.0107422 0.0185547 -0.00683594 0.0185547 -0.0166016c0 -13.4707 -8.34277 -25.0088 -20.1387 -29.7236l-32.5508 -13.0205c-15.4395 -6.17969 -33.04 0.5 -40.4893 15.3701l-18.9004 37.7002l-16 7.11035v-102.471 +c0.00976562 -0.219727 0.0800781 -0.419922 0.0800781 -0.639648c0 -30.4697 -12.2598 -58.0303 -32 -78.2197v-177.78c0 -8.83984 -7.16016 -16 -16 -16h-64c-8.83984 0 -16 7.16016 -16 16v150.4l-133.97 22.3301l-23.8398 -63.5908l26.3096 -105.26 +c2.53027 -10.0996 -5.11035 -19.8799 -15.5195 -19.8799h-65.9609c-7.48633 0 -13.7783 5.16602 -15.5098 12.1201l-24.8496 99.4102c-1.24707 4.98047 -1.8916 10.1924 -1.8916 15.5576c0 7.8916 1.43262 15.4502 4.05176 22.4316l25.7197 68.6006 +c-18.7002 17.5195 -30.54 42.2402 -30.54 69.8799c0 2.62988 0.570312 5.09961 0.780273 7.67969c-9.91016 -7.29004 -16.7803 -18.46 -16.7803 -31.6797v-56c0 -8.83984 -7.16016 -16 -16 -16h-16c-8.83984 0 -16 7.16016 -16 16v56c0 48.5303 39.4697 88 88 88v-1.11035 +c17.5996 20.1299 43.1602 33.1104 72 33.1104h159.92c0 70.6904 57.3105 128 128 128h119.98c5.05957 0 8.94922 -4.67969 7.92969 -9.63965c-2.67969 -13.1699 -11.1201 -23.8203 -22.1797 -30.6602c5.10938 -5.37988 9.90918 -10.4697 13.6895 -14.5 +c5.56055 -5.93066 8.57031 -13.6699 8.58008 -21.7998zM511.92 352c8.83984 0 16 7.16016 16 16s-7.16016 16 -16 16s-16 -7.16016 -16 -16s7.16016 -16 16 -16z" /> +d="M634.92 -14.7002c3.2041 -4.98145 5.06348 -10.9756 5.06348 -17.334c0 -5.53906 -1.41113 -10.751 -3.89355 -15.2959c-5.60938 -10.2803 -16.3799 -16.6699 -28.0898 -16.6699h-576c-12.1191 0 -22.6582 6.7168 -28.0898 16.6602 +c-2.48242 4.5459 -3.89355 9.82715 -3.89355 15.3672c0 6.36035 1.85938 12.2891 5.06348 17.2725l288 448c5.88965 9.16016 16.0303 14.7002 26.9199 14.7002s21.0303 -5.54004 26.9199 -14.7002zM320 356.82l-102.06 -158.761l38.0596 -38.0596l64 64h85.3896z" /> +c-4.91016 28.1201 5 54.2197 23.1904 71.7998c23.5596 22.75 39.5596 52.1396 39.5596 84.8896v1.61035c0 106.04 85.96 192 192 192h56l153.25 87.5703c9.66992 5.51953 20.6104 8.42969 31.75 8.42969h20.4902c0.00390625 0 0.0166016 0.00878906 0.0214844 0.00878906 +c17.6602 0 33.6582 -7.17188 45.2383 -18.7588l13.25 -13.25h32zM512 400c-8.83984 0 -16 -7.16016 -16 -16s7.16016 -16 16 -16s16 7.16016 16 16s-7.16016 16 -16 16zM544 304c20.8301 0 38.4297 13.4199 45.0498 32h-77.0498l-118.57 -59.29l13.7705 -27.5498 +l101.84 54.8398h34.96z" /> +c0 0.00292969 0.0205078 0.0400391 0.0205078 0.0439453c0 6.20898 1.77246 12.0078 4.83984 16.916l60.8301 97.3301h-47.0605l-48 -72c-4.89941 -7.35059 -14.8398 -9.33984 -22.1895 -4.44043l-13.3105 8.87988c-7.36035 4.90039 -9.33984 14.8398 -4.43945 22.1904 +l52.7393 79.1299c5.74121 8.60547 15.5186 14.248 26.6299 14.25h77.9404l-68.9902 24.3496c-6.81738 2.27441 -12.5947 6.74023 -16.5098 12.6104l-53.5996 80.4102c-4.90039 7.36035 -2.91016 17.29 4.43945 22.1895l13.3105 8.88086 +c7.35938 4.89941 17.29 2.90918 22.1895 -4.44043l50.5703 -75.8301l60.4902 -20.1699h36.0996l10.3701 51.8496c2.18945 10.9707 17.3701 60.1504 69.6299 60.1504s67.4404 -49.1797 69.6299 -60.1504l10.3701 -51.8496h36.0996l60.5 20.1699l50.5605 75.8301 +c4.89941 7.34961 14.8398 9.33984 22.1895 4.44043l13.3105 -8.88086c7.34961 -4.89941 9.33984 -14.8398 4.43945 -22.1895l-53.5996 -80.4102c-3.91504 -5.87012 -9.69238 -10.3359 -16.5098 -12.6104l-68.9902 -24.3594h77.9404 +c11.1084 -0.00292969 20.8828 -5.64453 26.6191 -14.25zM406.09 350.49l-23.7998 71.3896c-2.79004 8.37988 1.74023 17.4404 10.1201 20.2402l15.1699 5.05957c8.37988 2.80078 17.4502 -1.73926 20.2402 -10.1201l25.8896 -77.6797 +c1.06152 -3.18164 1.62598 -6.62109 1.62598 -10.1582c0 -5.12695 -1.20801 -9.97461 -3.35547 -14.2715l-27.1504 -54.2998l-25.9297 -8.65039h-4.66992l-5.2207 26.1201c-0.719727 3.58008 -1.7998 7.58008 -3.20996 11.79z" /> +c-8.58984 8.58984 -8.58984 22.5195 0 31.1104l31.1104 31.1094c7.92969 7.93066 20.2598 8.2002 28.8896 1.4707v146.52c0 26.4697 21.5303 48 48 48h133.45c0.015625 0 0.00878906 0.0341797 0.0244141 0.0341797c19.7969 0 36.8047 -12.0312 44.1055 -29.1738 +l56.0898 -130.86h102.33v40.2002c0 29.9902 10.5801 58.8994 29.5 81.7197c6.37988 7.7002 18.04 8.23047 24.7002 0.780273l21.6299 -24.1699c4.87012 -5.43066 5.74023 -13.6904 1.32031 -19.4902c-8.4502 -11.0801 -13.1504 -24.7197 -13.1504 -38.8398v-40.2002h64z +M176 32c44.1797 0 80 35.8203 80 80s-35.8203 80 -80 80s-80 -35.8203 -80 -80s35.8203 -80 80 -80zM198 288h110.04l-41.1504 96h-106.89v-96h38z" /> +d="M511.328 427.197c-11.6074 -38.7021 -34.3076 -111.702 -61.3037 -187.701c6.99902 -2.09375 13.4043 -4 18.6074 -5.59277c6.58301 -2.00684 11.3779 -8.13184 11.3779 -15.3672c0 -2.71875 -0.685547 -5.29395 -1.87988 -7.53906 +c-22.1055 -42.2969 -82.6904 -152.795 -142.479 -214.403c-0.999023 -1.09375 -1.99902 -2.5 -2.99902 -3.5c-35.2676 -35.2773 -83.9824 -57.1094 -137.757 -57.1094c-107.53 0 -194.83 87.2998 -194.83 194.83c0 53.7559 21.7637 102.511 57.0195 137.775 +c1 1 2.40625 2 3.49902 3c61.6006 59.9053 171.975 120.405 214.374 142.498c2.24512 1.19434 4.80664 1.87109 7.52441 1.87109c7.23535 0 13.374 -4.78711 15.3779 -11.3711c1.59375 -5.09375 3.5 -11.5928 5.59277 -18.5928 +c75.8955 26.999 148.978 49.7021 187.675 61.2959c1.4834 0.448242 3.05664 0.689453 4.68652 0.689453c8.93164 0 16.1826 -7.25098 16.1826 -16.1826c0 -1.59961 -0.236328 -3.14062 -0.668945 -4.60059zM319.951 127.998 +c-0.00976562 70.626 -57.3525 127.962 -127.98 127.962c-70.6348 0 -127.98 -57.3457 -127.98 -127.98c0 -70.6338 57.3457 -127.979 127.98 -127.979c70.6318 0 127.976 57.3438 127.976 127.976c0 0.0078125 0.00488281 0.0146484 0.00488281 0.0224609zM191.971 159.997 +c-0.00292969 -17.6562 -14.3379 -31.9902 -31.9951 -31.9902c-17.6582 0 -31.9951 14.3369 -31.9951 31.9951c0 17.6592 14.3369 31.9951 31.9951 31.9951h0.0371094c17.6387 0 31.959 -14.3203 31.959 -31.959 +c0 -0.0136719 -0.000976562 -0.0263672 -0.000976562 -0.0410156v0zM223.966 79.998c-0.000976562 -8.82812 -7.16895 -15.9951 -15.998 -15.9951s-15.9971 7.16895 -15.9971 15.998s7.16797 15.9971 15.9971 15.9971c8.81738 -0.0283203 15.9707 -7.18262 15.998 -16v0z +" /> d="M96 -48c0 -8.7998 -7.2002 -16 -16 -16h-32c-8.7998 0 -16 7.2002 -16 16v480c0 8.7998 7.2002 16 16 16h32c8.7998 0 16 -7.2002 16 -16v-480zM224 -48c0 -8.7998 -7.2002 -16 -16 -16h-32c-8.7998 0 -16 7.2002 -16 16v480c0 8.7998 7.2002 16 16 16h32 c8.7998 0 16 -7.2002 16 -16v-480z" /> +d="M502.63 409c5.77344 -5.79004 9.34473 -13.7852 9.34473 -22.5996c0 -8.8291 -3.58398 -16.8281 -9.375 -22.6201l-46.3301 -46.3203c-3.82617 -3.83691 -8.53223 -6.78125 -13.7891 -8.53027l-36.4805 -12.1602l-76.2402 -76.2393 +c8.79004 -12.2002 15.7705 -25.5605 19.1602 -40.2002c7.74023 -33.3896 0.870117 -66.8701 -22 -89.75c-9.26367 -9.2207 -20.71 -16.2314 -33.4795 -20.25c-18.54 -6.00977 -32.6709 -23.29 -34.4307 -42.1396c-2.29004 -23.8105 -11.4502 -45.8301 -28.4502 -62.71 +c-45.5596 -45.4805 -127.5 -37.3809 -182.979 18.0693c-55.4805 55.4502 -63.6904 137.45 -18.0498 182.96c16.8799 16.9902 38.9102 26.1699 62.6094 28.4404c18.9404 1.76953 36.1504 15.8994 42.1504 34.46c4.01172 12.7686 11.0195 24.2119 20.2402 33.4697 +c22.8799 22.8799 56.4297 29.7803 89.8799 22c14.5996 -3.39941 27.9395 -10.3799 40.0996 -19.1396l76.2598 76.2598l12.1602 36.5098c1.74902 5.25781 4.69336 9.96387 8.53027 13.79l46.2803 46.3301c5.79199 5.79395 13.8018 9.37988 22.6338 9.37988 +s16.833 -3.58594 22.626 -9.37988zM208 96c26.4922 0 48 21.5078 48 48s-21.5078 48 -48 48s-48 -21.5078 -48 -48s21.5078 -48 48 -48z" /> @@ -4387,20 +4696,21 @@ c14.2998 -1.2002 26.5 -10.7002 29.7998 -24.2002zM336 448c8.7998 0 16 -7.2002 16 c0 -13.2998 -10.7002 -24 -24 -24h-8v-136c0 -13.2998 -10.7002 -24 -24 -24h-80c-13.2998 0 -24 10.7002 -24 24v136h-8c-13.2998 0 -24 10.7002 -24 24v136c0 25.0996 19.2998 45.5 43.9004 47.5996c15 -9.7998 32.8994 -15.5996 52.0996 -15.5996 s37.0996 5.7998 52.0996 15.5996z" /> +d="M502.609 137.958l-96.7041 -96.7168c-5.76758 -5.74707 -13.7207 -9.30176 -22.499 -9.30176c-8.77734 0 -16.7402 3.55469 -22.5078 9.30176l-80.3262 80.418l-9.89258 -9.9082c10.8848 -23.9746 16.9482 -50.5957 16.9482 -78.6221 +c0 -32.3584 -8.10156 -63.1982 -22.3555 -89.9004c-4.50098 -8.50098 -16.3936 -9.59473 -23.207 -2.79785l-107.519 107.515l-17.7998 -17.7988c0.703125 -2.60938 1.60938 -5.00098 1.60938 -7.79785c0 -17.6641 -14.3408 -32.0059 -32.0049 -32.0059 +s-32.0059 14.3418 -32.0059 32.0059s14.3418 32.0039 32.0059 32.0039c2.79688 0 5.18848 -0.90625 7.79785 -1.60938l17.7998 17.7998l-107.518 107.515c-6.79883 6.8125 -5.7041 18.6113 2.79688 23.2061c26.7031 14.2539 57.1895 22.3359 89.5479 22.3359 +c28.0273 0 55.0049 -6.04395 78.9805 -16.9297l9.79883 9.79883l-80.3105 80.417c-5.74609 5.78613 -9.29785 13.7539 -9.29785 22.5449s3.55176 16.7686 9.29785 22.5547l96.7197 96.7168c5.72754 5.74512 13.6484 9.30273 22.3945 9.30273 +c0.0351562 0 0.0732422 -0.00488281 0.109375 -0.00488281h0.0458984c8.79199 0 16.7656 -3.5498 22.5518 -9.29785l80.3262 -80.3076l47.8047 47.8965c6.08301 6.07715 14.4805 9.83789 23.749 9.83789c9.26953 0 17.6768 -3.76074 23.7588 -9.83789l47.5088 -47.5059 +c6.07031 -6.08594 9.82617 -14.4824 9.82617 -23.749s-3.75586 -17.6719 -9.82617 -23.7578l-47.8057 -47.8975l80.3105 -80.417c5.73633 -5.75195 9.28516 -13.6865 9.28516 -22.4434c0 -8.81348 -3.59277 -16.8018 -9.39453 -22.5625zM219.562 250.567l73.8252 73.8223 +l-68.918 68.8994l-73.8096 -73.8066zM457.305 160.461l-68.9023 68.916l-73.8242 -73.8232l68.918 -68.8994z" /> +c-0.6875 2.60938 -1.59375 5.00098 -1.59375 7.81348c0 17.6631 14.3398 32.0039 32.0039 32.0039c17.6631 0 32.0039 -14.3408 32.0039 -32.0039c0 -17.6641 -14.3408 -32.0039 -32.0039 -32.0039c-2.79785 0 -5.2041 0.890625 -7.79785 1.59375l-27.4102 -27.4102z +M511.976 144.933c0.0175781 -0.301758 0.0253906 -0.605469 0.0253906 -0.912109c0 -8.86133 -7.1748 -16.0488 -16.0273 -16.0898h-32.1133c-8.46289 0.0244141 -15.3867 6.65918 -15.8926 15.002c-7.50098 129.519 -111.515 234.533 -240.937 241.534 +c-8.34863 0.444336 -14.9902 7.36426 -14.9902 15.8223c0 0.0292969 -0.0126953 0.0566406 -0.0117188 0.0859375v31.5986c0.0361328 8.85156 7.2334 16.0264 16.0938 16.0264c0.308594 0 0.603516 -0.00683594 0.908203 -0.0244141 +c163.224 -8.59473 294.443 -139.816 302.944 -303.043zM415.964 145.229c0.0244141 -0.364258 0.0371094 -0.732422 0.0371094 -1.10254c0 -8.92578 -7.23145 -16.1621 -16.1484 -16.1963h-32.208c-8.34961 0.0605469 -15.1953 6.51953 -15.8926 14.7051 +c-6.90625 77.0107 -68.1172 138.91 -144.924 145.224c-8.25781 0.592773 -14.7959 7.48633 -14.7988 15.8926v32.1143v0.00390625c0 8.9043 7.22949 16.1338 16.1338 16.1338c0.396484 0 0.775391 -0.0136719 1.16504 -0.0419922 +c110.123 -8.50098 198.229 -96.6074 206.636 -206.732z" /> +c0 54.4004 41.5996 96 96 96h326.4c16 0 25.5996 -9.59961 25.5996 -25.5996v-332.801zM144 280v-48c0 -4.41504 3.58496 -8 8 -8h56v-56c0 -4.41504 3.58496 -8 8 -8h48c4.41504 0 8 3.58496 8 8v56h56c4.41504 0 8 3.58496 8 8v48c0 4.41504 -3.58496 8 -8 8h-56v56 +c0 4.41504 -3.58496 8 -8 8h-48c-4.41504 0 -8 -3.58496 -8 -8v-56h-56c-4.41504 0 -8 -3.58496 -8 -8zM380.8 0v64h-284.8c-16 0 -32 -12.7998 -32 -32s12.7998 -32 32 -32h284.8z" /> +d="M0 160h512v-160c0 -17.6611 -14.3389 -32 -32 -32h-448c-17.6611 0 -32 14.3389 -32 32v160zM299.83 416c118.17 -6.2002 212.17 -104.11 212.17 -224h-512l278.7 217c5.47656 4.38477 12.4277 7.02051 19.9814 7.02051 +c0.384766 0 0.767578 -0.00683594 1.14844 -0.0205078z" /> +d="M288 333l218.74 -192.9c1.54004 -1.37988 3.55957 -2.04004 5.25977 -3.19922v-184.9c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v184.94c1.78027 1.20996 3.84961 1.88965 5.46973 3.34961zM384 72v48c0 4.41504 -3.58496 8 -8 8h-56v56 +c0 4.41504 -3.58496 8 -8 8h-48c-4.41504 0 -8 -3.58496 -8 -8v-56h-56c-4.41504 0 -8 -3.58496 -8 -8v-48c0 -4.41504 3.58496 -8 8 -8h56v-56c0 -4.41504 3.58496 -8 8 -8h48c4.41504 0 8 3.58496 8 8v56h56c4.41504 0 8 3.58496 8 8zM570.69 211.72 +c3.2627 -2.92969 5.30762 -7.18555 5.30762 -11.9121c0 -4.10156 -1.54688 -7.84473 -4.08789 -10.6777l-21.4004 -23.8203c-2.92969 -3.2627 -7.18457 -5.30762 -11.9111 -5.30762c-4.10742 0 -7.85449 1.55078 -10.6885 4.09766l-229.32 202.271 +c-2.82031 2.48828 -6.53906 3.99902 -10.5928 3.99902c-4.05273 0 -7.75684 -1.51074 -10.5771 -3.99902l-229.32 -202.28c-2.83398 -2.54688 -6.58594 -4.10645 -10.6924 -4.10645c-4.72656 0 -8.97754 2.05371 -11.9072 5.31641l-21.4102 23.8203 +c-2.54688 2.83398 -4.10645 6.58594 -4.10645 10.6934c0 4.72559 2.05371 8.97656 5.31641 11.9062l256 226c7.06934 6.3916 16.4707 10.2852 26.7412 10.2852c10.2715 0 19.6396 -3.89355 26.709 -10.2852z" /> +d="M256 416c141.39 0 256 -93.1201 256 -208s-114.61 -208 -256 -208c-0.161133 0 -0.446289 0.107422 -0.606445 0.107422c-37.5674 0 -73.5547 6.81445 -106.794 19.2725c-24.5996 -19.6299 -74.3398 -51.3799 -140.6 -51.3799 +c-4.41113 0.00488281 -7.99023 3.58984 -7.99023 8.00195c0 2.12891 0.833008 4.06445 2.19043 5.49805c0.5 0.5 42.2598 45.4502 54.7998 95.7598c-35.5898 35.7402 -57 81.1807 -57 130.74c0 114.88 114.62 208 256 208zM352 184v48c0 4.41504 -3.58496 8 -8 8h-56v56 +c0 4.41504 -3.58496 8 -8 8h-48c-4.41504 0 -8 -3.58496 -8 -8v-56h-56c-4.41504 0 -8 -3.58496 -8 -8v-48c0 -4.41504 3.58496 -8 8 -8h56v-56c0 -4.41504 3.58496 -8 8 -8h48c4.41504 0 8 3.58496 8 8v56h56c4.41504 0 8 3.58496 8 8z" /> +d="M507.31 262.29c2.87109 -2.89258 4.64551 -6.87891 4.64551 -11.2725c0 -4.42285 -1.79883 -8.42969 -4.70508 -11.3271l-22.6201 -22.6309c-2.89648 -2.89648 -6.90137 -4.68945 -11.3174 -4.68945s-8.41602 1.79297 -11.3125 4.68945l-181 181 +c-2.89648 2.89648 -4.68945 6.90137 -4.68945 11.3174s1.79297 8.41699 4.68945 11.3135l22.6904 22.5996c2.89551 2.89355 6.89844 4.68457 11.3115 4.68457c4.41406 0 8.41211 -1.79102 11.3076 -4.68457zM327.77 195.88l55.1006 55.1201l45.25 -45.2695l-109.68 -109.681 +c-12.4922 -12.4961 -28.4805 -21.5479 -46.29 -25.6494l-120.25 -27.75l-102 -102c-2.89648 -2.89746 -6.90137 -4.69043 -11.3174 -4.69043s-8.41699 1.79297 -11.3135 4.69043l-22.6191 22.6191c-2.89746 2.89648 -4.69043 6.90137 -4.69043 11.3174 +s1.79297 8.41699 4.69043 11.3135l102 102l27.7393 120.26c4.11816 17.8066 13.1738 33.7939 25.6699 46.29l109.671 109.67l45.25 -45.25l-55.1006 -55.1006zM273.2 141.31l9.30957 9.31055l-67.8896 67.8896l-9.31055 -9.30957 +c-4.16113 -4.17676 -7.17969 -9.51074 -8.55957 -15.4502l-18.2998 -79.2998l79.2998 18.3193c5.94238 1.36328 11.2783 4.37695 15.4502 8.54004z" /> +c20.6602 -1.62012 40.9404 5.59961 54.2002 19.3096l46.0898 47.7207c33.4297 34.5098 98.4199 21.1494 110 -22.6201l16 -60.4502c4.60059 -17.3906 18.8604 -31.71 38.1406 -38.3105zM160 192c17.6611 0 32 14.3389 32 32s-14.3389 32 -32 32s-32 -14.3389 -32 -32 +s14.3389 -32 32 -32zM288 96c17.6611 0 32 14.3389 32 32s-14.3389 32 -32 32s-32 -14.3389 -32 -32s14.3389 -32 32 -32zM304 224c8.83105 0 16 7.16895 16 16s-7.16895 16 -16 16s-16 -7.16895 -16 -16s7.16895 -16 16 -16z" /> +d="M464 192c26.4922 0 48 -21.5078 48 -48s-21.5078 -48 -48 -48h-416c-26.4922 0 -48 21.5078 -48 48s21.5078 48 48 48h416zM480 64c8.83105 0 16 -7.16895 16 -16v-16c0 -35.3223 -28.6777 -64 -64 -64h-352c-35.3223 0 -64 28.6777 -64 64v16 +c0 8.83105 7.16895 16 16 16h448zM58.6396 224c-34.5693 0 -54.6396 43.9102 -34.8193 75.8896c40.1797 64.9102 128.64 116.011 232.18 116.11c103.55 -0.0996094 192 -51.2002 232.18 -116.12c19.8008 -31.9795 -0.25 -75.8799 -34.8193 -75.8799h-394.721zM384 336 +c-8.83105 0 -16 -7.16895 -16 -16s7.16895 -16 16 -16s16 7.16895 16 16s-7.16895 16 -16 16zM256 368c-8.83105 0 -16 -7.16895 -16 -16s7.16895 -16 16 -16s16 7.16895 16 16s-7.16895 16 -16 16zM128 336c-8.83105 0 -16 -7.16895 -16 -16s7.16895 -16 16 -16 +s16 7.16895 16 16s-7.16895 16 -16 16z" /> +d="M479.93 130.88l0.0703125 -82.8799c0 -61.7979 -50.1592 -111.973 -111.95 -112h-215c-30.9053 0.00292969 -58.9189 12.5361 -79.1895 32.8096l-30.9307 30.9307c-6.75488 6.75391 -10.9297 16.0928 -10.9297 26.3896v73.4697 +c0 14.6221 8.38574 27.2734 20.6396 33.4004l27.3604 15v-76c0 -4.41504 3.58496 -8 8 -8s8 3.58496 8 8v147.04c0 15.2598 12.8701 28.3799 30.8701 31.3799l30.6797 5.12012c17.8203 2.96973 34.4502 -8.38965 34.4502 -23.54v-32c0 -4.41504 3.58496 -8 8 -8 +s8 3.58496 8 8v200c0 0.0078125 -0.0244141 0.015625 -0.0244141 0.0234375c0 26.4912 21.5078 48 48 48c0.50293 0 1.00488 -0.0078125 1.50488 -0.0234375c26.2695 -0.799805 46.5195 -23.7197 46.5195 -50v-198c0 -4.41504 3.58496 -8 8 -8s8 3.58496 8 8v32 +c0 15.1396 16.6299 26.5 34.4502 23.5303l38.3994 -6.40039c13.46 -2.25 23.1504 -12.0996 23.1504 -23.54v-49.5898l35.6504 -8.92969c16.2188 -4.05371 28.2676 -18.7256 28.2793 -36.1904z" /> +d="M480 160v-64h-448v64c0 80.25 49.2803 148.92 119.19 177.62l40.8096 -81.6201v112c0 8.83105 7.16895 16 16 16h96c8.83105 0 16 -7.16895 16 -16v-112l40.8096 81.6201c69.9102 -28.7002 119.19 -97.3701 119.19 -177.62zM496 64c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-480c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h480z" /> +d="M480 128c-52.9834 0 -96 43.0166 -96 96s43.0166 96 96 96s96 -43.0166 96 -96s-43.0166 -96 -96 -96zM528 96c61.8145 0 112.002 -50.1738 112.002 -111.988c0 -0.210938 -0.000976562 -0.420898 -0.00195312 -0.631836 +c-0.139648 -26.2598 -21.7305 -47.3799 -48 -47.3799h-224c-26.2695 0 -47.8604 21.1201 -48 47.3799c-0.000976562 0.210938 0 0.40918 0 0.620117c0 61.8145 50.1855 112 112 112c0.0273438 0 0.0556641 -0.00488281 0.0830078 -0.00488281 +c2.42871 0 4.77051 -0.380859 6.9668 -1.08496c12.8193 -4.4541 26.6504 -6.87402 40.9775 -6.87402s28.0938 2.41992 40.9131 6.87402c2.19922 0.704102 4.54395 1.08984 6.97656 1.08984h0.0830078zM329.91 85.5498c-25.9033 -25.8965 -41.915 -61.665 -41.915 -101.15 +c0 -0.396484 0.00195312 -0.792969 0.00488281 -1.18945c0.166016 -17.7246 6.24512 -34.1309 16.3096 -47.21h-288.31c-8.83105 0 -16 7.16895 -16 16v368c0 17.6611 14.3389 32 32 32h32v64c0 17.6611 14.3389 32 32 32h160c17.6611 0 32 -14.3389 32 -32v-64h32 +c17.6611 0 32 -14.3389 32 -32v-216.62c-7.98633 -5.24609 -15.3037 -11.1562 -22.0898 -17.8301zM144 44v40c0 6.62305 -5.37695 12 -12 12h-40c-6.62305 0 -12 -5.37695 -12 -12v-40c0 -6.62305 5.37695 -12 12 -12h40c6.62305 0 12 5.37695 12 12zM144 172v40 +c0 6.62305 -5.37695 12 -12 12h-40c-6.62305 0 -12 -5.37695 -12 -12v-40c0 -6.62305 5.37695 -12 12 -12h40c6.62305 0 12 5.37695 12 12zM192 294v26h26c3.31152 0 6 2.68848 6 6v20c0 3.31152 -2.68848 6 -6 6h-26v26c0 3.31152 -2.68848 6 -6 6h-20 +c-3.31152 0 -6 -2.68848 -6 -6v-26h-26c-3.31152 0 -6 -2.68848 -6 -6v-20c0 -3.31152 2.68848 -6 6 -6h26v-26c0 -3.31152 2.68848 -6 6 -6h20c3.31152 0 6 2.68848 6 6zM272 44v40c0 6.62305 -5.37695 12 -12 12h-40c-6.62305 0 -12 -5.37695 -12 -12v-40 +c0 -6.62305 5.37695 -12 12 -12h40c6.62305 0 12 5.37695 12 12zM272 172v40c0 6.62305 -5.37695 12 -12 12h-40c-6.62305 0 -12 -5.37695 -12 -12v-40c0 -6.62305 5.37695 -12 12 -12h40c6.62305 0 12 5.37695 12 12z" /> +d="M368 288c26.4922 0 48 -21.5078 48 -48s-21.5078 -48 -48 -48h-288c-26.4922 0 -48 21.5078 -48 48s21.5078 48 48 48h0.94043c-0.625 5.43945 -0.93457 10.9707 -0.93457 16.5762c0 79.4756 64.5234 144 144 144c79.4756 0 144 -64.5244 144 -144 +c0 -5.60547 -0.321289 -11.1367 -0.946289 -16.5762h0.94043zM195.38 -45.6904l-99.3799 205.69h256l-99.3799 -205.69c-4.99414 -10.8223 -15.9111 -18.3398 -28.6035 -18.3398s-23.6426 7.51758 -28.6367 18.3398z" /> +d="M232 224c-4.41504 0 -8 3.58496 -8 8v48c0 4.41504 3.58496 8 8 8h56v56c0 4.41504 3.58496 8 8 8h48c4.41504 0 8 -3.58496 8 -8v-56h56c4.41504 0 8 -3.58496 8 -8v-48c0 -4.41504 -3.58496 -8 -8 -8h-56v-56c0 -4.41504 -3.58496 -8 -8 -8h-48 +c-4.41504 0 -8 3.58496 -8 8v56h-56zM576 400v-336h-512v336c0.0771484 26.4561 21.5439 47.9229 48 48h416c26.4561 -0.0771484 47.9229 -21.5439 48 -48zM512 128v256h-384v-256h384zM624 32c8.83105 0 16 -7.16895 16 -16v-16 +c-0.104492 -35.2744 -28.7256 -63.8955 -64 -64h-512c-35.2744 0.104492 -63.8955 28.7256 -64 64v16c0 8.83105 7.16895 16 16 16h239.23c-0.230469 -14.5303 14.0791 -32 32.7695 -32h60.7998c18.0303 0 32 12.1904 32.7402 32h242.46z" /> +d="M448 384c35.3223 0 64 -28.6777 64 -64v-256c0 -35.3223 -28.6777 -64 -64 -64h-384c-35.3223 0 -64 28.6777 -64 64v256c0 35.3223 28.6777 64 64 64h384zM160 80v48h-80c-8.83105 0 -16 -7.16895 -16 -16v-16c0 -8.83105 7.16895 -16 16 -16h80zM288 96v16 +c0 8.83105 -7.16895 16 -16 16h-80v-48h80c8.83105 0 16 7.16895 16 16zM448 224v64c0 17.6611 -14.3389 32 -32 32h-320c-17.6611 0 -32 -14.3389 -32 -32v-64c0 -17.6611 14.3389 -32 32 -32h320c17.6611 0 32 14.3389 32 32z" /> +d="M330.67 184.88h107.46l37.0498 -38.54c-48.5293 -87.4697 -206.54 -210.34 -419.18 -210.34c-30.9072 0 -56 25.0928 -56 56s25.0928 56 56 56c141.58 0 163.44 181.24 221.92 250.82l52.75 -24.2207v-89.7197zM461.76 313.25 +c30.8984 -28.1729 50.2402 -68.7275 50.2402 -113.795v-0.145508c0 -13.6797 -2.2998 -26.6895 -5.55957 -39.3096l-54.6807 56.8799h-89.0898v78.2402l-74.6699 34.29c22.3398 14.0498 48.3398 22.5898 76.3398 22.5898 +c20.2783 -0.0078125 39.6836 -4.32031 57.1602 -11.96c18.4502 37.2197 8.25977 61.96 1.40039 72.3203c-0.896484 1.29883 -1.42676 2.88184 -1.42676 4.57715c0 2.20117 0.884766 4.19727 2.31641 5.65234l22.9004 23c1.45117 1.47559 3.46777 2.39453 5.69922 2.39453 +c2.5166 0 4.76367 -1.16504 6.23047 -2.98438c18.5596 -23.4805 35.2998 -71.9102 3.13965 -131.75z" /> +M100.4 335.85c176.069 -1.95996 294.88 -119.25 299.149 -294.14l-379 -105.1c-1.37793 -0.381836 -2.82324 -0.59375 -4.32227 -0.59375c-8.94629 0 -16.21 7.26367 -16.21 16.21c0 1.42871 0.18457 2.81348 0.532227 4.13379zM128 32c17.6611 0 32 14.3389 32 32 +s-14.3389 32 -32 32s-32 -14.3389 -32 -32s14.3389 -32 32 -32zM176 184c17.6611 0 32 14.3389 32 32s-14.3389 32 -32 32s-32 -14.3389 -32 -32s14.3389 -32 32 -32zM280 80c17.6611 0 32 14.3389 32 32s-14.3389 32 -32 32s-32 -14.3389 -32 -32s14.3389 -32 32 -32z" /> +d="M53.2002 -19l-21.2002 339h384l-21.2002 -339c-1.57031 -25.0762 -22.4316 -44.9971 -47.8994 -45h-245.801c-25.4678 0.00292969 -46.3291 19.9238 -47.8994 45zM123.31 156.8c-10.0791 -10.6201 -2.93945 -28.7998 11.3203 -28.7998h57.3701v-112 +c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16v112h57.3701c14.2598 0 21.3994 18.1797 11.3203 28.7998l-89.3809 94.2598c-2.81543 3.04297 -6.83984 4.94922 -11.3086 4.94922s-8.49512 -1.90625 -11.3105 -4.94922zM432 416 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h120l9.40039 18.7002c3.85547 7.88574 11.9434 13.2998 21.3066 13.2998h0.0927734h114.3 +c0.00585938 0 -0.00195312 0.0234375 0.00390625 0.0234375c9.41113 0 17.5645 -5.42871 21.4961 -13.3232l9.40039 -18.7002h120z" /> +d="M32 -16v336h384v-336c0 -26.4922 -21.5078 -48 -48 -48h-288c-26.4922 0 -48 21.5078 -48 48zM123.31 156.8c-10.0791 -10.6201 -2.93945 -28.7998 11.3203 -28.7998h57.3701v-112c0 -8.83105 7.16895 -16 16 -16h32c8.83105 0 16 7.16895 16 16v112h57.3701 +c14.2598 0 21.3994 18.1797 11.3203 28.7998l-89.3809 94.2598c-2.81543 3.04297 -6.83984 4.94922 -11.3086 4.94922s-8.49512 -1.90625 -11.3105 -4.94922zM432 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-416 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h120l9.40039 18.7002c3.85547 7.88574 11.9434 13.2998 21.3066 13.2998h0.0927734h114.3c0.00585938 0 -0.00195312 0.0234375 0.00390625 0.0234375c9.41113 0 17.5645 -5.42871 21.4961 -13.3232 +l9.40039 -18.7002h120z" /> +d="M319.41 128c71.4902 -3.09961 128.59 -61.5996 128.59 -133.79c0 -32.127 -26.083 -58.21 -58.21 -58.21h-331.58c-32.127 0 -58.21 26.083 -58.21 58.21c0 72.1904 57.0996 130.69 128.59 133.79l95.4102 -95.3896zM224 144c-70.6455 0 -128 57.3545 -128 128v110.18 +c0 13.7119 8.62988 25.4092 20.7598 29.96l84.7705 31.79c6.98438 2.61914 14.6035 4.05176 22.498 4.05176s15.457 -1.43262 22.4414 -4.05176l84.7705 -31.75c12.1309 -4.55078 20.7598 -16.248 20.7598 -29.96v-0.0400391v-110.18c0 -70.6455 -57.3545 -128 -128 -128z +M184 376.33v-16.6602c0 -2.75977 2.24023 -5 5 -5h21.6699v-21.6699c0 -2.75977 2.24023 -5 5 -5h16.6602c2.75977 0 5 2.24023 5 5v21.6699h21.6699c2.75977 0 5 2.24023 5 5v16.6602c0 2.75977 -2.24023 5 -5 5h-21.6699v21.6699c0 2.75977 -2.24023 5 -5 5h-16.6602 +c-2.75977 0 -5 -2.24023 -5 -5v-21.6699h-21.6699c-2.75977 0 -5 -2.24023 -5 -5zM144 288v-16c0 -44.1533 35.8467 -80 80 -80s80 35.8467 80 80v16h-160z" /> +d="M476 -32h-152c-19.8691 0 -36 16.1309 -36 36v348h-96v-156c0 -19.8691 -16.1309 -36 -36 -36h-140c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h112v156c0 19.8691 16.1309 36 36 36h152c19.8691 0 36 -16.1309 36 -36v-348h96v156 +c0 19.8691 16.1309 36 36 36h140c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-112v-156c0 -19.8691 -16.1309 -36 -36 -36z" /> +d="M400 352c-26.4922 0 -48 21.5078 -48 48s21.5078 48 48 48s48 -21.5078 48 -48s-21.5078 -48 -48 -48zM396 231l-41.3604 33.1104l-58.25 -49.9199l41.3604 -27.5703c8.60547 -5.7373 14.248 -15.5117 14.25 -26.6201v-128c0 -17.6611 -14.3389 -32 -32 -32 +s-32 14.3389 -32 32v110.88l-81.7305 54.5205c-8.60742 5.7373 -14.2686 15.5068 -14.2686 26.6191c0 9.71777 4.3418 18.4297 11.1895 24.3008l112 96c5.58887 4.80176 12.8965 7.70117 20.8359 7.70117c7.55566 0 14.502 -2.62891 19.9736 -7.02148l71.2197 -57h52.7803 +c17.6611 0 32 -14.3389 32 -32s-14.3389 -32 -32 -32h-64c-0.0205078 0 -0.0625 0.0117188 -0.0830078 0.0117188c-7.53125 0 -14.457 2.61621 -19.917 6.98828zM512 192c70.6455 0 128 -57.3545 128 -128s-57.3545 -128 -128 -128s-128 57.3545 -128 128 +s57.3545 128 128 128zM512 0c35.3223 0 64 28.6777 64 64s-28.6777 64 -64 64s-64 -28.6777 -64 -64s28.6777 -64 64 -64zM128 192c70.6455 0 128 -57.3545 128 -128s-57.3545 -128 -128 -128s-128 57.3545 -128 128s57.3545 128 128 128zM128 0c35.3223 0 64 28.6777 64 64 +s-28.6777 64 -64 64s-64 -28.6777 -64 -64s28.6777 -64 64 -64z" /> +d="M240 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM336 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h32zM432 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM144 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM240 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM336 32c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 128 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 320c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h32zM240 128c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM240 320c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM144 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM240 416c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM336 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 416 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM48 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h32zM48 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM48 128c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM48 320c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM48 416c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM144 416c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32z" /> +d="M240 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM144 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32 +c0 8.83105 7.16895 16 16 16h32zM336 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 224c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32 +c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 128c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 32c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 320c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32zM432 416 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-368v-368c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v400c0 17.6611 14.3389 32 32 32h400z" /> +c-8.34082 22.9707 -12.8604 48.9707 -12.8604 77.0605c0 81.79 61.6299 149.3 141.33 159.3c10.4795 1.30957 19.6699 -7.17969 18.5898 -17.6201l-12.4102 -123.11c22.9707 8.34082 48.9707 12.8604 77.0605 12.8604zM256 160c17.6611 0 32 14.3389 32 32 +s-14.3389 32 -32 32s-32 -14.3389 -32 -32s14.3389 -32 32 -32z" /> +l-96.75 -99.8301c-2.85449 -2.98242 -6.875 -4.83984 -11.3252 -4.83984s-8.46973 1.85742 -11.3242 4.83984zM260.57 128.16c15.1406 -0.0107422 27.4297 -12.3066 27.4297 -27.4502v-0.00976562v-137.25c0 -15.1436 -12.2891 -27.4395 -27.4297 -27.4502h-233.141 +c-15.1396 0.00585938 -27.4297 12.2988 -27.4297 27.4395v0.0107422v137.25v0.00976562c0 15.1504 12.2998 27.4502 27.4502 27.4502h0.00976562h48l7 14.2402c3.89258 10.3887 13.9082 17.7793 25.6484 17.7793h0.0117188h71.71 +c0.00390625 0 -0.00195312 0.0126953 0.000976562 0.0126953c11.7412 0 21.7666 -7.40332 25.6592 -17.792l7.08008 -14.2402h48zM144 -20c28.6992 0 52 23.3008 52 52s-23.3008 52 -52 52s-52 -23.3008 -52 -52s23.3008 -52 52 -52zM499.4 95.9004 +c9.70996 0 15.75 -8.79004 10.8691 -15.7002l-92.3994 -138.91c-2.42188 -3.19824 -6.24805 -5.25488 -10.5654 -5.25488c-0.118164 0 -0.236328 0.00195312 -0.354492 0.00488281c-8.03027 0 -14.1201 6.25 -12.2305 12.9004l24.2002 83h-62.3096 +c-7.62012 0 -13.5 5.58984 -12.5 11.8896l16.7998 106.93c0.839844 5.2002 6.2002 9.10059 12.5 9.10059h75.5898c8.25 0 14.2803 -6.56055 12.1797 -13.21l-22.3594 -50.75h60.5801zM478.08 447.67c17.9199 2.75 33.9199 -12.1895 33.9199 -31.6699v-144.26 +c-0.269531 -26.3398 -28.7998 -47.6602 -64 -47.6602c-35.3496 0 -64 21.4795 -64 48c0 26.5195 28.6504 48 64 48c5.49219 -0.0498047 10.8096 -0.633789 16 -1.7002v47.1797l-112 -17.2197v-108.58c-0.269531 -26.3398 -28.7998 -47.6602 -64 -47.6602 +c-35.3496 0 -64 21.4805 -64 48c0 26.5205 28.6504 48 64 48c5.49219 -0.0498047 10.8096 -0.632812 16 -1.69922v106.77c0 15.9102 10.8701 29.4102 25.5098 31.6602z" /> +d="M497.39 86.2002c8.60059 -3.74121 14.6006 -12.2891 14.6006 -22.2588c0 -1.83496 -0.204102 -3.62305 -0.589844 -5.3418l-24 -104c-2.45801 -10.6416 -12 -18.5996 -23.3848 -18.5996h-0.015625c-256.1 0 -464 207.5 -464 464l0.0136719 0.00390625 +c0 11.3848 7.94434 20.9287 18.5859 23.3857l104 24c1.72754 0.392578 3.49805 0.619141 5.34375 0.619141c9.9082 0 18.4307 -5.97656 22.1562 -14.5186l48 -112c1.23828 -2.88965 1.95117 -6.0791 1.95117 -9.41895c0 -7.49512 -3.45215 -14.1904 -8.85059 -18.5811 +l-60.6006 -49.6006c36.7334 -77.9072 99.2822 -140.457 177.19 -177.189l49.5996 60.5996c4.40332 5.39258 11.1113 8.81055 18.6084 8.81055c3.33203 0 6.50684 -0.680664 9.3916 -1.91016z" /> +d="M400 416c26.4922 0 48 -21.5078 48 -48v-352c0 -26.4922 -21.5078 -48 -48 -48h-352c-26.4922 0 -48 21.5078 -48 48v352c0 26.4922 21.5078 48 48 48h352zM383.61 108.63c0.235352 1.09082 0.369141 2.21387 0.389648 3.37012 +c-0.301758 6.06445 -3.91992 11.2607 -9.08984 13.79l-70 30c-1.83594 0.71582 -3.83789 1.14355 -5.91016 1.20996c-4.58496 -0.251953 -8.69922 -2.31836 -11.6104 -5.5l-31 -37.8896c-48.7002 22.9775 -87.8018 62.0791 -110.779 110.779l37.8896 31 +c3.18164 2.91113 5.24805 7.02539 5.5 11.6104c-0.0673828 2.07129 -0.495117 4.07324 -1.20996 5.91016l-30 70c-2.53223 5.16797 -7.72754 8.78418 -13.79 9.08984c-1.15527 -0.0253906 -2.27734 -0.15918 -3.37012 -0.389648l-65 -15 +c-6.52246 -1.74707 -11.3818 -7.59961 -11.6299 -14.6104c0 -160.29 130 -290 290 -290c7.11426 0.00292969 13.0762 4.97852 14.6104 11.6299z" /> +d="M608 448c17.6611 0 32 -14.3389 32 -32v-320c0 -17.6611 -14.3389 -32 -32 -32h-128v320h-192v-64h-160v96c0 17.6611 14.3389 32 32 32h448zM232 345v30c0 4.9668 -4.0332 9 -9 9h-30c-4.9668 0 -9 -4.0332 -9 -9v-30c0 -4.9668 4.0332 -9 9 -9h30 +c4.9668 0 9 4.0332 9 9zM584 137v30c0 4.9668 -4.0332 9 -9 9h-30c-4.9668 0 -9 -4.0332 -9 -9v-30c0 -4.9668 4.0332 -9 9 -9h30c4.9668 0 9 4.0332 9 9zM584 241v30c0 4.9668 -4.0332 9 -9 9h-30c-4.9668 0 -9 -4.0332 -9 -9v-30c0 -4.9668 4.0332 -9 9 -9h30 +c4.9668 0 9 4.0332 9 9zM584 345v30c0 4.9668 -4.0332 9 -9 9h-30c-4.9668 0 -9 -4.0332 -9 -9v-30c0 -4.9668 4.0332 -9 9 -9h30c4.9668 0 9 4.0332 9 9zM416 288c17.6611 0 32 -14.3389 32 -32v-288c0 -17.6611 -14.3389 -32 -32 -32h-384c-17.6611 0 -32 14.3389 -32 32 +v288c0 17.6611 14.3389 32 32 32h384zM96 224c-17.6611 0 -32 -14.3389 -32 -32s14.3389 -32 32 -32s32 14.3389 32 32s-14.3389 32 -32 32zM384 0v96l-96 96l-128 -128l-32 32l-64 -64v-32h320z" /> +d="M336 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h32.4902l26.5098 79.5996l67.0898 -51.8301l-9.25977 -27.7695h11.1699zM633.82 -10.0996 +c3.76855 -2.92871 6.17676 -7.50977 6.17676 -12.6475c0 -3.69238 -1.25293 -7.09375 -3.35742 -9.80273l-19.6396 -25.2705c-2.92871 -3.76855 -7.50879 -6.17578 -12.6465 -6.17578c-3.69727 0 -7.10254 1.25684 -9.81348 3.36621l-588.36 454.72 +c-3.76562 2.92871 -6.1709 7.50781 -6.1709 12.6426c0 3.69434 1.25488 7.09766 3.36133 9.80762l19.6299 25.2695c2.92871 3.76855 7.50879 6.17676 12.6465 6.17676c3.69727 0 7.10254 -1.25684 9.81348 -3.36621l114.54 -88.5205v43.9004c0 8.83105 7.16895 16 16 16h416 +c8.83105 0 16 -7.16895 16 -16v-96c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v32h-117.83l-49.1699 -147.59zM309.91 240.24l31.9199 95.7598h-117.83v-29.3604z" /> +d="M176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555l-80 96c-10.0703 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32 +c8.83105 0 16 -7.16895 16 -16v-304h48zM288 224c-8.83105 0 -16 7.16895 -16 16v17.6299c0 9.51074 4.14355 18.0566 10.7402 23.9199l61.2598 70.4502h-56c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128c8.83105 0 16 -7.16895 16 -16v-17.6299 +c0 -9.51074 -4.14355 -18.0566 -10.7402 -23.9199l-61.2598 -70.4502h56c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128zM447.06 -10.6201c0.600586 -1.67969 0.931641 -3.49512 0.931641 -5.37988c0 -8.82812 -7.16406 -15.9951 -15.9912 -16 +h-24.8398c-0.015625 0 -0.0263672 -0.00195312 -0.0419922 -0.00195312c-7.11426 0 -13.1514 4.6543 -15.2285 11.082l-4.40918 12.9199h-71l-4.4209 -12.9199c-2.07617 -6.42773 -8.10938 -11.0801 -15.2246 -11.0801h-0.00488281h-24.8301 +c-8.82715 0.00488281 -15.9863 7.17773 -15.9863 16.0049c0 1.88574 0.326172 3.69531 0.926758 5.375l59.2695 160c2.20996 6.19043 8.125 10.6201 15.0703 10.6201h41.4395c6.94531 0 12.8604 -4.42969 15.0703 -10.6201zM335.61 48h32.7793l-16.3896 48z" /> +d="M16 288c-14.2197 0 -21.3496 17.2598 -11.3096 27.3096l80 96c2.89551 2.89453 6.89844 4.68555 11.3115 4.68555c4.41406 0 8.41211 -1.79102 11.3076 -4.68555l80 -96c10.0703 -10.0693 2.90039 -27.3096 -11.3096 -27.3096h-48v-304c0 -8.83105 -7.16895 -16 -16 -16 +h-32c-8.83105 0 -16 7.16895 -16 16v304h-48zM288 224c-8.83105 0 -16 7.16895 -16 16v17.6299c0 9.51074 4.14355 18.0566 10.7402 23.9199l61.2598 70.4502h-56c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128c8.83105 0 16 -7.16895 16 -16v-17.6299 +c0 -9.51074 -4.14355 -18.0566 -10.7402 -23.9199l-61.2598 -70.4502h56c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-128zM447.06 -10.6201c0.600586 -1.67969 0.931641 -3.49512 0.931641 -5.37988c0 -8.82812 -7.16406 -15.9951 -15.9912 -16 +h-24.8398c-0.015625 0 -0.0263672 -0.00195312 -0.0419922 -0.00195312c-7.11426 0 -13.1514 4.6543 -15.2285 11.082l-4.40918 12.9199h-71l-4.4209 -12.9199c-2.07617 -6.42773 -8.10938 -11.0801 -15.2246 -11.0801h-0.00488281h-24.8301 +c-8.82715 0.00488281 -15.9863 7.17773 -15.9863 16.0049c0 1.88574 0.326172 3.69531 0.926758 5.375l59.2695 160c2.20996 6.19043 8.125 10.6201 15.0703 10.6201h41.4395c6.94531 0 12.8604 -4.42969 15.0703 -10.6201zM335.61 48h32.7793l-16.3896 48z" /> +d="M240 352c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-64zM240 224c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-128zM496 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-256c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h256zM240 96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h192 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-192zM176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555l-80 96 +c-10.0801 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32c8.83105 0 16 -7.16895 16 -16v-304h48z" /> +d="M240 352c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h64c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-64zM240 224c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h128c8.83105 0 16 -7.16895 16 -16v-32 +c0 -8.83105 -7.16895 -16 -16 -16h-128zM496 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-256c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h256zM240 96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h192 +c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-192zM16 288c-14.2197 0 -21.3496 17.2598 -11.3096 27.3096l80 96c2.89551 2.89453 6.89844 4.68555 11.3115 4.68555c4.41406 0 8.41211 -1.79102 11.3076 -4.68555l80 -96 +c10.0801 -10.0693 2.90039 -27.3096 -11.3096 -27.3096h-48v-304c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v304h-48z" /> +d="M176 96c14.2197 0 21.3496 -17.2598 11.3301 -27.3096l-80 -96c-2.89551 -2.89453 -6.89844 -4.68555 -11.3125 -4.68555c-4.41309 0 -8.41211 1.79102 -11.3076 4.68555l-80 96c-10.0703 10.0693 -2.90039 27.3096 11.29 27.3096h48v304c0 8.83105 7.16895 16 16 16h32 +c8.83105 0 16 -7.16895 16 -16v-304h48zM400 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v64h-16c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156 +c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-112h16zM330.17 413.09c53.4502 14.25 101.83 -25.8799 101.85 -77.0898v-10.7695c0 -70.3906 -28.25 -107.23 -86.25 -132 +c-8.36914 -3.58008 -18.0293 1.2793 -20.8994 9.90918l-9.90039 20c-2.62012 7.87012 0.610352 16.9404 8.18066 20.3408c7.59961 3.28516 14.6064 7.64258 20.8496 12.9092c-47.6396 4.76074 -83.0996 51.4805 -68.8301 102.53c7.62891 26.2793 28.5596 46.9287 55 54.1699 +zM352 316c11.0381 0 20 8.96191 20 20s-8.96191 20 -20 20s-20 -8.96191 -20 -20s8.96191 -20 20 -20z" /> +d="M107.31 411.31l80 -96c10.0703 -10.0693 2.90039 -27.3096 -11.3096 -27.3096h-48v-304c0 -8.83105 -7.16895 -16 -16 -16h-32c-8.83105 0 -16 7.16895 -16 16v304h-48c-14.2197 0 -21.3496 17.2598 -11.3096 27.3096l80 96 +c2.89551 2.89453 6.89844 4.68555 11.3115 4.68555c4.41406 0 8.41211 -1.79102 11.3076 -4.68555zM400 32c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-96c-8.83105 0 -16 7.16895 -16 16v32c0 8.83105 7.16895 16 16 16h16v64h-16 +c-8.82422 0.0078125 -15.9775 7.18945 -15.9775 16.0156c0 2.57129 0.608398 5.00098 1.6875 7.1543l16 32c2.62598 5.23926 8.03613 8.8252 14.29 8.83008h48c8.83105 0 16 -7.16895 16 -16v-112h16zM330.17 413.09c53.4502 14.25 101.83 -25.8799 101.85 -77.0898 +v-10.7695c0 -70.3906 -28.25 -107.23 -86.25 -132c-8.36914 -3.58008 -18.0293 1.2793 -20.8994 9.90918l-9.90039 20c-2.62012 7.87012 0.610352 16.9404 8.18066 20.3408c7.59961 3.28516 14.6064 7.64258 20.8496 12.9092 +c-47.6396 4.76074 -83.0996 51.4805 -68.8301 102.53c7.62891 26.2793 28.5596 46.9287 55 54.1699zM352 316c11.0381 0 20 8.96191 20 20s-8.96191 20 -20 20s-20 -8.96191 -20 -20s8.96191 -20 20 -20z" /> +d="M272 192c-8.83105 0 -16 7.16895 -16 16v224c0 8.83105 7.16895 16 16 16h75c42.2998 0 80.9004 -30.5703 84.6699 -72.6797c0.225586 -2.44238 0.289062 -4.91895 0.289062 -7.41895c0 -13.5479 -3.38281 -26.3115 -9.34863 -37.4912 +c15.6377 -14.5762 25.3984 -35.2832 25.3984 -58.3262c0 -1.59277 -0.046875 -3.1748 -0.138672 -4.74414c-2.50977 -43.1396 -41.3105 -75.3398 -84.5098 -75.3398h-91.3604zM312 392v-48h40c13.2461 0 24 10.7539 24 24s-10.7539 24 -24 24h-40zM312 296v-48h56 +c13.2461 0 24 10.7539 24 24s-10.7539 24 -24 24h-56zM155.12 425.75l68.2998 -213.48c0.376953 -1.36035 0.580078 -2.79004 0.580078 -4.26953c0 -8.83105 -7.16895 -16 -16 -16h-24.9297c-7.35059 0 -13.5488 4.97168 -15.4199 11.7305l-11.9404 36.2695h-87.4199 +l-11.9404 -36.2695c-1.87109 -6.75879 -8.06934 -11.7305 -15.4199 -11.7305h-24.9297c-8.82617 0.00488281 -15.9883 7.16895 -15.9883 15.9961c0 1.47949 0.201172 2.91309 0.578125 4.27344l68.29 213.48c4.12695 12.9004 16.2168 22.25 30.4805 22.25h25.2793 +c14.2637 0 26.3535 -9.34961 30.4805 -22.25zM89.3701 304h45.2598l-22.6299 68.7002zM571.37 171.52c2.8916 -2.89453 4.65918 -6.89648 4.65918 -11.3066c0 -4.40137 -1.78027 -8.38867 -4.65918 -11.2832l-208 -208.21 +c-2.88086 -2.91406 -6.88379 -4.7207 -11.3018 -4.7207s-8.41699 1.80664 -11.2988 4.7207l-112 112.21c-2.88477 2.89453 -4.66895 6.8916 -4.66895 11.2979c0 4.40527 1.78418 8.39746 4.66895 11.292l45.3008 45.3008c2.87891 2.91309 6.87988 4.71973 11.2969 4.71973 +c4.41602 0 8.41309 -1.80664 11.293 -4.71973l55.4102 -55.5l151.5 151.5c2.87891 2.91309 6.87988 4.71973 11.2969 4.71973c4.41602 0 8.41309 -1.80664 11.293 -4.71973z" /> +d="M496 320c79.4756 0 144 -64.5244 144 -144s-64.5244 -144 -144 -144h-352c-79.4727 0.00390625 -144.079 64.3818 -144.079 143.854c0 79.4766 64.5244 144 144 144c79.4766 0 144 -64.5234 144 -144c0 -29.5293 -8.90723 -56.9961 -24.1807 -79.8545h112.52 +c-15.2734 22.8584 -24.2598 50.4697 -24.2598 80c0 79.4756 64.5244 144 144 144zM64 176c0 -44.1533 35.8467 -80 80 -80s80 35.8467 80 80s-35.8467 80 -80 80s-80 -35.8467 -80 -80zM496 96c44.1533 0 80 35.8467 80 80s-35.8467 80 -80 80s-80 -35.8467 -80 -80 +s35.8467 -80 80 -80z" /> +d="M490 151.1c-38.7695 -12.5898 -93.7305 -23.0996 -170 -23.0996s-131.19 10.5303 -169.99 23.1201c9.50977 57.4102 39.5098 232.88 97.71 232.88c14 0 26.4902 -6 37 -14c9.78516 -7.45996 22.0947 -11.8906 35.3369 -11.8906c13.2432 0 25.458 4.43066 35.2432 11.8906 +c10.5098 8.07031 23 14 37 14c58.21 0 88.21 -175.51 97.7002 -232.9zM632.9 188.28c4.27637 -2.87402 7.08008 -7.75195 7.08008 -13.2871c0 -1.94043 -0.34668 -3.80078 -0.980469 -5.52344c-0.730469 -2.01953 -77.3203 -201.47 -319 -201.47s-318.27 199.45 -319 201.47 +c-0.625977 1.71289 -0.966797 3.56543 -0.966797 5.49316c0 8.83105 7.16992 16 16 16c4.12012 0 7.87891 -1.56055 10.7168 -4.12305c1.01953 -0.899414 102.42 -90.8398 293.24 -90.8398c191.89 0 292.16 89.8799 293.16 90.7803 +c2.84863 2.61816 6.6709 4.20996 10.8428 4.20996c3.2959 0 6.36035 -0.999023 8.90723 -2.70996z" /> +c34.3994 0 67.7695 -12.1201 96.3994 -35.0596zM495.45 175.23c114.95 -7.90039 144.55 -101.841 144.55 -127.23c0 -26.4922 -21.5078 -48 -48 -48c-97.0996 0 -141.24 35.46 -212.31 96.7002l-98 84.4795c-35.29 28.2705 -75.5 42.8203 -117.29 42.8203 +c-7.09082 0 -13.8906 -1.16992 -20.79 -2l6.88965 65.21c2.96094 27.6465 23.6035 50.1143 50.3496 55.79l191.15 40.5898c4.31055 0.916992 8.73828 1.34277 13.3203 1.34277c31.6191 0 57.9131 -22.9785 63.0801 -53.1328z" /> +d="M0 96v128h384v-128c0 -88.3066 -71.6934 -160 -160 -160h-64c-88.3066 0 -160 71.6934 -160 160zM176 448v-192h-176v32c0 88.3066 71.6934 160 160 160h16zM224 448c88.3066 0 160 -71.6934 160 -160v-32h-176v192h16z" /> +d="M256 296c57.3994 0 104 -46.6006 104 -104s-46.6006 -104 -104 -104s-104 46.6006 -104 104s46.6006 104 104 104zM256 168c13.2461 0 24 10.7539 24 24s-10.7539 24 -24 24s-24 -10.7539 -24 -24s10.7539 -24 24 -24zM256 440c137 0 248 -111 248 -248 +s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 64c70.6455 0 128 57.3545 128 128s-57.3545 128 -128 128s-128 -57.3545 -128 -128s57.3545 -128 128 -128z" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - +d="M416 240c8.83105 0 16 -7.16895 16 -16s-7.16895 -16 -16 -16s-16 7.16895 -16 16s7.16895 16 16 16zM624 128c8.83105 0 16 -7.16895 16 -16v-32c0 -8.83105 -7.16895 -16 -16 -16h-336c0 -52.9834 -43.0166 -96 -96 -96s-96 43.0166 -96 96h-32 +c-35.3223 0 -64 28.6777 -64 64v256c0 35.3223 28.6777 64 64 64h352c88.3066 0 160 -71.6934 160 -160v-160h48zM192 16c26.4795 0.0273438 47.9727 21.5205 48 48c0 26.4922 -21.5078 48 -48 48s-48 -21.5078 -48 -48s21.5078 -48 48 -48zM256 256v64 +c0 17.6611 -14.3389 32 -32 32h-128c-17.6611 0 -32 -14.3389 -32 -32v-64c0 -17.6611 14.3389 -32 32 -32h128c17.6611 0 32 14.3389 32 32zM448 128v192c0 17.6611 -14.3389 32 -32 32h-64c-17.6611 0 -32 -14.3389 -32 -32v-192h128z" /> diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 00000000..90fbe201 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,92 @@ +{{template "base" .}} + +{{define "title"}}{{.Title}}{{end}} + +{{define "page_body"}} + +

{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}

+{{if .Error}} +
+
{{.Error}}
+
+{{end}} +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + {{if not .IsAdd}} + + If empty the current password will not be changed + + {{end}} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" + +
+
+ +
+ +
+ + + Free form text field + +
+
+ + +
+ +{{end}} \ No newline at end of file diff --git a/templates/admins.html b/templates/admins.html new file mode 100644 index 00000000..37b3124a --- /dev/null +++ b/templates/admins.html @@ -0,0 +1,185 @@ +{{template "base" .}} + +{{define "title"}}{{.Title}}{{end}} + +{{define "extra_css"}} + + + +{{end}} + +{{define "page_body"}} + + + + + +
+
+
View and manage admins
+
+
+
+ + + + + + + + + + + + {{range .Admins}} + + + + + + + + {{end}} + + +
IDUsernameStatusPermissionsOther
{{.ID}}{{.Username}}{{if eq .Status 1 }}Active{{else}}Inactive{{end}}{{.GetPermissionsAsString}}{{.GetInfoString}}
+
+
+
+ +{{end}} + +{{define "dialog"}} + +{{end}} + +{{define "extra_js"}} + + + + + + + +{{end}} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 9f63a6b9..9826a4f8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,7 +15,8 @@ - + + @@ -38,6 +39,7 @@
+ {{if .LoggedAdmin.Username}} + {{end}}
@@ -94,11 +110,43 @@
+ {{if .LoggedAdmin.Username}} -
@@ -132,6 +181,26 @@ + + + {{block "dialog" .}}{{end}} diff --git a/templates/changepwd.html b/templates/changepwd.html new file mode 100644 index 00000000..7e6447c0 --- /dev/null +++ b/templates/changepwd.html @@ -0,0 +1,38 @@ +{{template "base" .}} + +{{define "title"}}{{.Title}}{{end}} + +{{define "page_body"}} + + +

Change password

+{{if .Error}} +
+
{{.Error}}
+
+{{end}} +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+{{end}} diff --git a/templates/connections.html b/templates/connections.html index 99fdc3a1..2d970ab1 100644 --- a/templates/connections.html +++ b/templates/connections.html @@ -90,9 +90,9 @@ function disconnectAction() { var table = $('#dataTable').DataTable(); - table.button(0).enable(false); + table.button('disconnect:name').enable(false); var connectionID = table.row({ selected: true }).data()[0]; - var path = '{{.APIConnectionsURL}}' + "/" + connectionID; + var path = '{{.ConnectionsURL}}' + "/" + connectionID; $('#disconnectModal').modal('hide'); $.ajax({ url: path, @@ -101,12 +101,12 @@ timeout: 15000, success: function (result) { setTimeout(function () { - table.button(0).enable(true); + table.button('disconnect:name').enable(true); window.location.href = '{{.ConnectionsURL}}'; }, 1000); }, error: function ($xhr, textStatus, errorThrown) { - table.button(0).enable(true); + table.button('disconnect:name').enable(true); var txt = "Unable to close the selected connection"; if ($xhr) { var json = $xhr.responseJSON; @@ -126,6 +126,7 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.disconnect = { text: 'Disconnect', + name: 'disconnect', action: function (e, dt, node, config) { $('#disconnectModal').modal('show'); }, @@ -138,9 +139,7 @@ "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", select: true, - buttons: [ - 'disconnect' - ], + buttons: [], "columnDefs": [ { "targets": [0], @@ -152,10 +151,14 @@ "order": [[1, 'asc']] }); + {{if .LoggedAdmin.HasPermission "close_conns"}} + table.button().add(0,'disconnect'); + table.on('select deselect', function () { var selectedRows = table.rows({ selected: true }).count(); - table.button(0).enable(selectedRows == 1); + table.button('disconnect:name').enable(selectedRows == 1); }); + {{end}} }); {{end}} \ No newline at end of file diff --git a/templates/folders.html b/templates/folders.html index 45ada93a..21b31aa6 100644 --- a/templates/folders.html +++ b/templates/folders.html @@ -87,9 +87,9 @@ function deleteAction() { var table = $('#dataTable').DataTable(); - table.button(1).enable(false); + table.button('delete:name').enable(false); var folderPath = table.row({ selected: true }).data()[0]; - var path = '{{.APIFoldersURL}}' + "?folder_path=" + encodeURIComponent(folderPath); + var path = '{{.FolderURL}}' + "?folder_path=" + encodeURIComponent(folderPath); $('#deleteModal').modal('hide'); $.ajax({ url: path, @@ -97,12 +97,11 @@ function deleteAction() { dataType: 'json', timeout: 15000, success: function (result) { - table.button(1).enable(true); + table.button('delete:name').enable(true); window.location.href = '{{.FoldersURL}}'; }, error: function ($xhr, textStatus, errorThrown) { - console.log("delete error") - table.button(1).enable(true); + table.button('delete:name').enable(true); var txt = "Unable to delete the selected folder"; if ($xhr) { var json = $xhr.responseJSON; @@ -122,6 +121,7 @@ function deleteAction() { $(document).ready(function () { $.fn.dataTable.ext.buttons.add = { text: 'Add', + name: 'add', action: function (e, dt, node, config) { window.location.href = '{{.FolderURL}}'; } @@ -129,6 +129,7 @@ function deleteAction() { $.fn.dataTable.ext.buttons.delete = { text: 'Delete', + name: 'delete', action: function (e, dt, node, config) { $('#deleteModal').modal('show'); }, @@ -137,10 +138,11 @@ function deleteAction() { $.fn.dataTable.ext.buttons.quota_scan = { text: 'Quota scan', + name: 'quota_scan', action: function (e, dt, node, config) { - table.button(2).enable(false); + dt.button('quota_scan:name').enable(false); var folderPath = dt.row({ selected: true }).data()[0]; - var path = '{{.APIFolderQuotaScanURL}}' + var path = '{{.FolderQuotaScanURL}}' $.ajax({ url: path, type: 'POST', @@ -148,7 +150,7 @@ function deleteAction() { data: JSON.stringify({ "mapped_path": folderPath }), timeout: 15000, success: function (result) { - table.button(2).enable(true); + dt.button('quota_scan:name').enable(true); $('#successTxt').text("Quota scan started for the selected folder. Please reload the folders page to check when the scan ends"); $('#successMsg').show(); setTimeout(function () { @@ -156,8 +158,7 @@ function deleteAction() { }, 5000); }, error: function ($xhr, textStatus, errorThrown) { - console.log("quota scan error") - table.button(2).enable(true); + dt.button('quota_scan:name').enable(true); var txt = "Unable to update quota for the selected folder"; if ($xhr) { var json = $xhr.responseJSON; @@ -186,17 +187,31 @@ function deleteAction() { "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", select: true, - buttons: [ - 'add','delete', 'quota_scan' - ], + buttons: [], "scrollX": false, "order": [[0, 'asc']] }); + {{if .LoggedAdmin.HasPermission "quota_scans"}} + table.button().add(0,'quota_scan'); + {{end}} + + {{if .LoggedAdmin.HasPermission "del_users"}} + table.button().add(0,'delete'); + {{end}} + + {{if .LoggedAdmin.HasPermission "add_users"}} + table.button().add(0,'add'); + {{end}} + table.on('select deselect', function () { var selectedRows = table.rows({ selected: true }).count(); - table.button(1).enable(selectedRows == 1); - table.button(2).enable(selectedRows == 1); + {{if .LoggedAdmin.HasPermission "del_users"}} + table.button('delete:name').enable(selectedRows == 1); + {{end}} + {{if .LoggedAdmin.HasPermission "quota_scans"}} + table.button('quota_scan:name').enable(selectedRows == 1); + {{end}} }); }); diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 00000000..9b1f9832 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + SFTPGo - Login + + + + + + + + + + + + + + + +
+ + +
+ +
+ +
+
+ +
+
+
+
+

SFTPGo - {{.Version}}

+
+ {{if .Error}} +
+
{{.Error}}
+
+ {{end}} +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/message.html b/templates/message.html index 42c9df7b..c017c7c8 100644 --- a/templates/message.html +++ b/templates/message.html @@ -3,6 +3,7 @@ {{define "title"}}{{.Title}}{{end}} {{define "page_body"}} +{{if .LoggedAdmin.Username}}

{{.Title}}

{{if .Error}}
@@ -15,5 +16,36 @@
{{.Success}}
{{end}} +{{else}} +
+
+ +
+
+
+
+
+
+

{{.Title}}

+
+ {{if .Error}} +
+
{{.Error}}
+
+ {{end}} + + {{if .Success}} +
+
{{.Success}}
+
+ {{end}} +
+
+
+
+
+
+
+{{end}} {{end}} \ No newline at end of file diff --git a/templates/user.html b/templates/user.html index 38e16e46..47fdeda1 100644 --- a/templates/user.html +++ b/templates/user.html @@ -50,7 +50,7 @@
+ {{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}> {{if not .IsAdd}} If empty the current password will not be changed diff --git a/templates/users.html b/templates/users.html index 78718305..e9dae679 100644 --- a/templates/users.html +++ b/templates/users.html @@ -97,22 +97,21 @@ function deleteAction() { var table = $('#dataTable').DataTable(); - table.button(3).enable(false); - var userID = table.row({ selected: true }).data()[0]; - var path = '{{.APIUserURL}}' + "/" + userID; + table.button('delete:name').enable(false); + var username = table.row({ selected: true }).data()[1]; + var path = '{{.UserURL}}' + "/" + username; $('#deleteModal').modal('hide'); $.ajax({ - url: path, + url: encodeURI(path), type: 'DELETE', dataType: 'json', timeout: 15000, success: function (result) { - table.button(3).enable(true); + table.button('delete:name').enable(true); window.location.href = '{{.UsersURL}}'; }, error: function ($xhr, textStatus, errorThrown) { - console.log("delete error") - table.button(3).enable(true); + table.button('delete:name').enable(true); var txt = "Unable to delete the selected user"; if ($xhr) { var json = $xhr.responseJSON; @@ -132,6 +131,7 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.add = { text: 'Add', + name: 'add', action: function (e, dt, node, config) { window.location.href = '{{.UserURL}}'; } @@ -139,19 +139,21 @@ $.fn.dataTable.ext.buttons.edit = { text: 'Edit', + name: 'edit', action: function (e, dt, node, config) { - var userID = dt.row({ selected: true }).data()[0]; - var path = '{{.UserURL}}' + "/" + userID; - window.location.href = path; + var username = dt.row({ selected: true }).data()[1]; + var path = '{{.UserURL}}' + "/" + username; + window.location.href = encodeURI(path); }, enabled: false }; $.fn.dataTable.ext.buttons.clone = { text: 'Clone', + name: 'clone', action: function (e, dt, node, config) { - var userID = dt.row({ selected: true }).data()[0]; - var path = '{{.UserURL}}' + "?cloneFromId=" + userID; + var username = dt.row({ selected: true }).data()[1]; + var path = '{{.UserURL}}' + "?cloneFrom=" + encodeURIComponent(username); window.location.href = path; }, enabled: false @@ -159,6 +161,7 @@ $.fn.dataTable.ext.buttons.delete = { text: 'Delete', + name: 'delete', action: function (e, dt, node, config) { /*console.log("delete clicked, num row selected: " + dt.rows({ selected: true }).count()); var data = dt.rows({ selected: true }).data(); @@ -172,10 +175,11 @@ $.fn.dataTable.ext.buttons.quota_scan = { text: 'Quota scan', + name: 'quota_scan', action: function (e, dt, node, config) { - table.button(4).enable(false); + dt.button('quota_scan:name').enable(false); var username = dt.row({ selected: true }).data()[1]; - var path = '{{.APIQuotaScanURL}}' + var path = '{{.QuotaScanURL}}' $.ajax({ url: path, type: 'POST', @@ -183,7 +187,7 @@ data: JSON.stringify({ "username": username }), timeout: 15000, success: function (result) { - table.button(4).enable(true); + dt.button('quota_scan:name').enable(true); $('#successTxt').text("Quota scan started for the selected user. Please reload the user's page to check when the scan ends"); $('#successMsg').show(); setTimeout(function () { @@ -191,8 +195,7 @@ }, 5000); }, error: function ($xhr, textStatus, errorThrown) { - console.log("quota scan error") - table.button(4).enable(true); + dt.button('quota_scan:name').enable(true); var txt = "Unable to update quota for the selected user"; if ($xhr) { var json = $xhr.responseJSON; @@ -221,9 +224,7 @@ "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", select: true, - buttons: [ - 'add', 'edit', 'clone', 'delete', 'quota_scan' - ], + buttons: [], "columnDefs": [ { "targets": [0], @@ -235,12 +236,41 @@ "order": [[1, 'asc']] }); + {{if .LoggedAdmin.HasPermission "quota_scans"}} + table.button().add(0,'quota_scan'); + {{end}} + + {{if .LoggedAdmin.HasPermission "del_users"}} + table.button().add(0,'delete'); + {{end}} + + {{if .LoggedAdmin.HasPermission "add_users"}} + table.button().add(0,'clone'); + {{end}} + + {{if .LoggedAdmin.HasPermission "edit_users"}} + table.button().add(0,'edit'); + {{end}} + + {{if .LoggedAdmin.HasPermission "add_users"}} + table.button().add(0,'add'); + {{end}} + + table.on('select deselect', function () { var selectedRows = table.rows({ selected: true }).count(); - table.button(1).enable(selectedRows == 1); - table.button(2).enable(selectedRows == 1); - table.button(3).enable(selectedRows == 1); - table.button(4).enable(selectedRows == 1); + {{if .LoggedAdmin.HasPermission "edit_users"}} + table.button('edit:name').enable(selectedRows == 1); + {{end}} + {{if .LoggedAdmin.HasPermission "add_users"}} + table.button('clone:name').enable(selectedRows == 1); + {{end}} + {{if .LoggedAdmin.HasPermission "del_users"}} + table.button('delete:name').enable(selectedRows == 1); + {{end}} + {{if .LoggedAdmin.HasPermission "quota_scans"}} + table.button('quota_scan:name').enable(selectedRows == 1); + {{end}} }); }); diff --git a/utils/utils.go b/utils/utils.go index e57cbb86..33882d26 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -26,12 +26,15 @@ import ( "strings" "time" + "github.com/rs/xid" "golang.org/x/crypto/ssh" "github.com/drakkan/sftpgo/logger" ) -const logSender = "utils" +const ( + logSender = "utils" +) // IsStringInSlice searches a string in a slice and returns true if the string is found func IsStringInSlice(obj string, list []string) bool { @@ -383,6 +386,22 @@ func createDirPathIfMissing(file string, perm os.FileMode) error { return nil } +// GenerateRandomBytes generates the secret to use for JWT auth +func GenerateRandomBytes(length int) []byte { + b := make([]byte, length) + _, err := io.ReadFull(rand.Reader, b) + if err != nil { + return b + } + + b = xid.New().Bytes() + for len(b) < length { + b = append(b, xid.New().Bytes()...) + } + + return b[:length] +} + // HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp // and Unix-domain sockets func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool, logSender string) error { diff --git a/webdavd/internal_test.go b/webdavd/internal_test.go index dc0e9a06..ae376021 100644 --- a/webdavd/internal_test.go +++ b/webdavd/internal_test.go @@ -980,7 +980,7 @@ func TestBasicUsersCache(t *testing.T) { _, ok = dataprovider.GetCachedWebDAVUser(username) assert.True(t, ok) // cache is invalidated after user deletion - err = dataprovider.DeleteUser(&user) + err = dataprovider.DeleteUser(user.Username) assert.NoError(t, err) _, ok = dataprovider.GetCachedWebDAVUser(username) assert.False(t, ok) @@ -1164,13 +1164,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) { _, ok = dataprovider.GetCachedWebDAVUser(user4.Username) assert.True(t, ok) - err = dataprovider.DeleteUser(&user1) + err = dataprovider.DeleteUser(user1.Username) assert.NoError(t, err) - err = dataprovider.DeleteUser(&user2) + err = dataprovider.DeleteUser(user2.Username) assert.NoError(t, err) - err = dataprovider.DeleteUser(&user3) + err = dataprovider.DeleteUser(user3.Username) assert.NoError(t, err) - err = dataprovider.DeleteUser(&user4) + err = dataprovider.DeleteUser(user4.Username) assert.NoError(t, err) err = os.RemoveAll(u.GetHomeDir()) diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index 18eecbf5..a017ba61 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -29,7 +29,7 @@ import ( "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpclient" - "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/httpdtest" "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" @@ -127,7 +127,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) if err != nil { logger.ErrorToConsole("error initializing data provider: %v", err) os.Exit(1) @@ -144,7 +144,7 @@ func TestMain(m *testing.M) { httpdConf := config.GetHTTPDConfig() httpdConf.BindPort = 8078 - httpd.SetBaseURLAndCredentials("http://127.0.0.1:8078", "", "") + httpdtest.SetBaseURL("http://127.0.0.1:8078") // required to test sftpfs sftpdConf := config.GetSFTPDConfig() @@ -288,11 +288,11 @@ func TestInitialization(t *testing.T) { func TestBasicHandling(t *testing.T) { u := getTestUser() u.QuotaSize = 6553600 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.QuotaSize = 6553600 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { client := getWebDavClient(user) @@ -311,7 +311,7 @@ func TestBasicHandling(t *testing.T) { localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = downloadFile(testFileName, localDownloadPath, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -322,13 +322,13 @@ func TestBasicHandling(t *testing.T) { // the webdav client hide the error we check the quota err = client.Remove(testFileName) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) err = client.Remove(testFileName + "1") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize) @@ -364,9 +364,9 @@ func TestBasicHandling(t *testing.T) { assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -378,7 +378,7 @@ func TestBasicHandling(t *testing.T) { func TestBasicHandlingCryptFs(t *testing.T) { u := getTestUserWithCryptFs() u.QuotaSize = 6553600 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) @@ -399,7 +399,7 @@ func TestBasicHandlingCryptFs(t *testing.T) { localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = downloadFile(testFileName, localDownloadPath, testFileSize, client) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize) @@ -410,7 +410,7 @@ func TestBasicHandlingCryptFs(t *testing.T) { } err = client.Remove(testFileName) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles) assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize) @@ -443,7 +443,7 @@ func TestBasicHandlingCryptFs(t *testing.T) { assert.NoError(t, err) err = os.Remove(localDownloadPath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -453,13 +453,13 @@ func TestBasicHandlingCryptFs(t *testing.T) { func TestPropPatch(t *testing.T) { u := getTestUser() u.Username = u.Username + "1" - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) sftpUser := getTestSFTPUser() sftpUser.FsConfig.SFTPConfig.Username = localUser.Username for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} { - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client), sftpUser.Username) @@ -486,13 +486,13 @@ func TestPropPatch(t *testing.T) { } err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) assert.Len(t, common.Connections.GetStats(), 0) } - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -500,14 +500,14 @@ func TestPropPatch(t *testing.T) { func TestLoginInvalidPwd(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) user.Password = "wrong" client = getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -527,7 +527,7 @@ func TestDefender(t *testing.T) { err := common.Initialize(cfg) assert.NoError(t, err) - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) @@ -545,7 +545,7 @@ func TestDefender(t *testing.T) { assert.Contains(t, err.Error(), "403") } - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -556,26 +556,26 @@ func TestDefender(t *testing.T) { func TestLoginInvalidURL(t *testing.T) { u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u1 := getTestUser() u1.Username = user.Username + "1" - user1, _, err := httpd.AddUser(u1, http.StatusOK) + user1, _, err := httpdtest.AddUser(u1, http.StatusCreated) assert.NoError(t, err) rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username+"1") client := gowebdav.NewClient(rootPath, user.Username, defaultPassword) client.SetTimeout(5 * time.Second) assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(user1, http.StatusOK) + _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) } func TestRootRedirect(t *testing.T) { errRedirect := errors.New("redirect error") u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) @@ -612,7 +612,7 @@ func TestRootRedirect(t *testing.T) { err = resp.Body.Close() assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) } @@ -630,29 +630,26 @@ func TestLoginExternalAuth(t *testing.T) { assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) client := getWebDavClient(u) assert.NoError(t, checkBasicFunc(client)) u.Username = defaultUsername + "1" client = getWebDavClient(u) assert.Error(t, checkBasicFunc(client)) - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, defaultUsername, user.Username) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) - if assert.Len(t, users, 1) { - user := users[0] - assert.Equal(t, defaultUsername, user.Username) - _, err = httpd.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) - err = os.RemoveAll(user.GetHomeDir()) - assert.NoError(t, err) - } err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(extAuthPath) assert.NoError(t, err) @@ -671,30 +668,27 @@ func TestPreLoginHook(t *testing.T) { err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm) assert.NoError(t, err) providerConf.PreLoginHook = preLoginPath - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) - users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + _, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound) assert.NoError(t, err) - assert.Equal(t, 0, len(users)) client := getWebDavClient(u) assert.NoError(t, checkBasicFunc(client)) - users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - user := users[0] // test login with an existing user client = getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm) assert.NoError(t, err) // update the user to remove it from the cache - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client = getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) // update the user to remove it from the cache - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) user.Status = 0 err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm) @@ -702,7 +696,7 @@ func TestPreLoginHook(t *testing.T) { client = getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -711,7 +705,7 @@ func TestPreLoginHook(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() - err = dataprovider.Initialize(providerConf, configDir) + err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) err = os.Remove(preLoginPath) assert.NoError(t, err) @@ -724,7 +718,7 @@ func TestPostConnectHook(t *testing.T) { common.Config.PostConnectHook = postConnectPath u := getTestUser() - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm) assert.NoError(t, err) @@ -734,13 +728,13 @@ func TestPostConnectHook(t *testing.T) { assert.NoError(t, err) assert.Error(t, checkBasicFunc(client)) - common.Config.PostConnectHook = "http://127.0.0.1:8078/api/v1/version" + common.Config.PostConnectHook = "http://127.0.0.1:8078/healthz" assert.NoError(t, checkBasicFunc(client)) common.Config.PostConnectHook = "http://127.0.0.1:8078/notfound" assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -752,7 +746,7 @@ func TestMaxConnections(t *testing.T) { oldValue := common.Config.MaxTotalConnections common.Config.MaxTotalConnections = 1 - user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) @@ -764,7 +758,7 @@ func TestMaxConnections(t *testing.T) { common.Connections.Add(connection) assert.Error(t, checkBasicFunc(client)) common.Connections.Remove(connection.GetID()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -776,7 +770,7 @@ func TestMaxConnections(t *testing.T) { func TestMaxSessions(t *testing.T) { u := getTestUser() u.MaxSessions = 1 - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) @@ -788,7 +782,7 @@ func TestMaxSessions(t *testing.T) { common.Connections.Add(connection) assert.Error(t, checkBasicFunc(client)) common.Connections.Remove(connection.GetID()) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -799,12 +793,12 @@ func TestLoginWithIPilters(t *testing.T) { u := getTestUser() u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"} u.Filters.AllowedIP = []string{"172.19.0.0/16"} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -833,7 +827,7 @@ func TestDownloadErrors(t *testing.T) { DeniedPatterns: []string{"*.jpg"}, }, } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) testFilePath1 := filepath.Join(user.HomeDir, subDir1, "file.zipp") @@ -861,7 +855,7 @@ func TestDownloadErrors(t *testing.T) { err = os.Remove(localDownloadPath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -883,7 +877,7 @@ func TestUploadErrors(t *testing.T) { DeniedExtensions: []string{".zip"}, }, } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) testFilePath := filepath.Join(homeBasePath, testFileName) @@ -918,7 +912,7 @@ func TestUploadErrors(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -927,18 +921,18 @@ func TestUploadErrors(t *testing.T) { func TestDeniedLoginMethod(t *testing.T) { u := getTestUser() u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client = getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -947,18 +941,18 @@ func TestDeniedLoginMethod(t *testing.T) { func TestDeniedProtocols(t *testing.T) { u := getTestUser() u.Filters.DeniedProtocols = []string{common.ProtocolWebDAV} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) client = getWebDavClient(user) assert.NoError(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -967,11 +961,11 @@ func TestDeniedProtocols(t *testing.T) { func TestQuotaLimits(t *testing.T) { u := getTestUser() u.QuotaFiles = 1 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.QuotaFiles = 1 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testFileSize := int64(65535) @@ -1002,7 +996,7 @@ func TestQuotaLimits(t *testing.T) { // test quota size user.QuotaSize = testFileSize - 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = uploadFile(testFilePath, testFileName+".quota", testFileSize, client) assert.Error(t, err) @@ -1011,7 +1005,7 @@ func TestQuotaLimits(t *testing.T) { // now test quota limits while uploading the current file, we have 1 bytes remaining user.QuotaSize = testFileSize + 1 user.QuotaFiles = 0 - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) err = uploadFile(testFilePath1, testFileName1, testFileSize1, client) assert.Error(t, err) @@ -1040,13 +1034,13 @@ func TestQuotaLimits(t *testing.T) { assert.NoError(t, err) user.QuotaFiles = 0 user.QuotaSize = 0 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1056,11 +1050,11 @@ func TestUploadMaxSize(t *testing.T) { testFileSize := int64(65535) u := getTestUser() u.Filters.MaxUploadFileSize = testFileSize + 1 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.Filters.MaxUploadFileSize = testFileSize + 1 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testFilePath := filepath.Join(homeBasePath, testFileName) @@ -1090,13 +1084,13 @@ func TestUploadMaxSize(t *testing.T) { err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) user.Filters.MaxUploadFileSize = 65536000 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1106,12 +1100,12 @@ func TestClientClose(t *testing.T) { u := getTestUser() u.UploadBandwidth = 64 u.DownloadBandwidth = 64 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.UploadBandwidth = 64 u.DownloadBandwidth = 64 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { testFileSize := int64(1048576) @@ -1179,9 +1173,9 @@ func TestClientClose(t *testing.T) { assert.NoError(t, err) } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1202,7 +1196,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) - err := dataprovider.Initialize(providerConf, configDir) + err := dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) if _, err = os.Stat(credentialsFile); err == nil { @@ -1210,7 +1204,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, os.Remove(credentialsFile)) } - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) @@ -1224,7 +1218,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { err = client.Connect() assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1232,7 +1226,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { assert.NoError(t, dataprovider.Close()) assert.NoError(t, config.LoadConfig(configDir, "")) providerConf = config.GetProviderConf() - assert.NoError(t, dataprovider.Initialize(providerConf, configDir)) + assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true)) } func TestLoginInvalidFs(t *testing.T) { @@ -1240,7 +1234,7 @@ func TestLoginInvalidFs(t *testing.T) { u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) providerConf := config.GetProviderConf() @@ -1256,7 +1250,7 @@ func TestLoginInvalidFs(t *testing.T) { client := getWebDavClient(user) assert.Error(t, checkBasicFunc(client)) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1265,13 +1259,13 @@ func TestLoginInvalidFs(t *testing.T) { func TestBytesRangeRequests(t *testing.T) { u := getTestUser() u.Username = u.Username + "1" - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) sftpUser := getTestSFTPUser() sftpUser.FsConfig.SFTPConfig.Username = localUser.Username for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} { - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) testFileName := "test_file.txt" testFilePath := filepath.Join(homeBasePath, testFileName) @@ -1309,12 +1303,12 @@ func TestBytesRangeRequests(t *testing.T) { assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) @@ -1324,7 +1318,7 @@ func TestGETAsPROPFIND(t *testing.T) { u := getTestUser() subDir1 := "/sub1" u.Permissions[subDir1] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username) httpClient := httpclient.GetHTTPClient() @@ -1371,13 +1365,13 @@ func TestGETAsPROPFIND(t *testing.T) { assert.Len(t, files, 0) // if we grant the permissions the files are listed user.Permissions[subDir1] = []string{dataprovider.PermDownload, dataprovider.PermListItems} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) files, err = client.ReadDir(subDir1) assert.NoError(t, err) assert.Len(t, files, 1) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1386,7 +1380,7 @@ func TestGETAsPROPFIND(t *testing.T) { func TestStat(t *testing.T) { u := getTestUser() u.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload} - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) subDir := "subdir" @@ -1401,7 +1395,7 @@ func TestStat(t *testing.T) { err = uploadFile(testFilePath, path.Join("/", subDir, testFileName), testFileSize, client) assert.NoError(t, err) user.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermDownload} - user, _, err = httpd.UpdateUser(user, http.StatusOK, "") + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) _, err = client.Stat(testFileName) assert.NoError(t, err) @@ -1410,7 +1404,7 @@ func TestStat(t *testing.T) { err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1430,7 +1424,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { }) err := os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) - user, _, err := httpd.AddUser(u, http.StatusOK) + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client := getWebDavClient(user) files, err := client.ReadDir(".") @@ -1454,7 +1448,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { assert.NoError(t, err) err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client) assert.NoError(t, err) - folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -1463,7 +1457,7 @@ func TestUploadOverwriteVfolder(t *testing.T) { } err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client) assert.NoError(t, err) - folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK) + folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK) assert.NoError(t, err) if assert.Len(t, folder, 1) { f := folder[0] @@ -1472,9 +1466,9 @@ func TestUploadOverwriteVfolder(t *testing.T) { } err = os.Remove(testFilePath) assert.NoError(t, err) - _, err = httpd.RemoveUser(user, http.StatusOK) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -1485,11 +1479,11 @@ func TestUploadOverwriteVfolder(t *testing.T) { func TestMiscCommands(t *testing.T) { u := getTestUser() u.QuotaFiles = 100 - localUser, _, err := httpd.AddUser(u, http.StatusOK) + localUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) u = getTestSFTPUser() u.QuotaFiles = 100 - sftpUser, _, err := httpd.AddUser(u, http.StatusOK) + sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) for _, user := range []dataprovider.User{localUser, sftpUser} { dir := "testDir" @@ -1508,7 +1502,7 @@ func TestMiscCommands(t *testing.T) { assert.NoError(t, err) err = client.Copy(dir, dir+"_copy", false) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 6, user.UsedQuotaFiles) assert.Equal(t, 6*testFileSize, user.UsedQuotaSize) @@ -1518,7 +1512,7 @@ func TestMiscCommands(t *testing.T) { assert.Error(t, err) err = client.Copy(dir+"_copy", dir+"_copy1", true) assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 9, user.UsedQuotaFiles) assert.Equal(t, 9*testFileSize, user.UsedQuotaSize) @@ -1532,7 +1526,7 @@ func TestMiscCommands(t *testing.T) { assert.NoError(t, err) err = client.RemoveAll(dir + "_copy1") assert.NoError(t, err) - user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 6, user.UsedQuotaFiles) assert.Equal(t, 6*testFileSize, user.UsedQuotaSize) @@ -1543,13 +1537,13 @@ func TestMiscCommands(t *testing.T) { err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) user.QuotaFiles = 0 - _, _, err = httpd.UpdateUser(user, http.StatusOK, "") + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) } } - _, err = httpd.RemoveUser(sftpUser, http.StatusOK) + _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK) assert.NoError(t, err) - _, err = httpd.RemoveUser(localUser, http.StatusOK) + _, err = httpdtest.RemoveUser(localUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(localUser.GetHomeDir()) assert.NoError(t, err) diff --git a/windows-installer/sftpgo.iss b/windows-installer/sftpgo.iss index 6a166d54..3a966346 100644 --- a/windows-installer/sftpgo.iss +++ b/windows-installer/sftpgo.iss @@ -44,7 +44,6 @@ Source: "{#MyAppDir}\sftpgo.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#MyAppDir}\sftpgo.db"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall Source: "{#MyAppDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion Source: "{#MyAppDir}\sftpgo.json"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall -Source: "{#MyAppDir}\sftpgo_api_cli.exe"; DestDir: "{app}\examples\rest-api-cli"; Flags: ignoreversion; MinVersion: 10 Source: "{#MyAppDir}\templates\*"; DestDir: "{commonappdata}\{#MyAppName}\templates"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#MyAppDir}\static\*"; DestDir: "{commonappdata}\{#MyAppName}\static"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -56,7 +55,6 @@ Name: "{commonappdata}\{#MyAppName}\credentials"; Permissions: everyone-full [Icons] Name: "{group}\Web Admin"; Filename: "http://127.0.0.1:8080/web"; Name: "{group}\Service Control"; WorkingDir: "{app}"; Filename: "powershell.exe"; Parameters: "-Command ""Start-Process cmd \""/k cd {app} & {#MyAppExeName} service --help\"" -Verb RunAs"; Comment: "Manage SFTPGo Service" -Name: "{group}\REST API CLI"; WorkingDir: "{app}\examples\rest-api-cli"; Filename: "{cmd}"; Parameters: "/k sftpgo_api_cli.exe --help"; Comment: "Manage users, folders and connections"; MinVersion: 10 Name: "{group}\Documentation"; Filename: "{#DocURL}"; Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"