sftpgo/httpd/web.go
Nicola Murino 1b1c740b29 Add support for allowed/denied IP/Mask
Login can be restricted to specific ranges of IP address or to a specific IP
address.

Please apply the appropriate SQL upgrade script to add the filter field to your
database.

The filter database field will allow to add other filters without requiring a
new database migration
2019-12-30 18:37:50 +01:00

399 lines
11 KiB
Go

package httpd
import (
"fmt"
"html/template"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
)
const (
templateBase = "base.html"
templateUsers = "users.html"
templateUser = "user.html"
templateConnections = "connections.html"
templateMessage = "message.html"
pageUsersTitle = "Users"
pageConnectionsTitle = "Connections"
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."
defaultUsersQueryLimit = 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
UsersTitle string
ConnectionsTitle string
Version string
}
type usersPage struct {
basePage
Users []dataprovider.User
}
type connectionsPage struct {
basePage
Connections []sftpd.ConnectionStatus
}
type userPage struct {
basePage
IsAdd bool
User dataprovider.User
RootPerms []string
Error string
ValidPerms []string
RootDirPerms []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),
}
usersTmpl := template.Must(template.ParseFiles(usersPaths...))
userTmpl := template.Must(template.ParseFiles(userPaths...))
connectionsTmpl := template.Must(template.ParseFiles(connectionsPaths...))
messageTmpl := template.Must(template.ParseFiles(messagePath...))
templates[templateUsers] = usersTmpl
templates[templateUser] = userTmpl
templates[templateConnections] = connectionsTmpl
templates[templateMessage] = messageTmpl
}
func getBasePageData(title, currentURL string) basePage {
version := utils.GetAppVersion()
return basePage{
Title: title,
CurrentURL: currentURL,
UsersURL: webUsersPath,
UserURL: webUserPath,
APIUserURL: userPath,
APIConnectionsURL: activeConnectionsPath,
APIQuotaScanURL: quotaScanPath,
ConnectionsURL: webConnectionsPath,
UsersTitle: pageUsersTitle,
ConnectionsTitle: pageConnectionsTitle,
Version: version.GetVersionAsString(),
}
}
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, page400Title, 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,
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,
RootDirPerms: user.GetPermissionsForPath("/"),
}
renderTemplate(w, templateUser, data)
}
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.ContainsRune(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 && len(perms) > 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 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"), ",")
return filters
}
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
var user dataprovider.User
err := r.ParseForm()
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)
}
user = dataprovider.User{
Username: r.Form.Get("username"),
Password: r.Form.Get("password"),
PublicKeys: publicKeys,
HomeDir: r.Form.Get("home_dir"),
UID: uid,
GID: gid,
Permissions: getUserPermissionsFromPostFields(r),
MaxSessions: maxSessions,
QuotaSize: quotaSize,
QuotaFiles: quotaFiles,
UploadBandwidth: bandwidthUL,
DownloadBandwidth: bandwidthDL,
Status: status,
ExpirationDate: expirationDateMillis,
Filters: getFiltersFromUserPostFields(r),
}
return user, err
}
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
limit := defaultUsersQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
var err error
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
if err != nil {
limit = defaultUsersQueryLimit
}
}
var users []dataprovider.User
u, err := dataprovider.GetUsers(dataProvider, limit, 0, "ASC", "")
users = append(users, u...)
for len(u) == limit {
u, err = dataprovider.GetUsers(dataProvider, limit, len(users), "ASC", "")
if err == nil && len(u) > 0 {
users = append(users, u...)
} else {
break
}
}
if err != nil {
renderInternalServerErrorPage(w, err)
return
}
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(userID string, w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(dataProvider, 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) {
user, err := getUserFromPostFields(r)
if err != nil {
renderAddUserPage(w, user, err.Error())
return
}
err = dataprovider.AddUser(dataProvider, user)
if err == nil {
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else {
renderAddUserPage(w, user, err.Error())
}
}
func handleWebUpdateUserPost(userID string, w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(dataProvider, 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(dataProvider, updatedUser)
if err == nil {
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else {
renderUpdateUserPage(w, user, err.Error())
}
}
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
connectionStats := sftpd.GetConnectionsStats()
data := connectionsPage{
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
Connections: connectionStats,
}
renderTemplate(w, templateConnections, data)
}