Config: Update CORS header defaults and add /api/v1/echo endpoint #3931
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
c660c729e2
commit
02a1b12edb
16 changed files with 148 additions and 28 deletions
|
@ -62,7 +62,7 @@ func AbortDeleteFailed(c *gin.Context) {
|
|||
Abort(c, http.StatusInternalServerError, i18n.ErrDeleteFailed)
|
||||
}
|
||||
|
||||
func AbortUnexpected(c *gin.Context) {
|
||||
func AbortUnexpectedError(c *gin.Context) {
|
||||
Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||
if err := a.Create(); err != nil {
|
||||
// Report unexpected error.
|
||||
log.Errorf("album: %s (create)", err)
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
@ -112,7 +112,7 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||
} else if err := a.Restore(); err != nil {
|
||||
// Report unexpected error.
|
||||
log.Errorf("album: %s (restore)", err)
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,13 +8,15 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CloseableResponseRecorder struct {
|
||||
|
@ -109,7 +111,7 @@ func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, name string, pas
|
|||
Password: password,
|
||||
}))
|
||||
|
||||
authToken = r.Header().Get(header.XAuthToken)
|
||||
authToken = gjson.Get(r.Body.String(), "access_token").String()
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ func SaveSettings(router *gin.RouterGroup) {
|
|||
user := s.User()
|
||||
|
||||
if user == nil {
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
49
internal/api/echo.go
Normal file
49
internal/api/echo.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
)
|
||||
|
||||
// Echo returns the request and response headers as JSON if debug mode is enabled.
|
||||
//
|
||||
// The supported request methods are:
|
||||
//
|
||||
// - GET
|
||||
// - POST
|
||||
// - PUT
|
||||
// - PATCH
|
||||
// - HEAD
|
||||
// - OPTIONS
|
||||
// - DELETE
|
||||
// - CONNECT
|
||||
// - TRACE
|
||||
//
|
||||
// ANY /api/v1/echo
|
||||
func Echo(router *gin.RouterGroup) {
|
||||
router.Any("/echo", func(c *gin.Context) {
|
||||
// Abort if debug mode is disabled.
|
||||
if !get.Config().Debug() {
|
||||
AbortFeatureDisabled(c)
|
||||
return
|
||||
} else if c.Request == nil || c.Writer == nil {
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Return request information.
|
||||
echoResponse := gin.H{
|
||||
"url": c.Request.URL.String(),
|
||||
"method": c.Request.Method,
|
||||
"headers": map[string]http.Header{
|
||||
"request": c.Request.Header,
|
||||
"response": c.Writer.Header(),
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, echoResponse)
|
||||
})
|
||||
}
|
63
internal/api/echo_test.go
Normal file
63
internal/api/echo_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestEcho(t *testing.T) {
|
||||
t.Run("GET", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
Echo(router)
|
||||
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
t.Logf("Auth Token: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/echo", authToken)
|
||||
t.Logf("Response Body: %s", r.Body.String())
|
||||
|
||||
body := r.Body.String()
|
||||
url := gjson.Get(body, "url").String()
|
||||
method := gjson.Get(body, "method").String()
|
||||
request := gjson.Get(body, "headers.request")
|
||||
response := gjson.Get(body, "headers.response")
|
||||
|
||||
assert.Equal(t, "/api/v1/echo", url)
|
||||
assert.Equal(t, "GET", method)
|
||||
assert.Equal(t, "Bearer "+authToken, request.Get("Authorization.0").String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", response.Get("Content-Type.0").String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("POST", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
|
||||
Echo(router)
|
||||
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
|
||||
t.Logf("Auth Token: %s", authToken)
|
||||
r := AuthenticatedRequest(app, http.MethodPost, "/api/v1/echo", authToken)
|
||||
|
||||
body := r.Body.String()
|
||||
url := gjson.Get(body, "url").String()
|
||||
method := gjson.Get(body, "method").String()
|
||||
request := gjson.Get(body, "headers.request")
|
||||
response := gjson.Get(body, "headers.response")
|
||||
|
||||
assert.Equal(t, "/api/v1/echo", url)
|
||||
assert.Equal(t, "POST", method)
|
||||
assert.Equal(t, "Bearer "+authToken, request.Get("Authorization.0").String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", response.Get("Content-Type.0").String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
|
@ -40,7 +40,7 @@ func AddPhotoLabel(router *gin.RouterGroup) {
|
|||
|
||||
var f form.Label
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
if err = c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
@ -52,8 +52,9 @@ func AddPhotoLabel(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := labelEntity.Restore(); err != nil {
|
||||
if err = labelEntity.Restore(); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "could not restore label"})
|
||||
return
|
||||
}
|
||||
|
||||
photoLabel := entity.FirstOrCreatePhotoLabel(entity.NewPhotoLabel(m.ID, labelEntity.ID, f.Uncertainty, "manual"))
|
||||
|
@ -79,7 +80,7 @@ func AddPhotoLabel(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := p.SaveLabels(); err != nil {
|
||||
if err = p.SaveLabels(); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
|
||||
if err != nil {
|
||||
log.Errorf("photo: cannot find primary file for %s (unstack)", clean.Log(baseName))
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
|
||||
if err := unstackFile.Move(destName); err != nil {
|
||||
log.Errorf("photo: cannot rename %s to %s (unstack)", clean.Log(unstackFile.BaseName()), clean.Log(filepath.Base(destName)))
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
// Reset type for existing photo stack to image.
|
||||
if err := stackPhoto.Update("PhotoType", entity.MediaImage); err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err, clean.Log(baseName))
|
||||
AbortUnexpected(c)
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -83,9 +83,6 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
event.AuditInfo([]string{clientIp, "session %s", "updated"}, sess.RefID)
|
||||
}
|
||||
|
||||
// Add auth token to response header.
|
||||
AddAuthTokenHeader(c, sess.AuthToken())
|
||||
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
|
||||
|
||||
|
|
|
@ -69,9 +69,6 @@ func GetSession(router *gin.RouterGroup) {
|
|||
// Update user information.
|
||||
sess.RefreshUser()
|
||||
|
||||
// Add auth token to response header.
|
||||
AddAuthTokenHeader(c, authToken)
|
||||
|
||||
// Response includes user data, session data, and client config values.
|
||||
response := GetSessionResponse(authToken, sess, get.Config().ClientSession(sess))
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestSession(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSessionResponse(t *testing.T) {
|
||||
func TestGetSessionResponse(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
sess := get.Session().Public()
|
||||
conf := get.Config().ClientSession(sess)
|
||||
|
|
|
@ -174,4 +174,5 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.Connect(APIv1)
|
||||
api.WebSocket(APIv1)
|
||||
api.GetMetrics(APIv1)
|
||||
api.Echo(APIv1)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
package header
|
||||
|
||||
const (
|
||||
Accept = "Accept"
|
||||
AcceptRanges = "Accept-Ranges"
|
||||
ContentType = "Content-Type"
|
||||
ContentTypeForm = "application/x-www-form-urlencoded"
|
||||
ContentTypeJson = "application/json"
|
||||
ContentDisposition = "Content-Disposition"
|
||||
ContentEncoding = "Content-Encoding"
|
||||
ContentRange = "Content-Range"
|
||||
Location = "Location"
|
||||
Origin = "Origin"
|
||||
Vary = "Vary"
|
||||
)
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cross-Origin Resource Sharing (CORS) headers.
|
||||
const (
|
||||
AccessControlAllowOrigin = "Access-Control-Allow-Origin" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
||||
|
@ -13,7 +18,9 @@ const (
|
|||
var (
|
||||
DefaultAccessControlAllowOrigin = ""
|
||||
DefaultAccessControlAllowCredentials = ""
|
||||
DefaultAccessControlAllowHeaders = "Origin, Accept, Accept-Ranges, Content-Range"
|
||||
DefaultAccessControlAllowMethods = "GET, HEAD, OPTIONS"
|
||||
SafeHeaders = []string{Accept, AcceptRanges, ContentDisposition, ContentEncoding, ContentRange, Location, Vary}
|
||||
DefaultAccessControlAllowHeaders = strings.Join(SafeHeaders, ", ")
|
||||
SafeMethods = []string{http.MethodGet, http.MethodHead, http.MethodOptions}
|
||||
DefaultAccessControlAllowMethods = strings.Join(SafeMethods, ", ")
|
||||
DefaultAccessControlMaxAge = "3600"
|
||||
)
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const UnknownIP = "0.0.0.0"
|
||||
const (
|
||||
UnknownIP = "0.0.0.0"
|
||||
)
|
||||
|
||||
// ClientIP returns the client IP address from the request context or a placeholder if it is unknown.
|
||||
func ClientIP(c *gin.Context) (ip string) {
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package header
|
||||
|
||||
const (
|
||||
Vary = "Vary"
|
||||
Origin = "Origin"
|
||||
)
|
Loading…
Reference in a new issue