From 88542c6863f418ee212d4ee6ad3f9b5fe51bc715 Mon Sep 17 00:00:00 2001 From: AiAe Date: Tue, 30 Jun 2026 20:00:55 +0300 Subject: [PATCH 1/2] Ignore .gocache --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9e05047..e17b958 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ go.work data/ config.json temp/ -.DS_STORE \ No newline at end of file +.DS_STORE +.gocache \ No newline at end of file From f61553b5250a91ef0971c9df4922b5eac7233264 Mon Sep 17 00:00:00 2001 From: AiAe Date: Tue, 30 Jun 2026 20:01:21 +0300 Subject: [PATCH 2/2] Require auth or application secret for downloading qua and replay files --- cmd/api/server.go | 4 +-- db/applications.go | 15 ++++++++++ middleware/authentication.go | 56 +++++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/cmd/api/server.go b/cmd/api/server.go index 07bce53..e238e1e 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -186,8 +186,8 @@ func initializeRoutes(engine *gin.Engine) { engine.GET("/v2/server/stats/mostplayed", handlers.CreateHandler(handlers.GetWeeklyMostPlayedMapsets)) // Download - engine.GET("/v2/download/map/:id", handlers.CreateHandler(handlers.DownloadQua)) - engine.GET("/v2/download/replay/:id", handlers.CreateHandler(handlers.DownloadReplay)) + engine.GET("/v2/download/map/:id", middleware.RequireAuthOrClientSecret, handlers.CreateHandler(handlers.DownloadQua)) + engine.GET("/v2/download/replay/:id", middleware.RequireAuthOrClientSecret, handlers.CreateHandler(handlers.DownloadReplay)) engine.Match([]string{"GET", "HEAD"}, "/v2/download/mapset/:id", middleware.RequireAuth, handlers.CreateHandler(handlers.DownloadMapset)) engine.POST("/v2/download/multiplayer/:id/upload", middleware.RequireAuth, handlers.CreateHandler(handlers.UploadMultiplayerMapset)) engine.Match([]string{"GET", "HEAD"}, "/v2/download/multiplayer/:id", middleware.RequireAuth, handlers.CreateHandler(handlers.DownloadMultiplayerMapset)) diff --git a/db/applications.go b/db/applications.go index 4f50052..974c70e 100644 --- a/db/applications.go +++ b/db/applications.go @@ -62,6 +62,21 @@ func GetApplicationById(id int) (*Application, error) { return application, nil } +// GetActiveApplicationByClientSecret Retrieves an active application by client secret +func GetActiveApplicationByClientSecret(secret string) (*Application, error) { + var application *Application + + result := SQL. + Where("client_secret = ? AND active = 1", secret). + First(&application) + + if result.Error != nil { + return nil, result.Error + } + + return application, nil +} + // SetActiveStatus Sets an applications active status func (app *Application) SetActiveStatus(active bool) error { app.Active = active diff --git a/middleware/authentication.go b/middleware/authentication.go index 9ab4205..8bd4ae6 100644 --- a/middleware/authentication.go +++ b/middleware/authentication.go @@ -22,7 +22,8 @@ type JWTClaims struct { } const ( - messageNoHeader = "You must provide a valid `Authorization` or `auth` header." + messageNoHeader = "You must provide a valid `Authorization` or `auth` header." + messageNoAuthOrSecret = "You must provide a valid `Authorization`, `auth`, or `client_secret`." ) // RequireAuth Middleware authentication function @@ -42,6 +43,38 @@ func RequireAuth(c *gin.Context) { c.Next() } +// RequireAuthOrClientSecret requires either user authentication or a valid application client secret. +func RequireAuthOrClientSecret(c *gin.Context) { + user, authErr := authenticateUser(c) + + if authErr == nil { + c.Set("user", user) + c.Next() + return + } + + _, secretErr := authenticateApplicationClientSecret(c) + + if secretErr == nil { + c.Next() + return + } + + apiErr := authErr + + if authErr.Message == messageNoHeader && secretErr.Error == gorm.ErrRecordNotFound { + apiErr = &handlers.APIError{Status: http.StatusUnauthorized, Message: messageNoAuthOrSecret} + } else if secretErr.Error != gorm.ErrRecordNotFound { + apiErr = secretErr + } + + handlers.CreateHandler(func(ctx *gin.Context) *handlers.APIError { + return apiErr + })(c) + + c.Abort() +} + // AllowAuth Allows user authentication but does not require it. This middleware fails // in the event that the user passes in an invalid token OR some other error func AllowAuth(c *gin.Context) { @@ -94,6 +127,27 @@ func authenticateUser(c *gin.Context) (*db.User, *handlers.APIError) { return user, nil } +// authenticateApplicationClientSecret authenticates an active application client secret. +func authenticateApplicationClientSecret(c *gin.Context) (*db.Application, *handlers.APIError) { + secret := c.GetHeader("client_secret") + + if secret == "" { + return nil, &handlers.APIError{Status: http.StatusUnauthorized, Message: messageNoAuthOrSecret, Error: gorm.ErrRecordNotFound} + } + + application, err := db.GetActiveApplicationByClientSecret(secret) + + if err != nil && err != gorm.ErrRecordNotFound { + return nil, handlers.APIErrorServerError("Error occurred while authenticating application", err) + } + + if application == nil { + return nil, &handlers.APIError{Status: http.StatusUnauthorized, Message: "You are unauthorized to access this resource.", Error: gorm.ErrRecordNotFound} + } + + return application, nil +} + // AuthenticateInGameRequest authenticates a request using the in-game auth header. func AuthenticateInGameRequest(c *gin.Context) (*db.User, error) { token := c.GetHeader("auth")