rename utils.SanitizeSortExpressions -> utils.SanitizeSorts, add query complexity limit

This commit is contained in:
Dawid Wysokiński 2021-03-20 18:13:45 +01:00
parent 44372ecc81
commit 95dbbc4d8a
18 changed files with 223 additions and 50 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.env.development
.env.production
.env
.netrc
.idea

View File

@ -1,4 +1,4 @@
generate-graphql-schema:
gqlgen:
bash ./scripts/gqlgen-generate.sh
dev:
bash ./scripts/dev.sh

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg dailyplayerstats.FetchConfi
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > dailyplayerstats.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = dailyplayerstats.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg dailytribestats.FetchConfig
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > dailytribestats.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = dailytribestats.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -26,7 +26,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg ennoblement.FetchConfig) ([
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > ennoblement.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = ennoblement.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -1,7 +1,11 @@
package httpdelivery
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
"github.com/tribalwarshelp/api/middleware"
"github.com/tribalwarshelp/shared/models"
"time"
"github.com/99designs/gqlgen/graphql/handler"
@ -15,7 +19,9 @@ import (
)
const (
playgroundTTL = time.Hour / time.Second
endpointMain = "/graphql"
endpointPlayground = "/"
playgroundTTL = time.Hour / time.Second
)
type Config struct {
@ -27,16 +33,15 @@ func Attach(cfg Config) error {
if cfg.Resolver == nil {
return fmt.Errorf("Graphql resolver cannot be nil")
}
gqlHandler := graphqlHandler(cfg.Resolver)
cfg.RouterGroup.GET("/graphql", gqlHandler)
cfg.RouterGroup.POST("/graphql", gqlHandler)
cfg.RouterGroup.GET("/", playgroundHandler())
gqlHandler := graphqlHandler(prepareConfig(cfg.Resolver))
cfg.RouterGroup.GET(endpointMain, gqlHandler)
cfg.RouterGroup.POST(endpointMain, gqlHandler)
cfg.RouterGroup.GET(endpointPlayground, playgroundHandler())
return nil
}
// Defining the GraphQL handler
func graphqlHandler(r *resolvers.Resolver) gin.HandlerFunc {
cfg := generated.Config{Resolvers: r}
func graphqlHandler(cfg generated.Config) gin.HandlerFunc {
srv := handler.New(generated.NewExecutableSchema(cfg))
srv.AddTransport(transport.GET{})
@ -46,6 +51,15 @@ func graphqlHandler(r *resolvers.Resolver) gin.HandlerFunc {
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New(100),
})
srv.SetQueryCache(lru.New(1000))
srv.Use(&extension.ComplexityLimit{
Func: func(ctx context.Context, rc *graphql.OperationContext) int {
if middleware.CanExceedLimit(ctx) {
return 500000
}
return 1000
},
})
return func(c *gin.Context) {
c.Header("Cache-Control", "no-store, must-revalidate")
@ -55,10 +69,167 @@ func graphqlHandler(r *resolvers.Resolver) gin.HandlerFunc {
// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
h := playground.Handler("Playground", "/graphql")
h := playground.Handler("Playground", endpointMain)
return func(c *gin.Context) {
c.Header("Cache-Control", fmt.Sprintf(`public, max-age=%d`, playgroundTTL))
h.ServeHTTP(c.Writer, c.Request)
}
}
func prepareConfig(r *resolvers.Resolver) generated.Config {
return generated.Config{
Resolvers: r,
Complexity: getComplexityRoot(),
}
}
func getComplexityRoot() generated.ComplexityRoot {
complexityRoot := generated.ComplexityRoot{}
complexityRoot.Player.NameChanges = func(childComplexity int) int {
return 50 + childComplexity
}
complexityRoot.Query.DailyPlayerStats = func(
childComplexity int,
server string,
filter *models.DailyPlayerStatsFilter,
limit *int,
offset *int,
sort []string,
) int {
return 300 + childComplexity
}
complexityRoot.Query.DailyTribeStats = func(
childComplexity int,
server string,
filter *models.DailyTribeStatsFilter,
limit *int,
offset *int,
sort []string,
) int {
return 300 + childComplexity
}
complexityRoot.Query.Ennoblements = func(
childComplexity int,
server string,
filter *models.EnnoblementFilter,
limit *int,
offset *int,
sort []string,
) int {
return 300 + childComplexity
}
complexityRoot.Query.PlayerHistory = func(
childComplexity int,
server string,
filter *models.PlayerHistoryFilter,
limit *int,
offset *int,
sort []string,
) int {
return 150 + childComplexity
}
complexityRoot.Query.TribeHistory = func(
childComplexity int,
server string,
filter *models.TribeHistoryFilter,
limit *int,
offset *int,
sort []string,
) int {
return 100 + childComplexity
}
complexityRoot.Query.TribeChanges = func(
childComplexity int,
server string,
filter *models.TribeChangeFilter,
limit *int,
offset *int,
sort []string,
) int {
return 300 + childComplexity
}
complexityRoot.Query.SearchPlayer = func(
childComplexity int,
version string,
name *string,
id *int,
limit *int,
offset *int,
sort []string,
) int {
return 400 + childComplexity
}
complexityRoot.Query.SearchTribe = func(
childComplexity int,
version string,
query string,
limit *int,
offset *int,
sort []string,
) int {
return 400 + childComplexity
}
complexityRoot.Query.Players = func(
childComplexity int,
server string,
filter *models.PlayerFilter,
limit *int,
offset *int,
sort []string,
) int {
return 150 + childComplexity
}
complexityRoot.Query.Tribes = func(
childComplexity int,
server string,
filter *models.TribeFilter,
limit *int,
offset *int,
sort []string,
) int {
return 100 + childComplexity
}
complexityRoot.Query.Villages = func(
childComplexity int,
server string,
filter *models.VillageFilter,
limit *int,
offset *int,
sort []string,
) int {
return 300 + childComplexity
}
complexityRoot.Query.ServerStats = func(
childComplexity int,
server string,
filter *models.ServerStatsFilter,
limit *int,
offset *int,
sort []string,
) int {
return 100 + childComplexity
}
complexityRoot.Query.Server = func(childComplexity int, key string) int {
return 50 + childComplexity
}
complexityRoot.Query.Servers = func(
childComplexity int,
filter *models.ServerFilter,
limit *int,
offset *int,
sort []string,
) int {
return 250 + childComplexity
}
complexityRoot.Query.Versions = func(
childComplexity int,
filter *models.VersionFilter,
limit *int,
offset *int,
sort []string,
) int {
return 100 + childComplexity
}
return complexityRoot
}

25
main.go
View File

@ -74,7 +74,7 @@ func main() {
})
defer func() {
if err := db.Close(); err != nil {
log.Fatal("Database disconnecting:", err)
log.Fatal("While disconnecting from the database:", err)
}
}()
if strings.ToUpper(os.Getenv("LOG_DB_QUERIES")) == "TRUE" {
@ -124,15 +124,19 @@ func main() {
ServerUsecase: serverUcase,
})
graphql := router.Group("")
graphql.Use(middleware.DataLoadersToContext(middleware.DataLoadersToContextConfig{
ServerRepo: serverRepo,
},
dataloaders.Config{
PlayerRepo: playerRepo,
TribeRepo: tribeRepo,
VillageRepo: villageRepo,
VersionRepo: versionRepo,
}))
graphql.Use(
middleware.DataLoadersToContext(
middleware.DataLoadersToContextConfig{
ServerRepo: serverRepo,
},
dataloaders.Config{
PlayerRepo: playerRepo,
TribeRepo: tribeRepo,
VillageRepo: villageRepo,
VersionRepo: versionRepo,
},
),
)
graphql.Use(middleware.LimitWhitelist(middleware.LimitWhitelistConfig{
IPAddresses: strings.Split(os.Getenv("LIMIT_WHITELIST"), ","),
}))
@ -160,7 +164,6 @@ func main() {
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}

View File

@ -31,16 +31,16 @@ func DataLoadersToContext(dltcc DataLoadersToContextConfig, cfg dataloaders.Conf
Columns: []string{utils.Underscore("versionCode"), "key"},
})
if err != nil {
c.JSON(http.StatusOK, &gqlerror.Error{
c.JSON(http.StatusInternalServerError, &gqlerror.Error{
Message: err.Error(),
})
c.Abort()
return
}
for _, server := range servers {
serverDataLoaders[server.Key] = dataloaders.NewServerDataLoaders(server.Key, cfg)
if _, ok := versionDataLoaders[server.VersionCode]; !ok {
versionDataLoaders[server.VersionCode] = dataloaders.NewVersionDataLoaders(server.VersionCode, cfg)
for _, serv := range servers {
serverDataLoaders[serv.Key] = dataloaders.NewServerDataLoaders(serv.Key, cfg)
if _, ok := versionDataLoaders[serv.VersionCode]; !ok {
versionDataLoaders[serv.VersionCode] = dataloaders.NewVersionDataLoaders(serv.VersionCode, cfg)
}
}
ctx = StoreServerDataLoadersInContext(ctx, serverDataLoaders)

View File

@ -27,7 +27,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg player.FetchConfig) ([]*mod
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > player.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = player.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}
@ -59,6 +59,6 @@ func (ucase *usecase) SearchPlayer(ctx context.Context, cfg player.SearchPlayerC
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > player.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = player.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.SearchPlayer(ctx, cfg)
}

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg playerhistory.FetchConfig)
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > playerhistory.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = playerhistory.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -25,7 +25,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg server.FetchConfig) ([]*mod
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > server.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = server.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg serverstats.FetchConfig) ([
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > serverstats.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = serverstats.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -27,7 +27,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg tribe.FetchConfig) ([]*mode
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > tribe.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = tribe.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}
@ -59,6 +59,6 @@ func (ucase *usecase) SearchTribe(ctx context.Context, cfg tribe.SearchTribeConf
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > tribe.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = tribe.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.SearchTribe(ctx, cfg)
}

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg tribechange.FetchConfig) ([
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > tribechange.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = tribechange.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -25,6 +25,6 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg tribehistory.FetchConfig) (
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > tribehistory.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = tribehistory.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -9,9 +9,8 @@ var (
sortexprRegex = regexp.MustCompile(`^[\p{L}\_\.]+$`)
)
func SanitizeSortExpression(expr string) string {
trimmed := strings.TrimSpace(expr)
splitted := strings.Split(trimmed, " ")
func SanitizeSort(expr string) string {
splitted := strings.Split(strings.TrimSpace(expr), " ")
length := len(splitted)
if length != 2 || !sortexprRegex.Match([]byte(splitted[0])) {
return ""
@ -30,13 +29,13 @@ func SanitizeSortExpression(expr string) string {
return strings.ToLower(table+Underscore(column)) + " " + keyword
}
func SanitizeSortExpressions(exprs []string) []string {
filtered := []string{}
for _, expr := range exprs {
sanitized := SanitizeSortExpression(expr)
if sanitized != "" {
filtered = append(filtered, sanitized)
func SanitizeSorts(sorts []string) []string {
sanitized := []string{}
for _, sort := range sorts {
sanitizedSort := SanitizeSort(sort)
if sanitizedSort != "" {
sanitized = append(sanitized, sanitizedSort)
}
}
return filtered
return sanitized
}

View File

@ -29,7 +29,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg version.FetchConfig) ([]*mo
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > version.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = version.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}

View File

@ -26,7 +26,7 @@ func (ucase *usecase) Fetch(ctx context.Context, cfg village.FetchConfig) ([]*mo
if !middleware.CanExceedLimit(ctx) && (cfg.Limit > village.PaginationLimit || cfg.Limit <= 0) {
cfg.Limit = village.PaginationLimit
}
cfg.Sort = utils.SanitizeSortExpressions(cfg.Sort)
cfg.Sort = utils.SanitizeSorts(cfg.Sort)
return ucase.repo.Fetch(ctx, cfg)
}