sftpgo/internal/httpd/webadmin.go
Nicola Murino ec5da8b4a5
ftpd: allow to require TLS on a per-user basis
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-26 18:51:39 +02:00

3166 lines
103 KiB
Go

// Copyright (C) 2019-2022 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package httpd
import (
"errors"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-chi/render"
"github.com/sftpgo/sdk"
sdkkms "github.com/sftpgo/sdk/kms"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/mfa"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/version"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
type userPageMode int
const (
userPageModeAdd userPageMode = iota + 1
userPageModeUpdate
userPageModeTemplate
)
type folderPageMode int
const (
folderPageModeAdd folderPageMode = iota + 1
folderPageModeUpdate
folderPageModeTemplate
)
type genericPageMode int
const (
genericPageModeAdd genericPageMode = iota + 1
genericPageModeUpdate
)
const (
templateAdminDir = "webadmin"
templateBase = "base.html"
templateBaseLogin = "baselogin.html"
templateFsConfig = "fsconfig.html"
templateSharedComponents = "sharedcomponents.html"
templateUsers = "users.html"
templateUser = "user.html"
templateAdmins = "admins.html"
templateAdmin = "admin.html"
templateConnections = "connections.html"
templateGroups = "groups.html"
templateGroup = "group.html"
templateFolders = "folders.html"
templateFolder = "folder.html"
templateEventRules = "eventrules.html"
templateEventRule = "eventrule.html"
templateEventActions = "eventactions.html"
templateEventAction = "eventaction.html"
templateMessage = "message.html"
templateStatus = "status.html"
templateLogin = "login.html"
templateDefender = "defender.html"
templateProfile = "profile.html"
templateChangePwd = "changepassword.html"
templateMaintenance = "maintenance.html"
templateMFA = "mfa.html"
templateSetup = "adminsetup.html"
pageUsersTitle = "Users"
pageAdminsTitle = "Admins"
pageConnectionsTitle = "Connections"
pageStatusTitle = "Status"
pageFoldersTitle = "Folders"
pageGroupsTitle = "Groups"
pageEventRulesTitle = "Event rules"
pageEventActionsTitle = "Event actions"
pageProfileTitle = "My profile"
pageChangePwdTitle = "Change password"
pageMaintenanceTitle = "Maintenance"
pageDefenderTitle = "Defender"
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user"
defaultQueryLimit = 500
)
var (
adminTemplates = make(map[string]*template.Template)
)
type basePage struct {
Title string
CurrentURL string
UsersURL string
UserURL string
UserTemplateURL string
AdminsURL string
AdminURL string
QuotaScanURL string
ConnectionsURL string
GroupsURL string
GroupURL string
FoldersURL string
FolderURL string
FolderTemplateURL string
DefenderURL string
LogoutURL string
ProfileURL string
ChangePwdURL string
MFAURL string
EventRulesURL string
EventRuleURL string
EventActionsURL string
EventActionURL string
FolderQuotaScanURL string
StatusURL string
MaintenanceURL string
StaticURL string
UsersTitle string
AdminsTitle string
ConnectionsTitle string
FoldersTitle string
GroupsTitle string
EventRulesTitle string
EventActionsTitle string
StatusTitle string
MaintenanceTitle string
DefenderTitle string
Version string
CSRFToken string
IsEventManagerPage bool
HasDefender bool
HasExternalLogin bool
LoggedAdmin *dataprovider.Admin
Branding UIBranding
}
type usersPage struct {
basePage
Users []dataprovider.User
}
type adminsPage struct {
basePage
Admins []dataprovider.Admin
}
type foldersPage struct {
basePage
Folders []vfs.BaseVirtualFolder
}
type groupsPage struct {
basePage
Groups []dataprovider.Group
}
type eventRulesPage struct {
basePage
Rules []dataprovider.EventRule
}
type eventActionsPage struct {
basePage
Actions []dataprovider.BaseEventAction
}
type connectionsPage struct {
basePage
Connections []common.ConnectionStatus
}
type statusPage struct {
basePage
Status *ServicesStatus
}
type fsWrapper struct {
vfs.Filesystem
IsUserPage bool
IsGroupPage bool
HasUsersBaseDir bool
DirPath string
}
type userPage struct {
basePage
User *dataprovider.User
RootPerms []string
Error string
ValidPerms []string
ValidLoginMethods []string
ValidProtocols []string
TwoFactorProtocols []string
WebClientOptions []string
RootDirPerms []string
Mode userPageMode
VirtualFolders []vfs.BaseVirtualFolder
Groups []dataprovider.Group
CanImpersonate bool
FsWrapper fsWrapper
}
type adminPage struct {
basePage
Admin *dataprovider.Admin
Error string
IsAdd bool
}
type profilePage struct {
basePage
Error string
AllowAPIKeyAuth bool
Email string
Description string
}
type changePasswordPage struct {
basePage
Error string
}
type mfaPage struct {
basePage
TOTPConfigs []string
TOTPConfig dataprovider.AdminTOTPConfig
GenerateTOTPURL string
ValidateTOTPURL string
SaveTOTPURL string
RecCodesURL string
}
type maintenancePage struct {
basePage
BackupPath string
RestorePath string
Error string
}
type defenderHostsPage struct {
basePage
DefenderHostsURL string
}
type setupPage struct {
basePage
Username string
HasInstallationCode bool
InstallationCodeHint string
HideSupportLink bool
Error string
}
type folderPage struct {
basePage
Folder vfs.BaseVirtualFolder
Error string
Mode folderPageMode
FsWrapper fsWrapper
}
type groupPage struct {
basePage
Group *dataprovider.Group
Error string
Mode genericPageMode
ValidPerms []string
ValidLoginMethods []string
ValidProtocols []string
TwoFactorProtocols []string
WebClientOptions []string
VirtualFolders []vfs.BaseVirtualFolder
FsWrapper fsWrapper
}
type eventActionPage struct {
basePage
Action dataprovider.BaseEventAction
ActionTypes []dataprovider.EnumMapping
HTTPMethods []string
RedactedSecret string
Error string
Mode genericPageMode
}
type eventRulePage struct {
basePage
Rule dataprovider.EventRule
TriggerTypes []dataprovider.EnumMapping
Actions []dataprovider.BaseEventAction
FsEvents []string
Protocols []string
ProviderEvents []string
ProviderObjects []string
Error string
Mode genericPageMode
IsShared bool
}
type messagePage struct {
basePage
Error string
Success string
}
type userTemplateFields struct {
Username string
Password string
PublicKeys []string
}
func loadAdminTemplates(templatesPath string) {
usersPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateUsers),
}
userPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
filepath.Join(templatesPath, templateAdminDir, templateUser),
}
adminsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateAdmins),
}
adminPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateAdmin),
}
profilePaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateProfile),
}
changePwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateChangePwd),
}
connectionsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateConnections),
}
messagePaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMessage),
}
foldersPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFolders),
}
folderPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
filepath.Join(templatesPath, templateAdminDir, templateFolder),
}
groupsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateGroups),
}
groupPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
filepath.Join(templatesPath, templateAdminDir, templateGroup),
}
eventRulesPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventRules),
}
eventRulePaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventRule),
}
eventActionsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventActions),
}
eventActionPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventAction),
}
statusPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateStatus),
}
loginPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateLogin),
}
maintenancePaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMaintenance),
}
defenderPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateDefender),
}
mfaPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMFA),
}
twoFactorPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateTwoFactor),
}
twoFactorRecoveryPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateTwoFactorRecovery),
}
setupPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateSetup),
}
forgotPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
}
resetPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
}
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
"ListFSProviders": func() []sdk.FilesystemProvider {
return []sdk.FilesystemProvider{sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider,
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider, sdk.AzureBlobFilesystemProvider,
sdk.SFTPFilesystemProvider, sdk.HTTPFilesystemProvider,
}
},
})
usersTmpl := util.LoadTemplate(nil, usersPaths...)
userTmpl := util.LoadTemplate(fsBaseTpl, userPaths...)
adminsTmpl := util.LoadTemplate(nil, adminsPaths...)
adminTmpl := util.LoadTemplate(nil, adminPaths...)
connectionsTmpl := util.LoadTemplate(nil, connectionsPaths...)
messageTmpl := util.LoadTemplate(nil, messagePaths...)
groupsTmpl := util.LoadTemplate(nil, groupsPaths...)
groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)
foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)
eventRulesTmpl := util.LoadTemplate(nil, eventRulesPaths...)
eventRuleTmpl := util.LoadTemplate(nil, eventRulePaths...)
eventActionsTmpl := util.LoadTemplate(nil, eventActionsPaths...)
eventActionTmpl := util.LoadTemplate(nil, eventActionPaths...)
statusTmpl := util.LoadTemplate(nil, statusPaths...)
loginTmpl := util.LoadTemplate(nil, loginPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...)
changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)
maintenanceTmpl := util.LoadTemplate(nil, maintenancePaths...)
defenderTmpl := util.LoadTemplate(nil, defenderPaths...)
mfaTmpl := util.LoadTemplate(nil, mfaPaths...)
twoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)
twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)
setupTmpl := util.LoadTemplate(nil, setupPaths...)
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
adminTemplates[templateUsers] = usersTmpl
adminTemplates[templateUser] = userTmpl
adminTemplates[templateAdmins] = adminsTmpl
adminTemplates[templateAdmin] = adminTmpl
adminTemplates[templateConnections] = connectionsTmpl
adminTemplates[templateMessage] = messageTmpl
adminTemplates[templateGroups] = groupsTmpl
adminTemplates[templateGroup] = groupTmpl
adminTemplates[templateFolders] = foldersTmpl
adminTemplates[templateFolder] = folderTmpl
adminTemplates[templateEventRules] = eventRulesTmpl
adminTemplates[templateEventRule] = eventRuleTmpl
adminTemplates[templateEventActions] = eventActionsTmpl
adminTemplates[templateEventAction] = eventActionTmpl
adminTemplates[templateStatus] = statusTmpl
adminTemplates[templateLogin] = loginTmpl
adminTemplates[templateProfile] = profileTmpl
adminTemplates[templateChangePwd] = changePwdTmpl
adminTemplates[templateMaintenance] = maintenanceTmpl
adminTemplates[templateDefender] = defenderTmpl
adminTemplates[templateMFA] = mfaTmpl
adminTemplates[templateTwoFactor] = twoFactorTmpl
adminTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl
adminTemplates[templateSetup] = setupTmpl
adminTemplates[templateForgotPassword] = forgotPwdTmpl
adminTemplates[templateResetPassword] = resetPwdTmpl
}
func isEventManagerResource(currentURL string) bool {
if currentURL == webAdminEventRulesPath {
return true
}
if currentURL == webAdminEventActionsPath {
return true
}
if currentURL == webAdminEventRulePath || strings.HasPrefix(currentURL, webAdminEventRulePath+"/") {
return true
}
if currentURL == webAdminEventActionPath || strings.HasPrefix(currentURL, webAdminEventActionPath+"/") {
return true
}
return false
}
func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
var csrfToken string
if currentURL != "" {
csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
}
return basePage{
Title: title,
CurrentURL: currentURL,
UsersURL: webUsersPath,
UserURL: webUserPath,
UserTemplateURL: webTemplateUser,
AdminsURL: webAdminsPath,
AdminURL: webAdminPath,
GroupsURL: webGroupsPath,
GroupURL: webGroupPath,
FoldersURL: webFoldersPath,
FolderURL: webFolderPath,
FolderTemplateURL: webTemplateFolder,
DefenderURL: webDefenderPath,
LogoutURL: webLogoutPath,
ProfileURL: webAdminProfilePath,
ChangePwdURL: webChangeAdminPwdPath,
MFAURL: webAdminMFAPath,
EventRulesURL: webAdminEventRulesPath,
EventRuleURL: webAdminEventRulePath,
EventActionsURL: webAdminEventActionsPath,
EventActionURL: webAdminEventActionPath,
QuotaScanURL: webQuotaScanPath,
ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath,
FolderQuotaScanURL: webScanVFolderPath,
MaintenanceURL: webMaintenancePath,
StaticURL: webStaticFilesPath,
UsersTitle: pageUsersTitle,
AdminsTitle: pageAdminsTitle,
ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle,
GroupsTitle: pageGroupsTitle,
EventRulesTitle: pageEventRulesTitle,
EventActionsTitle: pageEventActionsTitle,
StatusTitle: pageStatusTitle,
MaintenanceTitle: pageMaintenanceTitle,
DefenderTitle: pageDefenderTitle,
Version: version.GetAsString(),
LoggedAdmin: getAdminFromToken(r),
IsEventManagerPage: isEventManagerResource(currentURL),
HasDefender: common.Config.DefenderConfig.Enabled,
HasExternalLogin: isLoggedInWithOIDC(r),
CSRFToken: csrfToken,
Branding: s.binding.Branding.WebAdmin,
}
}
func renderAdminTemplate(w http.ResponseWriter, tmplName string, data any) {
err := adminTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *httpdServer) renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int,
err error, message string,
) {
var errorString string
if body != "" {
errorString = body + " "
}
if err != nil {
errorString += err.Error()
}
data := messagePage{
basePage: s.getBasePageData(title, "", r),
Error: errorString,
Success: message,
}
w.WriteHeader(statusCode)
renderAdminTemplate(w, templateMessage, data)
}
func (s *httpdServer) renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
s.renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
}
func (s *httpdServer) renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
s.renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
}
func (s *httpdServer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
s.renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
}
func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
s.renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
}
func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error, ip string) {
data := forgotPwdPage{
CurrentURL: webAdminForgotPwdPath,
Error: error,
CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath,
Title: pageForgotPwdTitle,
Branding: s.binding.Branding.WebAdmin,
}
renderAdminTemplate(w, templateForgotPassword, data)
}
func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error, ip string) {
data := resetPwdPage{
CurrentURL: webAdminResetPwdPath,
Error: error,
CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath,
Title: pageResetPwdTitle,
Branding: s.binding.Branding.WebAdmin,
}
renderAdminTemplate(w, templateResetPassword, data)
}
func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error, ip string) {
data := twoFactorPage{
CurrentURL: webAdminTwoFactorPath,
Version: version.Get().Version,
Error: error,
CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath,
RecoveryURL: webAdminTwoFactorRecoveryPath,
Branding: s.binding.Branding.WebAdmin,
}
renderAdminTemplate(w, templateTwoFactor, data)
}
func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, error, ip string) {
data := twoFactorPage{
CurrentURL: webAdminTwoFactorRecoveryPath,
Version: version.Get().Version,
Error: error,
CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath,
Branding: s.binding.Branding.WebAdmin,
}
renderAdminTemplate(w, templateTwoFactorRecovery, data)
}
func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
data := mfaPage{
basePage: s.getBasePageData(pageMFATitle, webAdminMFAPath, r),
TOTPConfigs: mfa.GetAvailableTOTPConfigNames(),
GenerateTOTPURL: webAdminTOTPGeneratePath,
ValidateTOTPURL: webAdminTOTPValidatePath,
SaveTOTPURL: webAdminTOTPSavePath,
RecCodesURL: webAdminRecoveryCodesPath,
}
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
data.TOTPConfig = admin.Filters.TOTPConfig
renderAdminTemplate(w, templateMFA, data)
}
func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, error string) {
data := profilePage{
basePage: s.getBasePageData(pageProfileTitle, webAdminProfilePath, r),
Error: error,
}
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
data.AllowAPIKeyAuth = admin.Filters.AllowAPIKeyAuth
data.Email = admin.Email
data.Description = admin.Description
renderAdminTemplate(w, templateProfile, data)
}
func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
data := changePasswordPage{
basePage: s.getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
Error: error,
}
renderAdminTemplate(w, templateChangePwd, data)
}
func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
data := maintenancePage{
basePage: s.getBasePageData(pageMaintenanceTitle, webMaintenancePath, r),
BackupPath: webBackupPath,
RestorePath: webRestorePath,
Error: error,
}
renderAdminTemplate(w, templateMaintenance, data)
}
func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
data := setupPage{
basePage: s.getBasePageData(pageSetupTitle, webAdminSetupPath, r),
Username: username,
HasInstallationCode: installationCode != "",
InstallationCodeHint: installationCodeHint,
HideSupportLink: hideSupportLink,
Error: error,
}
renderAdminTemplate(w, templateSetup, data)
}
func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
error string, isAdd bool) {
currentURL := webAdminPath
title := "Add a new admin"
if !isAdd {
currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
title = "Update admin"
}
data := adminPage{
basePage: s.getBasePageData(title, currentURL, r),
Admin: admin,
Error: error,
IsAdd: isAdd,
}
renderAdminTemplate(w, templateAdmin, data)
}
func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,
mode userPageMode, error string,
) {
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
if err != nil {
return
}
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
if err != nil {
return
}
user.SetEmptySecretsIfNil()
var title, currentURL string
switch mode {
case userPageModeAdd:
title = "Add a new user"
currentURL = webUserPath
case userPageModeUpdate:
title = "Update user"
currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username))
case userPageModeTemplate:
title = "User template"
currentURL = webTemplateUser
}
if user.Password != "" && user.IsPasswordHashed() {
switch mode {
case userPageModeUpdate:
user.Password = redactedSecret
default:
user.Password = ""
}
}
user.FsConfig.RedactedSecret = redactedSecret
data := userPage{
basePage: s.getBasePageData(title, currentURL, r),
Mode: mode,
Error: error,
User: user,
ValidPerms: dataprovider.ValidPerms,
ValidLoginMethods: dataprovider.ValidLoginMethods,
ValidProtocols: dataprovider.ValidProtocols,
TwoFactorProtocols: dataprovider.MFAProtocols,
WebClientOptions: sdk.WebClientOptions,
RootDirPerms: user.GetPermissionsForPath("/"),
VirtualFolders: folders,
Groups: groups,
CanImpersonate: os.Getuid() == 0,
FsWrapper: fsWrapper{
Filesystem: user.FsConfig,
IsUserPage: true,
IsGroupPage: false,
HasUsersBaseDir: dataprovider.HasUsersBaseDir(),
DirPath: user.HomeDir,
},
}
renderAdminTemplate(w, templateUser, data)
}
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
mode genericPageMode, error string,
) {
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
if err != nil {
return
}
group.SetEmptySecretsIfNil()
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add a new group"
currentURL = webGroupPath
case genericPageModeUpdate:
title = "Update group"
currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name))
}
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
data := groupPage{
basePage: s.getBasePageData(title, currentURL, r),
Error: error,
Group: &group,
Mode: mode,
ValidPerms: dataprovider.ValidPerms,
ValidLoginMethods: dataprovider.ValidLoginMethods,
ValidProtocols: dataprovider.ValidProtocols,
TwoFactorProtocols: dataprovider.MFAProtocols,
WebClientOptions: sdk.WebClientOptions,
VirtualFolders: folders,
FsWrapper: fsWrapper{
Filesystem: group.UserSettings.FsConfig,
IsUserPage: false,
IsGroupPage: true,
HasUsersBaseDir: false,
DirPath: group.UserSettings.HomeDir,
},
}
renderAdminTemplate(w, templateGroup, data)
}
func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Request, action dataprovider.BaseEventAction,
mode genericPageMode, error string,
) {
action.Options.SetEmptySecretsIfNil()
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add a new event action"
currentURL = webAdminEventActionPath
case genericPageModeUpdate:
title = "Update event action"
currentURL = fmt.Sprintf("%v/%v", webAdminEventActionPath, url.PathEscape(action.Name))
}
if action.Options.HTTPConfig.Timeout == 0 {
action.Options.HTTPConfig.Timeout = 20
}
if action.Options.CmdConfig.Timeout == 0 {
action.Options.CmdConfig.Timeout = 20
}
data := eventActionPage{
basePage: s.getBasePageData(title, currentURL, r),
Action: action,
ActionTypes: dataprovider.EventActionTypes,
HTTPMethods: dataprovider.SupportedHTTPActionMethods,
RedactedSecret: redactedSecret,
Error: error,
Mode: mode,
}
renderAdminTemplate(w, templateEventAction, data)
}
func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,
mode genericPageMode, error string,
) {
actions, err := s.getWebEventActions(w, r, defaultQueryLimit, true)
if err != nil {
return
}
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add new event rules"
currentURL = webAdminEventRulePath
case genericPageModeUpdate:
title = "Update event rules"
currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name))
}
data := eventRulePage{
basePage: s.getBasePageData(title, currentURL, r),
Rule: rule,
TriggerTypes: dataprovider.EventTriggerTypes,
Actions: actions,
FsEvents: dataprovider.SupportedFsEvents,
Protocols: dataprovider.SupportedRuleConditionProtocols,
ProviderEvents: dataprovider.SupportedProviderEvents,
ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
Error: error,
Mode: mode,
IsShared: s.isShared > 0,
}
renderAdminTemplate(w, templateEventRule, data)
}
func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder,
mode folderPageMode, error string,
) {
var title, currentURL string
switch mode {
case folderPageModeAdd:
title = "Add a new folder"
currentURL = webFolderPath
case folderPageModeUpdate:
title = "Update folder"
currentURL = fmt.Sprintf("%v/%v", webFolderPath, url.PathEscape(folder.Name))
case folderPageModeTemplate:
title = "Folder template"
currentURL = webTemplateFolder
}
folder.FsConfig.RedactedSecret = redactedSecret
folder.FsConfig.SetEmptySecretsIfNil()
data := folderPage{
basePage: s.getBasePageData(title, currentURL, r),
Error: error,
Folder: folder,
Mode: mode,
FsWrapper: fsWrapper{
Filesystem: folder.FsConfig,
IsUserPage: false,
IsGroupPage: false,
HasUsersBaseDir: false,
DirPath: folder.MappedPath,
},
}
renderAdminTemplate(w, templateFolder, data)
}
func getFoldersForTemplate(r *http.Request) []string {
var res []string
folderNames := r.Form["tpl_foldername"]
folders := make(map[string]bool)
for _, name := range folderNames {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if _, ok := folders[name]; ok {
continue
}
folders[name] = true
res = append(res, name)
}
return res
}
func getUsersForTemplate(r *http.Request) []userTemplateFields {
var res []userTemplateFields
tplUsernames := r.Form["tpl_username"]
tplPasswords := r.Form["tpl_password"]
tplPublicKeys := r.Form["tpl_public_keys"]
users := make(map[string]bool)
for idx, username := range tplUsernames {
username = strings.TrimSpace(username)
password := ""
publicKey := ""
if len(tplPasswords) > idx {
password = strings.TrimSpace(tplPasswords[idx])
}
if len(tplPublicKeys) > idx {
publicKey = strings.TrimSpace(tplPublicKeys[idx])
}
if username == "" {
continue
}
if _, ok := users[username]; ok {
continue
}
users[username] = true
res = append(res, userTemplateFields{
Username: username,
Password: password,
PublicKeys: []string{publicKey},
})
}
return res
}
func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
var virtualFolders []vfs.VirtualFolder
folderPaths := r.Form["vfolder_path"]
folderNames := r.Form["vfolder_name"]
folderQuotaSizes := r.Form["vfolder_quota_size"]
folderQuotaFiles := r.Form["vfolder_quota_files"]
for idx, p := range folderPaths {
p = strings.TrimSpace(p)
name := ""
if len(folderNames) > idx {
name = folderNames[idx]
}
if p != "" && name != "" {
vfolder := vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: name,
},
VirtualPath: p,
QuotaFiles: -1,
QuotaSize: -1,
}
if len(folderQuotaSizes) > idx {
quotaSize, err := strconv.ParseInt(strings.TrimSpace(folderQuotaSizes[idx]), 10, 64)
if err == nil {
vfolder.QuotaSize = quotaSize
}
}
if len(folderQuotaFiles) > idx {
quotaFiles, err := strconv.Atoi(strings.TrimSpace(folderQuotaFiles[idx]))
if err == nil {
vfolder.QuotaFiles = quotaFiles
}
}
virtualFolders = append(virtualFolders, vfolder)
}
}
return virtualFolders
}
func getSubDirPermissionsFromPostFields(r *http.Request) map[string][]string {
permissions := make(map[string][]string)
for k := range r.Form {
if strings.HasPrefix(k, "sub_perm_path") {
p := strings.TrimSpace(r.Form.Get(k))
if p != "" {
idx := strings.TrimPrefix(k, "sub_perm_path")
permissions[p] = r.Form[fmt.Sprintf("sub_perm_permissions%v", idx)]
}
}
}
return permissions
}
func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
permissions := getSubDirPermissionsFromPostFields(r)
permissions["/"] = r.Form["permissions"]
return permissions
}
func getDataTransferLimitsFromPostFields(r *http.Request) ([]sdk.DataTransferLimit, error) {
var result []sdk.DataTransferLimit
for k := range r.Form {
if strings.HasPrefix(k, "data_transfer_limit_sources") {
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
if len(sources) > 0 {
dtLimit := sdk.DataTransferLimit{
Sources: sources,
}
idx := strings.TrimPrefix(k, "data_transfer_limit_sources")
ul := r.Form.Get(fmt.Sprintf("upload_data_transfer_source%v", idx))
dl := r.Form.Get(fmt.Sprintf("download_data_transfer_source%v", idx))
total := r.Form.Get(fmt.Sprintf("total_data_transfer_source%v", idx))
if ul != "" {
dataUL, err := strconv.ParseInt(ul, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid upload_data_transfer_source%v %#v: %w", idx, ul, err)
}
dtLimit.UploadDataTransfer = dataUL
}
if dl != "" {
dataDL, err := strconv.ParseInt(dl, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid download_data_transfer_source%v %#v: %w", idx, dl, err)
}
dtLimit.DownloadDataTransfer = dataDL
}
if total != "" {
dataTotal, err := strconv.ParseInt(total, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid total_data_transfer_source%v %#v: %w", idx, total, err)
}
dtLimit.TotalDataTransfer = dataTotal
}
result = append(result, dtLimit)
}
}
}
return result, nil
}
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
var result []sdk.BandwidthLimit
for k := range r.Form {
if strings.HasPrefix(k, "bandwidth_limit_sources") {
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
if len(sources) > 0 {
bwLimit := sdk.BandwidthLimit{
Sources: sources,
}
idx := strings.TrimPrefix(k, "bandwidth_limit_sources")
ul := r.Form.Get(fmt.Sprintf("upload_bandwidth_source%v", idx))
dl := r.Form.Get(fmt.Sprintf("download_bandwidth_source%v", idx))
if ul != "" {
bandwidthUL, err := strconv.ParseInt(ul, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid upload_bandwidth_source%v %#v: %w", idx, ul, err)
}
bwLimit.UploadBandwidth = bandwidthUL
}
if dl != "" {
bandwidthDL, err := strconv.ParseInt(dl, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid download_bandwidth_source%v %#v: %w", idx, ul, err)
}
bwLimit.DownloadBandwidth = bandwidthDL
}
result = append(result, bwLimit)
}
}
}
return result, nil
}
func getPatterDenyPolicyFromString(policy string) int {
denyPolicy := sdk.DenyPolicyDefault
if policy == "1" {
denyPolicy = sdk.DenyPolicyHide
}
return denyPolicy
}
func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
var result []sdk.PatternsFilter
allowedPatterns := make(map[string][]string)
deniedPatterns := make(map[string][]string)
patternPolicies := make(map[string]string)
for k := range r.Form {
if strings.HasPrefix(k, "pattern_path") {
p := strings.TrimSpace(r.Form.Get(k))
idx := strings.TrimPrefix(k, "pattern_path")
filters := strings.TrimSpace(r.Form.Get(fmt.Sprintf("patterns%v", idx)))
filters = strings.ReplaceAll(filters, " ", "")
patternType := r.Form.Get(fmt.Sprintf("pattern_type%v", idx))
patternPolicy := r.Form.Get(fmt.Sprintf("pattern_policy%v", idx))
if p != "" && filters != "" {
if patternType == "allowed" {
allowedPatterns[p] = append(allowedPatterns[p], strings.Split(filters, ",")...)
} else {
deniedPatterns[p] = append(deniedPatterns[p], strings.Split(filters, ",")...)
}
if patternPolicy != "" && patternPolicy != "0" {
patternPolicies[p] = patternPolicy
}
}
}
}
for dirAllowed, allowPatterns := range allowedPatterns {
filter := sdk.PatternsFilter{
Path: dirAllowed,
AllowedPatterns: allowPatterns,
DenyPolicy: getPatterDenyPolicyFromString(patternPolicies[dirAllowed]),
}
for dirDenied, denPatterns := range deniedPatterns {
if dirAllowed == dirDenied {
filter.DeniedPatterns = denPatterns
break
}
}
result = append(result, filter)
}
for dirDenied, denPatterns := range deniedPatterns {
found := false
for _, res := range result {
if res.Path == dirDenied {
found = true
break
}
}
if !found {
result = append(result, sdk.PatternsFilter{
Path: dirDenied,
DeniedPatterns: denPatterns,
DenyPolicy: getPatterDenyPolicyFromString(patternPolicies[dirDenied]),
})
}
}
return result
}
func getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {
var groups []sdk.GroupMapping
primaryGroup := r.Form.Get("primary_group")
if primaryGroup != "" {
groups = append(groups, sdk.GroupMapping{
Name: primaryGroup,
Type: sdk.GroupTypePrimary,
})
}
secondaryGroups := r.Form["secondary_groups"]
for _, name := range secondaryGroups {
groups = append(groups, sdk.GroupMapping{
Name: name,
Type: sdk.GroupTypeSecondary,
})
}
return groups
}
func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) {
var filters sdk.BaseUserFilters
bwLimits, err := getBandwidthLimitsFromPostFields(r)
if err != nil {
return filters, err
}
dtLimits, err := getDataTransferLimitsFromPostFields(r)
if err != nil {
return filters, err
}
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
if err != nil {
return filters, fmt.Errorf("invalid max upload file size: %w", err)
}
if r.Form.Get("ftp_security") == "1" {
filters.FTPSecurity = 1
}
filters.BandwidthLimits = bwLimits
filters.DataTransferLimits = dtLimits
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["denied_login_methods"]
filters.DeniedProtocols = r.Form["denied_protocols"]
filters.TwoFactorAuthProtocols = r.Form["required_two_factor_protocols"]
filters.FilePatterns = getFilePatternsFromPostField(r)
filters.TLSUsername = sdk.TLSUsername(r.Form.Get("tls_username"))
filters.WebClient = r.Form["web_client_options"]
hooks := r.Form["hooks"]
if util.Contains(hooks, "external_auth_disabled") {
filters.Hooks.ExternalAuthDisabled = true
}
if util.Contains(hooks, "pre_login_disabled") {
filters.Hooks.PreLoginDisabled = true
}
if util.Contains(hooks, "check_password_disabled") {
filters.Hooks.CheckPasswordDisabled = true
}
filters.DisableFsChecks = r.Form.Get("disable_fs_checks") != ""
filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
filters.StartDirectory = r.Form.Get("start_directory")
filters.MaxUploadFileSize = maxFileSize
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
if err != nil {
return filters, fmt.Errorf("invalid external auth cache time: %w", err)
}
return filters, nil
}
func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
secret := kms.NewPlainSecret(r.Form.Get(field))
if strings.TrimSpace(secret.GetPayload()) == redactedSecret {
secret.SetStatus(sdkkms.SecretStatusRedacted)
}
if strings.TrimSpace(secret.GetPayload()) == "" {
secret.SetStatus("")
}
return secret
}
func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
var err error
config := vfs.S3FsConfig{}
config.Bucket = r.Form.Get("s3_bucket")
config.Region = r.Form.Get("s3_region")
config.AccessKey = r.Form.Get("s3_access_key")
config.RoleARN = r.Form.Get("s3_role_arn")
config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
config.Endpoint = r.Form.Get("s3_endpoint")
config.StorageClass = r.Form.Get("s3_storage_class")
config.ACL = r.Form.Get("s3_acl")
config.KeyPrefix = r.Form.Get("s3_key_prefix")
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid s3 upload part size: %w", err)
}
config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
if err != nil {
return config, fmt.Errorf("invalid s3 upload concurrency: %w", err)
}
config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("s3_download_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid s3 download part size: %w", err)
}
config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("s3_download_concurrency"))
if err != nil {
return config, fmt.Errorf("invalid s3 download concurrency: %w", err)
}
config.ForcePathStyle = r.Form.Get("s3_force_path_style") != ""
config.DownloadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_download_part_max_time"))
if err != nil {
return config, fmt.Errorf("invalid s3 download part max time: %w", err)
}
config.UploadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_upload_part_max_time"))
if err != nil {
return config, fmt.Errorf("invalid s3 upload part max time: %w", err)
}
return config, nil
}
func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
var err error
config := vfs.GCSFsConfig{}
config.Bucket = r.Form.Get("gcs_bucket")
config.StorageClass = r.Form.Get("gcs_storage_class")
config.ACL = r.Form.Get("gcs_acl")
config.KeyPrefix = r.Form.Get("gcs_key_prefix")
autoCredentials := r.Form.Get("gcs_auto_credentials")
if autoCredentials != "" {
config.AutomaticCredentials = 1
} else {
config.AutomaticCredentials = 0
}
credentials, _, err := r.FormFile("gcs_credential_file")
if err == http.ErrMissingFile {
return config, nil
}
if err != nil {
return config, err
}
defer credentials.Close()
fileBytes, err := io.ReadAll(credentials)
if err != nil || len(fileBytes) == 0 {
if len(fileBytes) == 0 {
err = errors.New("credentials file size must be greater than 0")
}
return config, err
}
config.Credentials = kms.NewPlainSecret(string(fileBytes))
config.AutomaticCredentials = 0
return config, err
}
func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
var err error
config := vfs.SFTPFsConfig{}
config.Endpoint = r.Form.Get("sftp_endpoint")
config.Username = r.Form.Get("sftp_username")
config.Password = getSecretFromFormField(r, "sftp_password")
config.PrivateKey = getSecretFromFormField(r, "sftp_private_key")
config.KeyPassphrase = getSecretFromFormField(r, "sftp_key_passphrase")
fingerprintsFormValue := r.Form.Get("sftp_fingerprints")
config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n")
config.Prefix = r.Form.Get("sftp_prefix")
config.DisableCouncurrentReads = r.Form.Get("sftp_disable_concurrent_reads") != ""
config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
}
return config, nil
}
func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
config := vfs.HTTPFsConfig{}
config.Endpoint = r.Form.Get("http_endpoint")
config.Username = r.Form.Get("http_username")
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
config.Password = getSecretFromFormField(r, "http_password")
config.APIKey = getSecretFromFormField(r, "http_api_key")
return config
}
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
var err error
config := vfs.AzBlobFsConfig{}
config.Container = r.Form.Get("az_container")
config.AccountName = r.Form.Get("az_account_name")
config.AccountKey = getSecretFromFormField(r, "az_account_key")
config.SASURL = getSecretFromFormField(r, "az_sas_url")
config.Endpoint = r.Form.Get("az_endpoint")
config.KeyPrefix = r.Form.Get("az_key_prefix")
config.AccessTier = r.Form.Get("az_access_tier")
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid azure upload part size: %w", err)
}
config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("az_upload_concurrency"))
if err != nil {
return config, fmt.Errorf("invalid azure upload concurrency: %w", err)
}
config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("az_download_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid azure download part size: %w", err)
}
config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("az_download_concurrency"))
if err != nil {
return config, fmt.Errorf("invalid azure download concurrency: %w", err)
}
return config, nil
}
func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
var fs vfs.Filesystem
fs.Provider = sdk.GetProviderByName(r.Form.Get("fs_provider"))
switch fs.Provider {
case sdk.S3FilesystemProvider:
config, err := getS3Config(r)
if err != nil {
return fs, err
}
fs.S3Config = config
case sdk.AzureBlobFilesystemProvider:
config, err := getAzureConfig(r)
if err != nil {
return fs, err
}
fs.AzBlobConfig = config
case sdk.GCSFilesystemProvider:
config, err := getGCSConfig(r)
if err != nil {
return fs, err
}
fs.GCSConfig = config
case sdk.CryptedFilesystemProvider:
fs.CryptConfig.Passphrase = getSecretFromFormField(r, "crypt_passphrase")
case sdk.SFTPFilesystemProvider:
config, err := getSFTPConfig(r)
if err != nil {
return fs, err
}
fs.SFTPConfig = config
case sdk.HTTPFilesystemProvider:
fs.HTTPConfig = getHTTPFsConfig(r)
}
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, fmt.Errorf("invalid status: %w", 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.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.AdditionalInfo = r.Form.Get("additional_info")
admin.Description = r.Form.Get("description")
return admin, nil
}
func replacePlaceholders(field string, replacements map[string]string) string {
for k, v := range replacements {
field = strings.ReplaceAll(field, k, v)
}
return field
}
func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVirtualFolder {
folder.Name = name
replacements := make(map[string]string)
replacements["%name%"] = folder.Name
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
folder.Description = replacePlaceholders(folder.Description, replacements)
switch folder.FsConfig.Provider {
case sdk.CryptedFilesystemProvider:
folder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements)
case sdk.S3FilesystemProvider:
folder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements)
case sdk.GCSFilesystemProvider:
folder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements)
case sdk.AzureBlobFilesystemProvider:
folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
case sdk.SFTPFilesystemProvider:
folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
case sdk.HTTPFilesystemProvider:
folder.FsConfig.HTTPConfig = getHTTPFsFromTemplate(folder.FsConfig.HTTPConfig, replacements)
}
return folder
}
func getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {
if fsConfig.Passphrase != nil {
if fsConfig.Passphrase.IsPlain() {
payload := replacePlaceholders(fsConfig.Passphrase.GetPayload(), replacements)
fsConfig.Passphrase = kms.NewPlainSecret(payload)
}
}
return fsConfig
}
func getS3FsFromTemplate(fsConfig vfs.S3FsConfig, replacements map[string]string) vfs.S3FsConfig {
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
fsConfig.AccessKey = replacePlaceholders(fsConfig.AccessKey, replacements)
if fsConfig.AccessSecret != nil && fsConfig.AccessSecret.IsPlain() {
payload := replacePlaceholders(fsConfig.AccessSecret.GetPayload(), replacements)
fsConfig.AccessSecret = kms.NewPlainSecret(payload)
}
return fsConfig
}
func getGCSFsFromTemplate(fsConfig vfs.GCSFsConfig, replacements map[string]string) vfs.GCSFsConfig {
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
return fsConfig
}
func getAzBlobFsFromTemplate(fsConfig vfs.AzBlobFsConfig, replacements map[string]string) vfs.AzBlobFsConfig {
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
fsConfig.AccountName = replacePlaceholders(fsConfig.AccountName, replacements)
if fsConfig.AccountKey != nil && fsConfig.AccountKey.IsPlain() {
payload := replacePlaceholders(fsConfig.AccountKey.GetPayload(), replacements)
fsConfig.AccountKey = kms.NewPlainSecret(payload)
}
return fsConfig
}
func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]string) vfs.SFTPFsConfig {
fsConfig.Prefix = replacePlaceholders(fsConfig.Prefix, replacements)
fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
if fsConfig.Password != nil && fsConfig.Password.IsPlain() {
payload := replacePlaceholders(fsConfig.Password.GetPayload(), replacements)
fsConfig.Password = kms.NewPlainSecret(payload)
}
return fsConfig
}
func getHTTPFsFromTemplate(fsConfig vfs.HTTPFsConfig, replacements map[string]string) vfs.HTTPFsConfig {
fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
return fsConfig
}
func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
user.Username = template.Username
user.Password = template.Password
user.PublicKeys = template.PublicKeys
replacements := make(map[string]string)
replacements["%username%"] = user.Username
if user.Password != "" && !user.IsPasswordHashed() {
user.Password = replacePlaceholders(user.Password, replacements)
replacements["%password%"] = user.Password
}
user.HomeDir = replacePlaceholders(user.HomeDir, replacements)
var vfolders []vfs.VirtualFolder
for _, vfolder := range user.VirtualFolders {
vfolder.Name = replacePlaceholders(vfolder.Name, replacements)
vfolder.VirtualPath = replacePlaceholders(vfolder.VirtualPath, replacements)
vfolders = append(vfolders, vfolder)
}
user.VirtualFolders = vfolders
user.Description = replacePlaceholders(user.Description, replacements)
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
switch user.FsConfig.Provider {
case sdk.CryptedFilesystemProvider:
user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)
case sdk.S3FilesystemProvider:
user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)
case sdk.GCSFilesystemProvider:
user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)
case sdk.AzureBlobFilesystemProvider:
user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
case sdk.SFTPFilesystemProvider:
user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
case sdk.HTTPFilesystemProvider:
user.FsConfig.HTTPConfig = getHTTPFsFromTemplate(user.FsConfig.HTTPConfig, replacements)
}
return user
}
func getTransferLimits(r *http.Request) (int64, int64, int64, error) {
dataTransferUL, err := strconv.ParseInt(r.Form.Get("upload_data_transfer"), 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid upload data transfer: %w", err)
}
dataTransferDL, err := strconv.ParseInt(r.Form.Get("download_data_transfer"), 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid download data transfer: %w", err)
}
dataTransferTotal, err := strconv.ParseInt(r.Form.Get("total_data_transfer"), 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid total data transfer: %w", err)
}
return dataTransferUL, dataTransferDL, dataTransferTotal, nil
}
func getQuotaLimits(r *http.Request) (int64, int, error) {
quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("invalid quota size: %w", err)
}
quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
if err != nil {
return 0, 0, fmt.Errorf("invalid quota files: %w", err)
}
return quotaSize, quotaFiles, nil
}
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
user := dataprovider.User{}
err := r.ParseMultipartForm(maxRequestSize)
if err != nil {
return user, err
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
uid, err := strconv.Atoi(r.Form.Get("uid"))
if err != nil {
return user, fmt.Errorf("invalid uid: %w", err)
}
gid, err := strconv.Atoi(r.Form.Get("gid"))
if err != nil {
return user, fmt.Errorf("invalid uid: %w", err)
}
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
if err != nil {
return user, fmt.Errorf("invalid max sessions: %w", err)
}
quotaSize, quotaFiles, err := getQuotaLimits(r)
if err != nil {
return user, err
}
bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
if err != nil {
return user, fmt.Errorf("invalid upload bandwidth: %w", err)
}
bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
if err != nil {
return user, fmt.Errorf("invalid download bandwidth: %w", err)
}
dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
if err != nil {
return user, err
}
status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil {
return user, fmt.Errorf("invalid status: %w", err)
}
expirationDateMillis := int64(0)
expirationDateString := r.Form.Get("expiration_date")
if strings.TrimSpace(expirationDateString) != "" {
expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)
if err != nil {
return user, err
}
expirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate)
}
fsConfig, err := getFsConfigFromPostFields(r)
if err != nil {
return user, err
}
filters, err := getFiltersFromUserPostFields(r)
if err != nil {
return user, err
}
user = dataprovider.User{
BaseUser: sdk.BaseUser{
Username: r.Form.Get("username"),
Email: r.Form.Get("email"),
Password: r.Form.Get("password"),
PublicKeys: r.Form["public_keys"],
HomeDir: r.Form.Get("home_dir"),
UID: uid,
GID: gid,
Permissions: getUserPermissionsFromPostFields(r),
MaxSessions: maxSessions,
QuotaSize: quotaSize,
QuotaFiles: quotaFiles,
UploadBandwidth: bandwidthUL,
DownloadBandwidth: bandwidthDL,
UploadDataTransfer: dataTransferUL,
DownloadDataTransfer: dataTransferDL,
TotalDataTransfer: dataTransferTotal,
Status: status,
ExpirationDate: expirationDateMillis,
AdditionalInfo: r.Form.Get("additional_info"),
Description: r.Form.Get("description"),
},
Filters: dataprovider.UserFilters{
BaseUserFilters: filters,
},
VirtualFolders: getVirtualFoldersFromPostFields(r),
FsConfig: fsConfig,
Groups: getGroupsFromUserPostFields(r),
}
return user, nil
}
func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
group := dataprovider.Group{}
err := r.ParseMultipartForm(maxRequestSize)
if err != nil {
return group, err
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
if err != nil {
return group, fmt.Errorf("invalid max sessions: %w", err)
}
quotaSize, quotaFiles, err := getQuotaLimits(r)
if err != nil {
return group, err
}
bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
if err != nil {
return group, fmt.Errorf("invalid upload bandwidth: %w", err)
}
bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
if err != nil {
return group, fmt.Errorf("invalid download bandwidth: %w", err)
}
dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
if err != nil {
return group, err
}
fsConfig, err := getFsConfigFromPostFields(r)
if err != nil {
return group, err
}
filters, err := getFiltersFromUserPostFields(r)
if err != nil {
return group, err
}
group = dataprovider.Group{
BaseGroup: sdk.BaseGroup{
Name: r.Form.Get("name"),
Description: r.Form.Get("description"),
},
UserSettings: dataprovider.GroupUserSettings{
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
HomeDir: r.Form.Get("home_dir"),
MaxSessions: maxSessions,
QuotaSize: quotaSize,
QuotaFiles: quotaFiles,
Permissions: getSubDirPermissionsFromPostFields(r),
UploadBandwidth: bandwidthUL,
DownloadBandwidth: bandwidthDL,
UploadDataTransfer: dataTransferUL,
DownloadDataTransfer: dataTransferDL,
TotalDataTransfer: dataTransferTotal,
Filters: filters,
},
FsConfig: fsConfig,
},
VirtualFolders: getVirtualFoldersFromPostFields(r),
}
return group, nil
}
func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.KeyValue {
var res []dataprovider.KeyValue
for k := range r.Form {
if strings.HasPrefix(k, key) {
formKey := r.Form.Get(k)
idx := strings.TrimPrefix(k, key)
formVal := r.Form.Get(fmt.Sprintf("%s%s", val, idx))
if formKey != "" && formVal != "" {
res = append(res, dataprovider.KeyValue{
Key: formKey,
Value: formVal,
})
}
}
}
return res
}
func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEventActionOptions, error) {
httpTimeout, err := strconv.Atoi(r.Form.Get("http_timeout"))
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid http timeout: %w", err)
}
cmdTimeout, err := strconv.Atoi(r.Form.Get("cmd_timeout"))
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid command timeout: %w", err)
}
options := dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: r.Form.Get("http_endpoint"),
Username: r.Form.Get("http_username"),
Password: getSecretFromFormField(r, "http_password"),
Headers: getKeyValsFromPostFields(r, "http_header_key", "http_header_val"),
Timeout: httpTimeout,
SkipTLSVerify: r.Form.Get("http_skip_tls_verify") != "",
Method: r.Form.Get("http_method"),
QueryParameters: getKeyValsFromPostFields(r, "http_query_key", "http_query_val"),
Body: r.Form.Get("http_body"),
},
CmdConfig: dataprovider.EventActionCommandConfig{
Cmd: r.Form.Get("cmd_path"),
Timeout: cmdTimeout,
EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"),
},
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: strings.Split(strings.ReplaceAll(r.Form.Get("email_recipients"), " ", ""), ","),
Subject: r.Form.Get("email_subject"),
Body: r.Form.Get("email_body"),
},
}
return options, nil
}
func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) {
err := r.ParseForm()
if err != nil {
return dataprovider.BaseEventAction{}, err
}
actionType, err := strconv.Atoi(r.Form.Get("type"))
if err != nil {
return dataprovider.BaseEventAction{}, fmt.Errorf("invalid action type: %w", err)
}
options, err := getEventActionOptionsFromPostFields(r)
if err != nil {
return dataprovider.BaseEventAction{}, err
}
action := dataprovider.BaseEventAction{
Name: r.Form.Get("name"),
Description: r.Form.Get("description"),
Type: actionType,
Options: options,
}
return action, nil
}
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
var schedules []dataprovider.Schedule
var names, fsPaths []dataprovider.ConditionPattern
for k := range r.Form {
if strings.HasPrefix(k, "schedule_hour") {
hour := r.Form.Get(k)
if hour != "" {
idx := strings.TrimPrefix(k, "schedule_hour")
dayOfWeek := r.Form.Get(fmt.Sprintf("schedule_day_of_week%s", idx))
dayOfMonth := r.Form.Get(fmt.Sprintf("schedule_day_of_month%s", idx))
month := r.Form.Get(fmt.Sprintf("schedule_month%s", idx))
schedules = append(schedules, dataprovider.Schedule{
Hours: hour,
DayOfWeek: dayOfWeek,
DayOfMonth: dayOfMonth,
Month: month,
})
}
}
if strings.HasPrefix(k, "name_pattern") {
pattern := r.Form.Get(k)
if pattern != "" {
idx := strings.TrimPrefix(k, "name_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx))
names = append(names, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == "inverse",
})
}
}
if strings.HasPrefix(k, "fs_path_pattern") {
pattern := r.Form.Get(k)
if pattern != "" {
idx := strings.TrimPrefix(k, "fs_path_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx))
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == "inverse",
})
}
}
}
minFileSize, err := strconv.ParseInt(r.Form.Get("fs_min_size"), 10, 64)
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid min file size: %w", err)
}
maxFileSize, err := strconv.ParseInt(r.Form.Get("fs_max_size"), 10, 64)
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid max file size: %w", err)
}
conditions := dataprovider.EventConditions{
FsEvents: r.Form["fs_events"],
ProviderEvents: r.Form["provider_events"],
Schedules: schedules,
Options: dataprovider.ConditionOptions{
Names: names,
FsPaths: fsPaths,
Protocols: r.Form["fs_protocols"],
ProviderObjects: r.Form["provider_objects"],
MinFileSize: minFileSize,
MaxFileSize: maxFileSize,
ConcurrentExecution: r.Form.Get("concurrent_execution") != "",
},
}
return conditions, nil
}
func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) {
var actions []dataprovider.EventAction
for k := range r.Form {
if strings.HasPrefix(k, "action_name") {
name := r.Form.Get(k)
if name != "" {
idx := strings.TrimPrefix(k, "action_name")
order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx)))
if err != nil {
return actions, fmt.Errorf("invalid order: %w", err)
}
options := r.Form[fmt.Sprintf("action_options%s", idx)]
actions = append(actions, dataprovider.EventAction{
BaseEventAction: dataprovider.BaseEventAction{
Name: name,
},
Order: order + 1,
Options: dataprovider.EventActionOptions{
IsFailureAction: util.Contains(options, "1"),
StopOnFailure: util.Contains(options, "2"),
ExecuteSync: util.Contains(options, "3"),
},
})
}
}
}
return actions, nil
}
func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
err := r.ParseForm()
if err != nil {
return dataprovider.EventRule{}, err
}
trigger, err := strconv.Atoi(r.Form.Get("trigger"))
if err != nil {
return dataprovider.EventRule{}, fmt.Errorf("invalid trigger: %w", err)
}
conditions, err := getEventRuleConditionsFromPostFields(r)
if err != nil {
return dataprovider.EventRule{}, err
}
actions, err := getEventRuleActionsFromPostFields(r)
if err != nil {
return dataprovider.EventRule{}, err
}
rule := dataprovider.EventRule{
Name: r.Form.Get("name"),
Description: r.Form.Get("description"),
Trigger: trigger,
Conditions: conditions,
Actions: actions,
}
return rule, nil
}
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() {
s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
s.renderForgotPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
}
func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err := r.ParseForm()
if err != nil {
s.renderForgotPwdPage(w, err.Error(), ipAddr)
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
err = handleForgotPassword(r, r.Form.Get("username"), true)
if err != nil {
if e, ok := err.(*util.ValidationError); ok {
s.renderForgotPwdPage(w, e.GetErrorString(), ipAddr)
return
}
s.renderForgotPwdPage(w, err.Error(), ipAddr)
return
}
http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
}
func (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
if !smtp.IsEnabled() {
s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
s.renderResetPwdPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
}
func (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderTwoFactorPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
}
func (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderTwoFactorRecoveryPage(w, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
}
func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderMFAPage(w, r)
}
func (s *httpdServer) handleWebAdminProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderProfilePage(w, r, "")
}
func (s *httpdServer) handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderChangePasswordPage(w, r, "")
}
func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
s.renderProfilePage(w, r, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderProfilePage(w, r, "Invalid token claims")
return
}
admin, err := dataprovider.AdminExists(claims.Username)
if err != nil {
s.renderProfilePage(w, r, err.Error())
return
}
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.Email = r.Form.Get("email")
admin.Description = r.Form.Get("description")
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr)
if err != nil {
s.renderProfilePage(w, r, err.Error())
return
}
s.renderMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
"Your profile has been successfully updated")
}
func (s *httpdServer) handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderMaintenancePage(w, r, "")
}
func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
err = r.ParseMultipartForm(MaxRestoreSize)
if err != nil {
s.renderMaintenancePage(w, r, err.Error())
return
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
restoreMode, err := strconv.Atoi(r.Form.Get("mode"))
if err != nil {
s.renderMaintenancePage(w, r, err.Error())
return
}
scanQuota, err := strconv.Atoi(r.Form.Get("quota"))
if err != nil {
s.renderMaintenancePage(w, r, err.Error())
return
}
backupFile, _, err := r.FormFile("backup_file")
if err != nil {
s.renderMaintenancePage(w, r, err.Error())
return
}
defer backupFile.Close()
backupContent, err := io.ReadAll(backupFile)
if err != nil || len(backupContent) == 0 {
if len(backupContent) == 0 {
err = errors.New("backup file size must be greater than 0")
}
s.renderMaintenancePage(w, r, err.Error())
return
}
if err := restoreBackup(backupContent, "", scanQuota, restoreMode, claims.Username, ipAddr); err != nil {
s.renderMaintenancePage(w, r, err.Error())
return
}
s.renderMessagePage(w, r, "Data restored", "", http.StatusOK, nil, "Your backup was successfully restored")
}
func (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
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 {
s.renderInternalServerErrorPage(w, r, err)
return
}
admins = append(admins, a...)
if len(a) < limit {
break
}
}
data := adminsPage{
basePage: s.getBasePageData(pageAdminsTitle, webAdminsPath, r),
Admins: admins,
}
renderAdminTemplate(w, templateAdmins, data)
}
func (s *httpdServer) handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
if dataprovider.HasAdmin() {
http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
return
}
s.renderAdminSetupPage(w, r, "", "")
}
func (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
admin := &dataprovider.Admin{
Status: 1,
Permissions: []string{dataprovider.PermAdminAny},
}
s.renderAddUpdateAdminPage(w, r, admin, "", true)
}
func (s *httpdServer) handleWebUpdateAdminGet(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 {
s.renderAddUpdateAdminPage(w, r, &admin, "", false)
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
admin, err := getAdminFromPostFields(r)
if err != nil {
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
return
}
if admin.Password == "" && s.binding.isWebAdminLoginFormDisabled() {
admin.Password = util.GenerateUniqueID()
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
err = dataprovider.AddAdmin(&admin, claims.Username, ipAddr)
if err != nil {
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
return
}
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}
func (s *httpdServer) 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.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedAdmin, err := getAdminFromPostFields(r)
if err != nil {
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedAdmin.ID = admin.ID
updatedAdmin.Username = admin.Username
if updatedAdmin.Password == "" {
updatedAdmin.Password = admin.Password
}
updatedAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig
updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "Invalid token claims", false)
return
}
if username == claims.Username {
if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false)
return
}
if updatedAdmin.Status == 0 {
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
return
}
}
err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr)
if err != nil {
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
return
}
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
data := defenderHostsPage{
basePage: s.getBasePageData(pageDefenderTitle, webDefenderPath, r),
DefenderHostsURL: webDefenderHostsPath,
}
renderAdminTemplate(w, templateDefender, data)
}
func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var limit int
if _, ok := r.URL.Query()["qlimit"]; ok {
var err error
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
if err != nil {
limit = defaultQueryLimit
}
} else {
limit = defaultQueryLimit
}
users := make([]dataprovider.User, 0, limit)
for {
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
users = append(users, u...)
if len(u) < limit {
break
}
}
data := usersPage{
basePage: s.getBasePageData(pageUsersTitle, webUsersPath, r),
Users: users,
}
renderAdminTemplate(w, templateUsers, data)
}
func (s *httpdServer) handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if r.URL.Query().Get("from") != "" {
name := r.URL.Query().Get("from")
folder, err := dataprovider.GetFolderByName(name)
if err == nil {
folder.FsConfig.SetEmptySecrets()
s.renderFolderPage(w, r, folder, folderPageModeTemplate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
} else {
folder := vfs.BaseVirtualFolder{}
s.renderFolderPage(w, r, folder, folderPageModeTemplate, "")
}
}
func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
templateFolder := vfs.BaseVirtualFolder{}
err = r.ParseMultipartForm(maxRequestSize)
if err != nil {
s.renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
return
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
templateFolder.MappedPath = r.Form.Get("mapped_path")
templateFolder.Description = r.Form.Get("description")
fsConfig, err := getFsConfigFromPostFields(r)
if err != nil {
s.renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
return
}
templateFolder.FsConfig = fsConfig
var dump dataprovider.BackupData
dump.Version = dataprovider.DumpVersion
foldersFields := getFoldersForTemplate(r)
for _, tmpl := range foldersFields {
f := getFolderFromTemplate(templateFolder, tmpl)
if err := dataprovider.ValidateFolder(&f); err != nil {
s.renderMessagePage(w, r, "Folder validation error", fmt.Sprintf("Error validating folder %#v", f.Name),
http.StatusBadRequest, err, "")
return
}
dump.Folders = append(dump.Folders, f)
}
if len(dump.Folders) == 0 {
s.renderMessagePage(w, r, "No folders defined", "No valid folders defined, unable to complete the requested action",
http.StatusBadRequest, nil, "")
return
}
if r.Form.Get("form_action") == "export_from_template" {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"",
len(dump.Folders)))
render.JSON(w, r, dump)
return
}
if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, ipAddr); err != nil {
s.renderMessagePage(w, r, "Unable to save folders", "Cannot save the defined folders:",
getRespStatus(err), err, "")
return
}
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if r.URL.Query().Get("from") != "" {
username := r.URL.Query().Get("from")
user, err := dataprovider.UserExists(username)
if err == nil {
user.SetEmptySecrets()
user.PublicKeys = nil
user.Email = ""
user.Description = ""
s.renderUserPage(w, r, &user, userPageModeTemplate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
} else {
user := dataprovider.User{BaseUser: sdk.BaseUser{
Status: 1,
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
}}
s.renderUserPage(w, r, &user, userPageModeTemplate, "")
}
}
func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
templateUser, err := getUserFromPostFields(r)
if err != nil {
s.renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "")
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
var dump dataprovider.BackupData
dump.Version = dataprovider.DumpVersion
userTmplFields := getUsersForTemplate(r)
for _, tmpl := range userTmplFields {
u := getUserFromTemplate(templateUser, tmpl)
if err := dataprovider.ValidateUser(&u); err != nil {
s.renderMessagePage(w, r, "User validation error", fmt.Sprintf("Error validating user %#v", u.Username),
http.StatusBadRequest, err, "")
return
}
dump.Users = append(dump.Users, u)
for _, folder := range u.VirtualFolders {
if !dump.HasFolder(folder.Name) {
dump.Folders = append(dump.Folders, folder.BaseVirtualFolder)
}
}
}
if len(dump.Users) == 0 {
s.renderMessagePage(w, r, "No users defined", "No valid users defined, unable to complete the requested action",
http.StatusBadRequest, nil, "")
return
}
if r.Form.Get("form_action") == "export_from_template" {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"",
len(dump.Users)))
render.JSON(w, r, dump)
return
}
if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, ipAddr); err != nil {
s.renderMessagePage(w, r, "Unable to save users", "Cannot save the defined users:",
getRespStatus(err), err, "")
return
}
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
user := dataprovider.User{BaseUser: sdk.BaseUser{
Status: 1,
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
}}
s.renderUserPage(w, r, &user, userPageModeAdd, "")
}
func (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
username := getURLParam(r, "username")
user, err := dataprovider.UserExists(username)
if err == nil {
s.renderUserPage(w, r, &user, userPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
user, err := getUserFromPostFields(r)
if err != nil {
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
user = getUserFromTemplate(user, userTemplateFields{
Username: user.Username,
Password: user.Password,
PublicKeys: user.PublicKeys,
})
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
if err != nil {
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
username := getURLParam(r, "username")
user, err := dataprovider.UserExists(username)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedUser, err := getUserFromPostFields(r)
if err != nil {
s.renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedUser.ID = user.ID
updatedUser.Username = user.Username
updatedUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes
updatedUser.Filters.TOTPConfig = user.Filters.TOTPConfig
updatedUser.SetEmptySecretsIfNil()
if updatedUser.Password == redactedSecret {
updatedUser.Password = user.Password
}
updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase,
user.FsConfig.HTTPConfig.Password, user.FsConfig.HTTPConfig.APIKey)
updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{
Username: updatedUser.Username,
Password: updatedUser.Password,
PublicKeys: updatedUser.PublicKeys,
})
err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
if err != nil {
s.renderUserPage(w, r, &updatedUser, userPageModeUpdate, err.Error())
return
}
if r.Form.Get("disconnect") != "" {
disconnectUser(user.Username)
}
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
data := statusPage{
basePage: s.getBasePageData(pageStatusTitle, webStatusPath, r),
Status: getServicesStatus(),
}
renderAdminTemplate(w, templateStatus, data)
}
func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
connectionStats := common.Connections.GetStats()
data := connectionsPage{
basePage: s.getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
Connections: connectionStats,
}
renderAdminTemplate(w, templateConnections, data)
}
func (s *httpdServer) handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, "")
}
func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
folder := vfs.BaseVirtualFolder{}
err = r.ParseMultipartForm(maxRequestSize)
if err != nil {
s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
return
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
folder.MappedPath = r.Form.Get("mapped_path")
folder.Name = r.Form.Get("name")
folder.Description = r.Form.Get("description")
fsConfig, err := getFsConfigFromPostFields(r)
if err != nil {
s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
return
}
folder.FsConfig = fsConfig
folder = getFolderFromTemplate(folder, folder.Name)
err = dataprovider.AddFolder(&folder, claims.Username, ipAddr)
if err == nil {
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
} else {
s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
}
}
func (s *httpdServer) handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
folder, err := dataprovider.GetFolderByName(name)
if err == nil {
s.renderFolderPage(w, r, folder, folderPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
folder, err := dataprovider.GetFolderByName(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
err = r.ParseMultipartForm(maxRequestSize)
if err != nil {
s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
return
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
fsConfig, err := getFsConfigFromPostFields(r)
if err != nil {
s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
return
}
updatedFolder := vfs.BaseVirtualFolder{
MappedPath: r.Form.Get("mapped_path"),
Description: r.Form.Get("description"),
}
updatedFolder.ID = folder.ID
updatedFolder.Name = folder.Name
updatedFolder.FsConfig = fsConfig
updatedFolder.FsConfig.SetEmptySecretsIfNil()
updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey,
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase,
folder.FsConfig.HTTPConfig.Password, folder.FsConfig.HTTPConfig.APIKey)
updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)
err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username, ipAddr)
if err != nil {
s.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
}
func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, limit)
for {
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return folders, err
}
folders = append(folders, f...)
if len(f) < limit {
break
}
}
return folders, nil
}
func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
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
}
}
folders, err := s.getWebVirtualFolders(w, r, limit, false)
if err != nil {
return
}
data := foldersPage{
basePage: s.getBasePageData(pageFoldersTitle, webFoldersPath, r),
Folders: folders,
}
renderAdminTemplate(w, templateFolders, data)
}
func (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Group, error) {
groups := make([]dataprovider.Group, 0, limit)
for {
f, err := dataprovider.GetGroups(limit, len(groups), dataprovider.OrderASC, minimal)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return groups, err
}
groups = append(groups, f...)
if len(f) < limit {
break
}
}
return groups, nil
}
func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
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
}
}
groups, err := s.getWebGroups(w, r, limit, false)
if err != nil {
return
}
data := groupsPage{
basePage: s.getBasePageData(pageGroupsTitle, webGroupsPath, r),
Groups: groups,
}
renderAdminTemplate(w, templateGroups, data)
}
func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, "")
}
func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
group, err := getGroupFromPostFields(r)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
err = dataprovider.AddGroup(&group, claims.Username, ipAddr)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
group, err := dataprovider.GroupExists(name)
if err == nil {
s.renderGroupPage(w, r, group, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
group, err := dataprovider.GroupExists(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedGroup, err := getGroupFromPostFields(r)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedGroup.ID = group.ID
updatedGroup.Name = group.Name
updatedGroup.SetEmptySecretsIfNil()
updateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, group.UserSettings.FsConfig.S3Config.AccessSecret,
group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL,
group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase,
group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey,
group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase, group.UserSettings.FsConfig.HTTPConfig.Password,
group.UserSettings.FsConfig.HTTPConfig.APIKey)
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr)
if err != nil {
s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
}
func (s *httpdServer) getWebEventActions(w http.ResponseWriter, r *http.Request, limit int, minimal bool,
) ([]dataprovider.BaseEventAction, error) {
actions := make([]dataprovider.BaseEventAction, 0, limit)
for {
res, err := dataprovider.GetEventActions(limit, len(actions), dataprovider.OrderASC, minimal)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return actions, err
}
actions = append(actions, res...)
if len(res) < limit {
break
}
}
return actions, nil
}
func (s *httpdServer) handleWebGetEventActions(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
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
}
}
actions, err := s.getWebEventActions(w, r, limit, false)
if err != nil {
return
}
data := eventActionsPage{
basePage: s.getBasePageData(pageEventActionsTitle, webAdminEventActionsPath, r),
Actions: actions,
}
renderAdminTemplate(w, templateEventActions, data)
}
func (s *httpdServer) handleWebAddEventActionGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
action := dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeHTTP,
}
s.renderEventActionPage(w, r, action, genericPageModeAdd, "")
}
func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
action, err := getEventActionFromPostFields(r)
if err != nil {
s.renderEventActionPage(w, r, action, genericPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
if err = dataprovider.AddEventAction(&action, claims.Username, ipAddr); err != nil {
s.renderEventActionPage(w, r, action, genericPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateEventActionGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
action, err := dataprovider.EventActionExists(name)
if err == nil {
s.renderEventActionPage(w, r, action, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
action, err := dataprovider.EventActionExists(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedAction, err := getEventActionFromPostFields(r)
if err != nil {
s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedAction.ID = action.ID
updatedAction.Name = action.Name
updatedAction.Options.SetEmptySecretsIfNil()
switch updatedAction.Type {
case dataprovider.ActionTypeHTTP:
if updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
updatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password
}
}
err = dataprovider.UpdateEventAction(&updatedAction, claims.Username, ipAddr)
if err != nil {
s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
if lim, err := strconv.Atoi(r.URL.Query().Get("qlimit")); err == nil {
limit = lim
}
}
rules := make([]dataprovider.EventRule, 0, limit)
for {
res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
rules = append(rules, res...)
if len(res) < limit {
break
}
}
data := eventRulesPage{
basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r),
Rules: rules,
}
renderAdminTemplate(w, templateEventRules, data)
}
func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
rule := dataprovider.EventRule{
Trigger: dataprovider.EventTriggerFsEvent,
}
s.renderEventRulePage(w, r, rule, genericPageModeAdd, "")
}
func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
rule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err = verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr)
if err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr); err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if err == nil {
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedRule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedRule.ID = rule.ID
updatedRule.Name = rule.Name
err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}