2022-10-08 23:34:43 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2023-03-14 21:47:14 +01:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2022-10-08 23:34:43 +02:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
gc "github.com/patrickmn/go-cache"
|
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/internal/api"
|
2023-03-14 21:47:14 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/config"
|
2022-10-08 23:34:43 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2023-01-24 06:05:31 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/form"
|
2022-10-11 22:44:11 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/server/limiter"
|
2022-10-08 23:34:43 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
2023-03-14 21:47:14 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2022-10-08 23:34:43 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Authentication cache with an expiration time of 5 minutes.
|
|
|
|
var basicAuthExpiration = 5 * time.Minute
|
|
|
|
var basicAuthCache = gc.New(basicAuthExpiration, basicAuthExpiration)
|
|
|
|
var basicAuthMutex = sync.Mutex{}
|
|
|
|
var BasicAuthRealm = "Basic realm=\"WebDAV Authorization Required\""
|
|
|
|
|
|
|
|
// GetAuthUser returns the authenticated user if found, nil otherwise.
|
|
|
|
func GetAuthUser(key string) *entity.User {
|
|
|
|
user, valid := basicAuthCache.Get(key)
|
|
|
|
|
|
|
|
if valid && user != nil {
|
|
|
|
return user.(*entity.User)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// BasicAuth implements an HTTP request handler that adds basic authentication.
|
2023-03-14 21:47:14 +01:00
|
|
|
func BasicAuth(conf *config.Config) gin.HandlerFunc {
|
2022-10-08 23:34:43 +02:00
|
|
|
var validate = func(c *gin.Context) (name, password, key string, valid bool) {
|
|
|
|
name, password, key = GetCredentials(c)
|
|
|
|
|
|
|
|
if name == "" || password == "" {
|
|
|
|
return name, password, "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
key = fmt.Sprintf("%x", sha1.Sum([]byte(key)))
|
|
|
|
|
|
|
|
if user := GetAuthUser(key); user != nil {
|
|
|
|
c.Set(gin.AuthUserKey, user)
|
|
|
|
return name, password, key, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return name, password, key, false
|
|
|
|
}
|
|
|
|
|
|
|
|
return func(c *gin.Context) {
|
2022-10-11 22:44:11 +02:00
|
|
|
if c == nil {
|
|
|
|
return
|
|
|
|
}
|
2022-10-08 23:34:43 +02:00
|
|
|
|
2022-10-11 22:44:11 +02:00
|
|
|
name, password, key, ok := validate(c)
|
|
|
|
|
|
|
|
if ok {
|
2022-10-08 23:34:43 +02:00
|
|
|
// Already authenticated.
|
|
|
|
return
|
|
|
|
} else if key == "" {
|
|
|
|
// Incomplete credentials.
|
|
|
|
c.Header("WWW-Authenticate", BasicAuthRealm)
|
|
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-11 22:44:11 +02:00
|
|
|
// Get client IP address.
|
|
|
|
clientIp := api.ClientIP(c)
|
|
|
|
|
|
|
|
// Check limit for failed auth requests (max. 10 per minute).
|
2022-10-19 05:09:09 +02:00
|
|
|
if limiter.Login.Reject(clientIp) {
|
2022-10-11 22:44:11 +02:00
|
|
|
limiter.Abort(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-08 23:34:43 +02:00
|
|
|
basicAuthMutex.Lock()
|
|
|
|
defer basicAuthMutex.Unlock()
|
|
|
|
|
2023-01-24 06:05:31 +01:00
|
|
|
// User credentials.
|
|
|
|
f := form.Login{
|
|
|
|
UserName: name,
|
|
|
|
Password: password,
|
|
|
|
}
|
2022-10-08 23:34:43 +02:00
|
|
|
|
2023-01-24 06:05:31 +01:00
|
|
|
// Check credentials and authorization.
|
|
|
|
if user, _, err := entity.Auth(f, nil, c); err != nil {
|
|
|
|
message := err.Error()
|
2022-10-19 05:09:09 +02:00
|
|
|
limiter.Login.Reserve(clientIp)
|
2023-01-24 06:05:31 +01:00
|
|
|
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
|
|
|
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
|
|
|
} else if user == nil {
|
|
|
|
message := "account not found"
|
|
|
|
limiter.Login.Reserve(clientIp)
|
|
|
|
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
2022-10-11 22:44:11 +02:00
|
|
|
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
2022-10-13 22:11:02 +02:00
|
|
|
} else if !user.CanUseWebDAV() {
|
2022-10-08 23:34:43 +02:00
|
|
|
// Sync disabled for this account.
|
|
|
|
message := "sync disabled"
|
|
|
|
|
2023-03-14 21:47:14 +01:00
|
|
|
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
|
|
|
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
|
|
|
} else if err = os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
|
|
|
|
message := "failed to create user upload path"
|
|
|
|
|
2022-10-11 22:44:11 +02:00
|
|
|
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
|
|
|
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
2022-10-08 23:34:43 +02:00
|
|
|
} else {
|
|
|
|
// Successfully authenticated.
|
2022-10-11 22:44:11 +02:00
|
|
|
event.AuditInfo([]string{clientIp, "webdav login as %s", "succeeded"}, clean.LogQuote(name))
|
|
|
|
event.LoginInfo(clientIp, "webdav", name, api.UserAgent(c))
|
2022-10-08 23:34:43 +02:00
|
|
|
|
|
|
|
// Cache successful authentication.
|
|
|
|
basicAuthCache.SetDefault(key, user)
|
|
|
|
c.Set(gin.AuthUserKey, user)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Abort request.
|
|
|
|
c.Header("WWW-Authenticate", BasicAuthRealm)
|
|
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCredentials parses the "Authorization" header into username and password.
|
|
|
|
func GetCredentials(c *gin.Context) (name, password, raw string) {
|
|
|
|
data := c.GetHeader("Authorization")
|
|
|
|
|
|
|
|
if !strings.HasPrefix(data, "Basic ") {
|
|
|
|
return "", "", data
|
|
|
|
}
|
|
|
|
|
|
|
|
data = strings.TrimPrefix(data, "Basic ")
|
|
|
|
|
|
|
|
auth, err := base64.StdEncoding.DecodeString(data)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", "", data
|
|
|
|
}
|
|
|
|
|
|
|
|
credentials := strings.SplitN(string(auth), ":", 2)
|
|
|
|
|
|
|
|
if len(credentials) != 2 {
|
|
|
|
return "", "", data
|
|
|
|
}
|
|
|
|
|
|
|
|
return credentials[0], credentials[1], data
|
|
|
|
}
|