Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
881bc4cb28
commit
3e924b70c7
21 changed files with 234 additions and 158 deletions
|
@ -6,14 +6,17 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// Auth checks if the user has permission to access the specified resource and returns the session if so.
|
||||
// Auth checks if the user is authorized to access a resource with the given permission
|
||||
// and returns the session or nil otherwise.
|
||||
func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.Session {
|
||||
return AuthAny(c, resource, acl.Permissions{grant})
|
||||
}
|
||||
|
||||
// AuthAny checks if at least one permission allows access and returns the session in this case.
|
||||
// AuthAny checks if the user is authorized to access a resource with any of the specified permissions
|
||||
// and returns the session or nil otherwise.
|
||||
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *entity.Session) {
|
||||
// Get the client IP and session ID from the request headers.
|
||||
ip := ClientIP(c)
|
||||
|
@ -75,3 +78,9 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
|||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// AuthToken returns the client authentication token from the request context if one was found,
|
||||
// or an empty string if no supported request header value was provided.
|
||||
func AuthToken(c *gin.Context) string {
|
||||
return header.AuthToken(c)
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
|
@ -22,7 +23,7 @@ func TestAuth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, session.PublicAuthToken)
|
||||
header.SetAuthorization(c.Request, session.PublicAuthToken)
|
||||
|
||||
// Check auth token.
|
||||
authToken := AuthToken(c)
|
||||
|
@ -56,7 +57,7 @@ func TestAuthAny(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, session.PublicAuthToken)
|
||||
header.SetAuthorization(c.Request, session.PublicAuthToken)
|
||||
|
||||
// Check auth token.
|
||||
authToken := AuthToken(c)
|
||||
|
@ -87,3 +88,52 @@ func TestAuthAny(t *testing.T) {
|
|||
assert.False(t, s.Abort(c))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
t.Run("None", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// No headers have been set, so no token should be returned.
|
||||
token := AuthToken(c)
|
||||
assert.Equal(t, "", token)
|
||||
})
|
||||
t.Run("BearerToken", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
header.SetAuthorization(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
bearerToken := header.BearerToken(c)
|
||||
assert.Equal(t, authToken, bearerToken)
|
||||
})
|
||||
t.Run("Header", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.XAuthToken, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
bearerToken := header.BearerToken(c)
|
||||
assert.Equal(t, "", bearerToken)
|
||||
})
|
||||
}
|
17
internal/api/api_request.go
Normal file
17
internal/api/api_request.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// ClientIP returns the client IP address from the request context or a placeholder if it is unknown.
|
||||
func ClientIP(c *gin.Context) (ip string) {
|
||||
return header.ClientIP(c)
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent from the request context or an empty string if it is unknown.
|
||||
func UserAgent(c *gin.Context) string {
|
||||
return header.UserAgent(c)
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// AuthToken returns the client authentication token from the request context,
|
||||
// or an empty string if none is found.
|
||||
func AuthToken(c *gin.Context) string {
|
||||
// Default is an empty string if no context or ID is set.
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// First check the "X-Auth-Token" and "X-Session-ID" headers for an auth token.
|
||||
if token := c.GetHeader(header.AuthToken); token != "" {
|
||||
return clean.ID(token)
|
||||
} else if id := c.GetHeader(header.SessionID); id != "" {
|
||||
return clean.ID(id)
|
||||
}
|
||||
|
||||
// Otherwise, the bearer token from the authorization request header is returned.
|
||||
return BearerToken(c)
|
||||
}
|
||||
|
||||
// BearerToken returns the client bearer token header value, or an empty string if none is found.
|
||||
func BearerToken(c *gin.Context) string {
|
||||
if authType, bearerToken := Authorization(c); authType == header.BearerAuth && bearerToken != "" {
|
||||
return bearerToken
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Authorization returns the authentication type and token from the authorization request header,
|
||||
// or an empty string if there is none.
|
||||
func Authorization(c *gin.Context) (authType, authToken string) {
|
||||
if c == nil {
|
||||
return "", ""
|
||||
} else if s := c.GetHeader(header.Authorization); s == "" {
|
||||
// Ignore.
|
||||
} else if t := strings.Split(s, " "); len(t) != 2 {
|
||||
// Ignore.
|
||||
} else {
|
||||
return clean.ID(t[0]), clean.ID(t[1])
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// AddRequestAuthorizationHeader adds a bearer token authorization header to a request.
|
||||
func AddRequestAuthorizationHeader(r *http.Request, authToken string) {
|
||||
if authToken != "" {
|
||||
r.Header.Add(header.Authorization, fmt.Sprintf("%s %s", header.BearerAuth, authToken))
|
||||
}
|
||||
}
|
||||
|
||||
// BasicAuth checks the basic authorization header for credentials and returns them if found.
|
||||
//
|
||||
// Note that OAuth 2.0 defines basic authentication differently than RFC 7617, however, this
|
||||
// does not matter as long as only alphanumeric characters are used for client id and secret:
|
||||
// https://www.scottbrady91.com/oauth/client-authentication#:~:text=OAuth%20Basic%20Authentication
|
||||
func BasicAuth(c *gin.Context) (username, password, cacheKey string) {
|
||||
authType, authToken := Authorization(c)
|
||||
|
||||
if authType != header.BasicAuth || authToken == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
auth, err := base64.StdEncoding.DecodeString(authToken)
|
||||
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
credentials := strings.SplitN(string(auth), ":", 2)
|
||||
|
||||
if len(credentials) != 2 {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
cacheKey = fmt.Sprintf("%x", sha1.Sum([]byte(authToken)))
|
||||
|
||||
return credentials[0], credentials[1], cacheKey
|
||||
}
|
|
@ -33,7 +33,7 @@ func AddDownloadHeader(c *gin.Context, fileName string) {
|
|||
|
||||
// AddSessionHeader adds a session id header to the response.
|
||||
func AddSessionHeader(c *gin.Context, id string) {
|
||||
c.Header(header.SessionID, id)
|
||||
c.Header(header.XSessionID, id)
|
||||
}
|
||||
|
||||
// AddContentTypeHeader adds a content type header to the response.
|
||||
|
|
|
@ -109,7 +109,7 @@ func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, pas
|
|||
Password: password,
|
||||
}))
|
||||
|
||||
authToken = r.Header().Get(header.SessionID)
|
||||
authToken = r.Header().Get(header.XSessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, pas
|
|||
func AuthenticatedRequest(r http.Handler, method, path, authToken string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
|
||||
AddRequestAuthorizationHeader(req, authToken)
|
||||
header.SetAuthorization(req, authToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
@ -131,7 +131,7 @@ func AuthenticatedRequestWithBody(r http.Handler, method, path, body string, aut
|
|||
reader := strings.NewReader(body)
|
||||
req, _ := http.NewRequest(method, path, reader)
|
||||
|
||||
AddRequestAuthorizationHeader(req, authToken)
|
||||
header.SetAuthorization(req, authToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
|
@ -39,7 +40,7 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
var f form.ClientCredentials
|
||||
|
||||
// Allow authentication with basic auth and form values.
|
||||
if clientId, clientSecret, _ := BasicAuth(c); clientId != "" && clientSecret != "" {
|
||||
if clientId, clientSecret, _ := header.BasicAuth(c); clientId != "" && clientSecret != "" {
|
||||
f.ClientID = clientId
|
||||
f.ClientSecret = clientSecret
|
||||
} else if err = c.Bind(&f); err != nil {
|
||||
|
|
|
@ -236,7 +236,7 @@ func TestDeleteOAuthToken(t *testing.T) {
|
|||
authToken := gjson.Get(createResp.Body.String(), "access_token").String()
|
||||
|
||||
deleteToken, _ := http.NewRequest("POST", logoutPath, nil)
|
||||
deleteToken.Header.Add(header.AuthToken, authToken)
|
||||
deleteToken.Header.Add(header.XAuthToken, authToken)
|
||||
|
||||
deleteResp := httptest.NewRecorder()
|
||||
app.ServeHTTP(deleteResp, deleteToken)
|
||||
|
@ -253,7 +253,7 @@ func TestDeleteOAuthToken(t *testing.T) {
|
|||
sess := entity.SessionFixtures.Get("alice_token")
|
||||
|
||||
deleteToken, _ := http.NewRequest("POST", "/api/v1/oauth/logout", nil)
|
||||
deleteToken.Header.Add(header.AuthToken, sess.AuthToken())
|
||||
deleteToken.Header.Add(header.XAuthToken, sess.AuthToken())
|
||||
|
||||
deleteResp := httptest.NewRecorder()
|
||||
app.ServeHTTP(deleteResp, deleteToken)
|
||||
|
|
|
@ -180,7 +180,7 @@ func (m *Session) SetAuthToken(authToken string) *Session {
|
|||
|
||||
// AuthTokenType returns the authentication token type.
|
||||
func (m *Session) AuthTokenType() string {
|
||||
return header.BearerAuth
|
||||
return header.AuthBearer
|
||||
}
|
||||
|
||||
// Regenerate (re-)initializes the session with a random auth token, ID, and RefID.
|
||||
|
|
|
@ -182,32 +182,32 @@ func TestSession_AuthToken(t *testing.T) {
|
|||
assert.Equal(t, "", sess.AuthToken())
|
||||
assert.False(t, rnd.IsSessionID(sess.ID))
|
||||
assert.False(t, rnd.IsAuthToken(sess.AuthToken()))
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
sess.Regenerate()
|
||||
assert.True(t, rnd.IsSessionID(sess.ID))
|
||||
assert.True(t, rnd.IsAuthToken(sess.AuthToken()))
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
sess.SetAuthToken(alice.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
sess := SessionFixtures.Get("alice")
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
})
|
||||
t.Run("Find", func(t *testing.T) {
|
||||
alice := SessionFixtures.Get("alice")
|
||||
sess := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
sess.SetAuthToken(alice.AuthToken())
|
||||
assert.Equal(t, "a3859489780243a78b331bd44f58255b552dee104041a45c0e79b610f63af2e5", sess.ID)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", sess.AuthToken())
|
||||
assert.Equal(t, header.BearerAuth, sess.AuthTokenType())
|
||||
assert.Equal(t, header.AuthBearer, sess.AuthTokenType())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// To improve performance, we use a basic auth cache
|
||||
|
@ -33,7 +34,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
|||
// Helper function that extracts the login information from the request headers.
|
||||
var basicAuth = func(c *gin.Context) (username, password, cacheKey string, authorized bool) {
|
||||
// Extract credentials from the HTTP request headers.
|
||||
username, password, cacheKey = api.BasicAuth(c)
|
||||
username, password, cacheKey = header.BasicAuth(c)
|
||||
|
||||
// Fail if the username or password is empty, as
|
||||
// this is not allowed under any circumstances.
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
@ -87,12 +88,12 @@ func WebDAV(filePath string, router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
if fs.FileExists(fileName) {
|
||||
// Flag the uploaded file as favorite if the "X-Favorite" header is set to "1".
|
||||
if r.Header.Get("X-Favorite") == "1" {
|
||||
if r.Header.Get(header.XFavorite) == "1" {
|
||||
FlagUploadAsFavorite(fileName)
|
||||
}
|
||||
|
||||
// Set the file modification time based on the Unix timestamp found in the "X-OC-MTime" header.
|
||||
if mtimeUnix := txt.Int64(r.Header.Get("X-OC-MTime")); mtimeUnix <= 0 {
|
||||
if mtimeUnix := txt.Int64(r.Header.Get(header.XModTime)); mtimeUnix <= 0 {
|
||||
// Ignore, as no Unix timestamp was provided.
|
||||
} else if mtime := time.Unix(mtimeUnix, 0); mtime.IsZero() || time.Now().Before(mtime) {
|
||||
log.Warnf("webdav: invalid modtime provided for %s", clean.Log(filepath.Base(fileName)))
|
||||
|
|
|
@ -1,9 +1,100 @@
|
|||
package header
|
||||
|
||||
const (
|
||||
SessionID = "X-Session-ID"
|
||||
AuthToken = "X-Auth-Token"
|
||||
Authorization = "Authorization" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
BasicAuth = "Basic"
|
||||
BearerAuth = "Bearer"
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
const (
|
||||
XAuthToken = "X-Auth-Token"
|
||||
XSessionID = "X-Session-ID"
|
||||
Auth = "Authorization" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
AuthBasic = "Basic"
|
||||
AuthBearer = "Bearer"
|
||||
)
|
||||
|
||||
// AuthToken returns the client authentication token from the request context,
|
||||
// or an empty string if none is found.
|
||||
func AuthToken(c *gin.Context) string {
|
||||
// Default is an empty string if no context or ID is set.
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// First check the "X-Auth-Token" and "X-Session-ID" headers for an auth token.
|
||||
if token := c.GetHeader(XAuthToken); token != "" {
|
||||
return clean.ID(token)
|
||||
} else if id := c.GetHeader(XSessionID); id != "" {
|
||||
return clean.ID(id)
|
||||
}
|
||||
|
||||
// Otherwise, the bearer token from the authorization request header is returned.
|
||||
return BearerToken(c)
|
||||
}
|
||||
|
||||
// BearerToken returns the client bearer token header value, or an empty string if none is found.
|
||||
func BearerToken(c *gin.Context) string {
|
||||
if authType, bearerToken := Authorization(c); authType == AuthBearer && bearerToken != "" {
|
||||
return bearerToken
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Authorization returns the authentication type and token from the authorization request header,
|
||||
// or an empty string if there is none.
|
||||
func Authorization(c *gin.Context) (authType, authToken string) {
|
||||
if c == nil {
|
||||
return "", ""
|
||||
} else if s := c.GetHeader(Auth); s == "" {
|
||||
// Ignore.
|
||||
} else if t := strings.Split(s, " "); len(t) != 2 {
|
||||
// Ignore.
|
||||
} else {
|
||||
return clean.ID(t[0]), clean.ID(t[1])
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// SetAuthorization adds a bearer token authorization header to a request.
|
||||
func SetAuthorization(r *http.Request, authToken string) {
|
||||
if authToken != "" {
|
||||
r.Header.Add(Auth, fmt.Sprintf("%s %s", AuthBearer, authToken))
|
||||
}
|
||||
}
|
||||
|
||||
// BasicAuth checks the basic authorization header for credentials and returns them if found.
|
||||
//
|
||||
// Note that OAuth 2.0 defines basic authentication differently than RFC 7617, however, this
|
||||
// does not matter as long as only alphanumeric characters are used for client id and secret:
|
||||
// https://www.scottbrady91.com/oauth/client-authentication#:~:text=OAuth%20Basic%20Authentication
|
||||
func BasicAuth(c *gin.Context) (username, password, cacheKey string) {
|
||||
authType, authToken := Authorization(c)
|
||||
|
||||
if authType != AuthBasic || authToken == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
auth, err := base64.StdEncoding.DecodeString(authToken)
|
||||
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
credentials := strings.SplitN(string(auth), ":", 2)
|
||||
|
||||
if len(credentials) != 2 {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
cacheKey = fmt.Sprintf("%x", sha1.Sum([]byte(authToken)))
|
||||
|
||||
return credentials[0], credentials[1], cacheKey
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -7,8 +7,6 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
|
@ -33,7 +31,7 @@ func TestAuthToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
SetAuthorization(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
|
@ -50,7 +48,7 @@ func TestAuthToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.AuthToken, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
c.Request.Header.Add(XAuthToken, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authToken := AuthToken(c)
|
||||
|
@ -82,7 +80,7 @@ func TestBearerToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
AddRequestAuthorizationHeader(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
SetAuthorization(c.Request, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
token := BearerToken(c)
|
||||
|
@ -113,11 +111,11 @@ func TestAuthorization(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.Authorization, "Bearer 69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
c.Request.Header.Add(Auth, "Bearer 69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0")
|
||||
|
||||
// Check result.
|
||||
authType, authToken := Authorization(c)
|
||||
assert.Equal(t, "Bearer", authType)
|
||||
assert.Equal(t, AuthBearer, authType)
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", authToken)
|
||||
})
|
||||
}
|
||||
|
@ -146,7 +144,7 @@ func TestBasicAuth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Add authorization header.
|
||||
c.Request.Header.Add(header.Authorization, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
|
||||
c.Request.Header.Add(Auth, AuthBasic+" QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
|
||||
|
||||
// Check result.
|
||||
user, pass, key := BasicAuth(c)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Package header provides common response header names and default values.
|
||||
Package header provides abstractions and naming constants for HTTP request and response headers.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
|
17
pkg/header/header_test.go
Normal file
17
pkg/header/header_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package header
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
t.Run("Auth", func(t *testing.T) {
|
||||
assert.Equal(t, "X-Auth-Token", XAuthToken)
|
||||
assert.Equal(t, "X-Session-ID", XSessionID)
|
||||
assert.Equal(t, "Authorization", Auth)
|
||||
assert.Equal(t, "Basic", AuthBasic)
|
||||
assert.Equal(t, "Bearer", AuthBearer)
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package header
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -11,6 +11,8 @@ func ClientIP(c *gin.Context) (ip string) {
|
|||
if c == nil {
|
||||
// Should never happen.
|
||||
return UnknownIP
|
||||
} else if c.Request == nil {
|
||||
return UnknownIP
|
||||
} else if ip = c.ClientIP(); ip != "" {
|
||||
return ip
|
||||
} else if ip = c.RemoteIP(); ip != "" {
|
||||
|
@ -26,6 +28,8 @@ func UserAgent(c *gin.Context) string {
|
|||
if c == nil {
|
||||
// Should never happen.
|
||||
return ""
|
||||
} else if c.Request == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.Request.UserAgent()
|
6
pkg/header/webdav.go
Normal file
6
pkg/header/webdav.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package header
|
||||
|
||||
const (
|
||||
XFavorite = "X-Favorite"
|
||||
XModTime = "X-OC-MTime"
|
||||
)
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
Package live provides types and abstractions for hybrid photo/video file formats.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
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.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package live
|
|
@ -21,7 +21,7 @@ Please Note:
|
|||
|
||||
## Hybrid Photo/Video Formats
|
||||
|
||||
For more information on hybrid photo/video file formats, e.g. Apple Live Photos and Samsung/Google Motion Photos, see [github.com/photoprism/photoprism/tree/develop/pkg/live](https://github.com/photoprism/photoprism/tree/develop/pkg/live) and [docs.photoprism.app/developer-guide/media/live](https://docs.photoprism.app/developer-guide/media/live/).
|
||||
For more information on hybrid photo/video file formats, e.g. Apple Live Photos and Samsung/Google Motion Photos, see [github.com/photoprism/photoprism/tree/develop/pkg/media](https://github.com/photoprism/photoprism/tree/develop/pkg/media) and [docs.photoprism.app/developer-guide/media/live](https://docs.photoprism.app/developer-guide/media/live/).
|
||||
|
||||
## Standard Resolutions
|
||||
|
||||
|
|
Loading…
Reference in a new issue