package httpd import ( "errors" "fmt" "html/template" "io/ioutil" "net/http" "path" "path/filepath" "strconv" "strings" "time" "github.com/go-chi/chi" "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/version" "github.com/drakkan/sftpgo/vfs" ) const ( templateBase = "base.html" templateUsers = "users.html" templateUser = "user.html" templateConnections = "connections.html" templateFolders = "folders.html" templateFolder = "folder.html" templateMessage = "message.html" pageUsersTitle = "Users" pageConnectionsTitle = "Connections" pageFoldersTitle = "Folders" page400Title = "Bad request" page404Title = "Not found" page404Body = "The page you are looking for does not exist." page500Title = "Internal Server Error" page500Body = "The server is unable to fulfill your request." defaultQueryLimit = 500 webDateTimeFormat = "2006-01-02 15:04:05" // YYYY-MM-DD HH:MM:SS ) var ( templates = make(map[string]*template.Template) ) type basePage struct { Title string CurrentURL string UsersURL string UserURL string APIUserURL string APIConnectionsURL string APIQuotaScanURL string ConnectionsURL string FoldersURL string FolderURL string APIFoldersURL string APIFolderQuotaScanURL string UsersTitle string ConnectionsTitle string FoldersTitle string Version string } type usersPage struct { basePage Users []dataprovider.User } type foldersPage struct { basePage Folders []vfs.BaseVirtualFolder } type connectionsPage struct { basePage Connections []common.ConnectionStatus } type userPage struct { basePage IsAdd bool User dataprovider.User RootPerms []string Error string ValidPerms []string ValidSSHLoginMethods []string ValidProtocols []string RootDirPerms []string } type folderPage struct { basePage Folder vfs.BaseVirtualFolder Error string } type messagePage struct { basePage Error string Success string } func loadTemplates(templatesPath string) { usersPaths := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateUsers), } userPaths := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateUser), } connectionsPaths := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateConnections), } messagePath := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateMessage), } foldersPath := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateFolders), } folderPath := []string{ filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateFolder), } usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...)) userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...)) connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...)) messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...)) foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...)) folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...)) templates[templateUsers] = usersTmpl templates[templateUser] = userTmpl templates[templateConnections] = connectionsTmpl templates[templateMessage] = messageTmpl templates[templateFolders] = foldersTmpl templates[templateFolder] = folderTmpl } func getBasePageData(title, currentURL string) basePage { return basePage{ Title: title, CurrentURL: currentURL, UsersURL: webUsersPath, UserURL: webUserPath, FoldersURL: webFoldersPath, FolderURL: webFolderPath, APIUserURL: userPath, APIConnectionsURL: activeConnectionsPath, APIQuotaScanURL: quotaScanPath, APIFoldersURL: folderPath, APIFolderQuotaScanURL: quotaScanVFolderPath, ConnectionsURL: webConnectionsPath, UsersTitle: pageUsersTitle, ConnectionsTitle: pageConnectionsTitle, FoldersTitle: pageFoldersTitle, Version: version.GetAsString(), } } func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) { err := templates[tmplName].ExecuteTemplate(w, tmplName, data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) { var errorString string if len(body) > 0 { errorString = body + " " } if err != nil { errorString += err.Error() } data := messagePage{ basePage: getBasePageData(title, ""), Error: errorString, Success: message, } w.WriteHeader(statusCode) renderTemplate(w, templateMessage, data) } func renderInternalServerErrorPage(w http.ResponseWriter, err error) { renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "") } func renderBadRequestPage(w http.ResponseWriter, err error) { renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "") } func renderNotFoundPage(w http.ResponseWriter, err error) { renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "") } func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) { data := userPage{ basePage: getBasePageData("Add a new user", webUserPath), IsAdd: true, Error: error, User: user, ValidPerms: dataprovider.ValidPerms, ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods, ValidProtocols: dataprovider.ValidProtocols, RootDirPerms: user.GetPermissionsForPath("/"), } renderTemplate(w, templateUser, data) } func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) { data := userPage{ basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)), IsAdd: false, Error: error, User: user, ValidPerms: dataprovider.ValidPerms, ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods, ValidProtocols: dataprovider.ValidProtocols, RootDirPerms: user.GetPermissionsForPath("/"), } renderTemplate(w, templateUser, data) } func renderAddFolderPage(w http.ResponseWriter, folder vfs.BaseVirtualFolder, error string) { data := folderPage{ basePage: getBasePageData("Add a new folder", webFolderPath), Error: error, Folder: folder, } renderTemplate(w, templateFolder, data) } func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder { var virtualFolders []vfs.VirtualFolder formValue := r.Form.Get("virtual_folders") for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") { if strings.Contains(cleaned, "::") { mapping := strings.Split(cleaned, "::") if len(mapping) > 1 { vfolder := vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ MappedPath: strings.TrimSpace(mapping[1]), }, VirtualPath: strings.TrimSpace(mapping[0]), QuotaFiles: -1, QuotaSize: -1, } if len(mapping) > 2 { quotaFiles, err := strconv.Atoi(strings.TrimSpace(mapping[2])) if err == nil { vfolder.QuotaFiles = quotaFiles } } if len(mapping) > 3 { quotaSize, err := strconv.ParseInt(strings.TrimSpace(mapping[3]), 10, 64) if err == nil { vfolder.QuotaSize = quotaSize } } virtualFolders = append(virtualFolders, vfolder) } } } return virtualFolders } func getUserPermissionsFromPostFields(r *http.Request) map[string][]string { permissions := make(map[string][]string) permissions["/"] = r.Form["permissions"] subDirsPermsValue := r.Form.Get("sub_dirs_permissions") for _, cleaned := range getSliceFromDelimitedValues(subDirsPermsValue, "\n") { if strings.Contains(cleaned, "::") { dirPerms := strings.Split(cleaned, "::") if len(dirPerms) > 1 { dir := dirPerms[0] dir = strings.TrimSpace(dir) perms := []string{} for _, p := range strings.Split(dirPerms[1], ",") { cleanedPerm := strings.TrimSpace(p) if len(cleanedPerm) > 0 { perms = append(perms, cleanedPerm) } } if len(dir) > 0 { permissions[dir] = perms } } } } return permissions } func getSliceFromDelimitedValues(values, delimiter string) []string { result := []string{} for _, v := range strings.Split(values, delimiter) { cleaned := strings.TrimSpace(v) if len(cleaned) > 0 { result = append(result, cleaned) } } return result } func getFileExtensionsFromPostField(value string, extesionsType int) []dataprovider.ExtensionsFilter { var result []dataprovider.ExtensionsFilter for _, cleaned := range getSliceFromDelimitedValues(value, "\n") { if strings.Contains(cleaned, "::") { dirExts := strings.Split(cleaned, "::") if len(dirExts) > 1 { dir := dirExts[0] dir = strings.TrimSpace(dir) exts := []string{} for _, e := range strings.Split(dirExts[1], ",") { cleanedExt := strings.TrimSpace(e) if len(cleanedExt) > 0 { exts = append(exts, cleanedExt) } } if len(dir) > 0 { filter := dataprovider.ExtensionsFilter{ Path: dir, } if extesionsType == 1 { filter.AllowedExtensions = exts filter.DeniedExtensions = []string{} } else { filter.DeniedExtensions = exts filter.AllowedExtensions = []string{} } result = append(result, filter) } } } } return result } func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters { var filters dataprovider.UserFilters filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",") filters.DeniedLoginMethods = r.Form["ssh_login_methods"] filters.DeniedProtocols = r.Form["denied_protocols"] allowedExtensions := getFileExtensionsFromPostField(r.Form.Get("allowed_extensions"), 1) deniedExtensions := getFileExtensionsFromPostField(r.Form.Get("denied_extensions"), 2) extensions := []dataprovider.ExtensionsFilter{} if len(allowedExtensions) > 0 && len(deniedExtensions) > 0 { for _, allowed := range allowedExtensions { for _, denied := range deniedExtensions { if path.Clean(allowed.Path) == path.Clean(denied.Path) { allowed.DeniedExtensions = append(allowed.DeniedExtensions, denied.DeniedExtensions...) } } extensions = append(extensions, allowed) } for _, denied := range deniedExtensions { found := false for _, allowed := range allowedExtensions { if path.Clean(denied.Path) == path.Clean(allowed.Path) { found = true break } } if !found { extensions = append(extensions, denied) } } } else if len(allowedExtensions) > 0 { extensions = append(extensions, allowedExtensions...) } else if len(deniedExtensions) > 0 { extensions = append(extensions, deniedExtensions...) } filters.FileExtensions = extensions return filters } func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, error) { var fs dataprovider.Filesystem provider, err := strconv.Atoi(r.Form.Get("fs_provider")) if err != nil { provider = int(dataprovider.LocalFilesystemProvider) } fs.Provider = dataprovider.FilesystemProvider(provider) if fs.Provider == dataprovider.S3FilesystemProvider { fs.S3Config.Bucket = r.Form.Get("s3_bucket") fs.S3Config.Region = r.Form.Get("s3_region") fs.S3Config.AccessKey = r.Form.Get("s3_access_key") fs.S3Config.AccessSecret = r.Form.Get("s3_access_secret") fs.S3Config.Endpoint = r.Form.Get("s3_endpoint") fs.S3Config.StorageClass = r.Form.Get("s3_storage_class") fs.S3Config.KeyPrefix = r.Form.Get("s3_key_prefix") fs.S3Config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64) if err != nil { return fs, err } fs.S3Config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency")) if err != nil { return fs, err } } else if fs.Provider == dataprovider.GCSFilesystemProvider { fs.GCSConfig.Bucket = r.Form.Get("gcs_bucket") fs.GCSConfig.StorageClass = r.Form.Get("gcs_storage_class") fs.GCSConfig.KeyPrefix = r.Form.Get("gcs_key_prefix") autoCredentials := r.Form.Get("gcs_auto_credentials") if len(autoCredentials) > 0 { fs.GCSConfig.AutomaticCredentials = 1 } else { fs.GCSConfig.AutomaticCredentials = 0 } credentials, _, err := r.FormFile("gcs_credential_file") if err == http.ErrMissingFile { return fs, nil } if err != nil { return fs, err } defer credentials.Close() fileBytes, err := ioutil.ReadAll(credentials) if err != nil || len(fileBytes) == 0 { if len(fileBytes) == 0 { err = errors.New("credentials file size must be greater than 0") } return fs, err } fs.GCSConfig.Credentials = fileBytes fs.GCSConfig.AutomaticCredentials = 0 } else if fs.Provider == dataprovider.AzureBlobFilesystemProvider { fs.AzBlobConfig.Container = r.Form.Get("az_container") fs.AzBlobConfig.AccountName = r.Form.Get("az_account_name") fs.AzBlobConfig.AccountKey = r.Form.Get("az_account_key") fs.AzBlobConfig.SASURL = r.Form.Get("az_sas_url") fs.AzBlobConfig.Endpoint = r.Form.Get("az_endpoint") fs.AzBlobConfig.KeyPrefix = r.Form.Get("az_key_prefix") fs.AzBlobConfig.AccessTier = r.Form.Get("az_access_tier") fs.AzBlobConfig.UseEmulator = len(r.Form.Get("az_use_emulator")) > 0 fs.AzBlobConfig.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64) if err != nil { return fs, err } fs.AzBlobConfig.UploadConcurrency, err = strconv.Atoi(r.Form.Get("az_upload_concurrency")) if err != nil { return fs, err } } return fs, nil } func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { var user dataprovider.User err := r.ParseMultipartForm(maxRequestSize) if err != nil { return user, err } publicKeysFormValue := r.Form.Get("public_keys") publicKeys := getSliceFromDelimitedValues(publicKeysFormValue, "\n") uid, err := strconv.Atoi(r.Form.Get("uid")) if err != nil { return user, err } gid, err := strconv.Atoi(r.Form.Get("gid")) if err != nil { return user, err } maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions")) if err != nil { return user, err } quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64) if err != nil { return user, err } quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files")) if err != nil { return user, err } bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64) if err != nil { return user, err } bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64) if err != nil { return user, err } status, err := strconv.Atoi(r.Form.Get("status")) if err != nil { return user, err } expirationDateMillis := int64(0) expirationDateString := r.Form.Get("expiration_date") if len(strings.TrimSpace(expirationDateString)) > 0 { expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString) if err != nil { return user, err } expirationDateMillis = utils.GetTimeAsMsSinceEpoch(expirationDate) } fsConfig, err := getFsConfigFromUserPostFields(r) if err != nil { return user, err } user = dataprovider.User{ Username: r.Form.Get("username"), Password: r.Form.Get("password"), PublicKeys: publicKeys, HomeDir: r.Form.Get("home_dir"), VirtualFolders: getVirtualFoldersFromPostFields(r), UID: uid, GID: gid, Permissions: getUserPermissionsFromPostFields(r), MaxSessions: maxSessions, QuotaSize: quotaSize, QuotaFiles: quotaFiles, UploadBandwidth: bandwidthUL, DownloadBandwidth: bandwidthDL, Status: status, ExpirationDate: expirationDateMillis, Filters: getFiltersFromUserPostFields(r), FsConfig: fsConfig, } maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64) user.Filters.MaxUploadFileSize = maxFileSize return user, err } func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { limit := defaultQueryLimit if _, ok := r.URL.Query()["qlimit"]; ok { var err error limit, err = strconv.Atoi(r.URL.Query().Get("qlimit")) if err != nil { limit = defaultQueryLimit } } users := make([]dataprovider.User, 0, limit) for { u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "") if err != nil { renderInternalServerErrorPage(w, err) return } users = append(users, u...) if len(u) < limit { break } } data := usersPage{ basePage: getBasePageData(pageUsersTitle, webUsersPath), Users: users, } renderTemplate(w, templateUsers, data) } func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { renderAddUserPage(w, dataprovider.User{Status: 1}, "") } func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) if err != nil { renderBadRequestPage(w, err) return } user, err := dataprovider.GetUserByID(id) if err == nil { renderUpdateUserPage(w, user, "") } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { renderNotFoundPage(w, err) } else { renderInternalServerErrorPage(w, err) } } func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) user, err := getUserFromPostFields(r) if err != nil { renderAddUserPage(w, user, err.Error()) return } err = dataprovider.AddUser(user) if err == nil { http.Redirect(w, r, webUsersPath, http.StatusSeeOther) } else { renderAddUserPage(w, user, err.Error()) } } func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) if err != nil { renderBadRequestPage(w, err) return } user, err := dataprovider.GetUserByID(id) if _, ok := err.(*dataprovider.RecordNotFoundError); ok { renderNotFoundPage(w, err) return } else if err != nil { renderInternalServerErrorPage(w, err) return } updatedUser, err := getUserFromPostFields(r) if err != nil { renderUpdateUserPage(w, user, err.Error()) return } updatedUser.ID = user.ID if len(updatedUser.Password) == 0 { updatedUser.Password = user.Password } err = dataprovider.UpdateUser(updatedUser) if err == nil { if len(r.Form.Get("disconnect")) > 0 { disconnectUser(user.Username) } http.Redirect(w, r, webUsersPath, http.StatusSeeOther) } else { renderUpdateUserPage(w, user, err.Error()) } } func handleWebGetConnections(w http.ResponseWriter, r *http.Request) { connectionStats := common.Connections.GetStats() data := connectionsPage{ basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath), Connections: connectionStats, } renderTemplate(w, templateConnections, data) } func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) { renderAddFolderPage(w, vfs.BaseVirtualFolder{}, "") } func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) folder := vfs.BaseVirtualFolder{} err := r.ParseForm() if err != nil { renderAddFolderPage(w, folder, err.Error()) return } folder.MappedPath = r.Form.Get("mapped_path") err = dataprovider.AddFolder(folder) if err == nil { http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) } else { renderAddFolderPage(w, folder, err.Error()) } } func handleWebGetFolders(w http.ResponseWriter, r *http.Request) { limit := defaultQueryLimit if _, ok := r.URL.Query()["qlimit"]; ok { var err error limit, err = strconv.Atoi(r.URL.Query().Get("qlimit")) if err != nil { limit = defaultQueryLimit } } folders := make([]vfs.BaseVirtualFolder, 0, limit) for { f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "") if err != nil { renderInternalServerErrorPage(w, err) return } folders = append(folders, f...) if len(f) < limit { break } } data := foldersPage{ basePage: getBasePageData(pageFoldersTitle, webFoldersPath), Folders: folders, } renderTemplate(w, templateFolders, data) }