Compare commits

...

11 Commits

12 changed files with 538 additions and 128 deletions
+3 -3
View File
@@ -9,15 +9,15 @@ import (
)
type CacheEntry struct {
Value []byte
ExpiresAt time.Time
Value []byte
}
type Cache struct {
sync.RWMutex
bytePool sync.Pool
entries sync.Map
globalTTL time.Duration
bytePool sync.Pool
sync.RWMutex
}
func New(globalTTL time.Duration) *Cache {
+36
View File
@@ -110,3 +110,39 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
})
}
}
func (suite *CacheTestSuite) Test_CacheCleanExpiredEntries() {
cache := New(5 * time.Second)
tests := []struct {
name string
cache_value string
ttl time.Duration
}{
{
name: "test1",
cache_value: "test1-123",
ttl: 2 * time.Second,
},
{
name: "test2",
cache_value: "test2-123",
ttl: 5 * time.Second,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), tt.ttl)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
time.Sleep(tt.ttl)
c, ok = cache.Get(tt.name)
suite.Equal(false, ok)
suite.Equal("", string(c))
cache.CleanExpiredEntries()
c, ok = cache.Get(tt.name)
suite.Equal(false, ok)
suite.Equal("", string(c))
})
}
}
+12 -12
View File
@@ -3,28 +3,28 @@ module github.com/lukaszraczylo/graphql-monitoring-proxy
go 1.21
require (
github.com/VictoriaMetrics/metrics v1.24.0
github.com/VictoriaMetrics/metrics v1.29.1
github.com/buger/jsonparser v1.1.1
github.com/gofiber/fiber/v2 v2.51.0
github.com/gofiber/fiber/v2 v2.52.0
github.com/gofrs/flock v0.8.1
github.com/google/uuid v1.4.0
github.com/gookit/goutil v0.6.14
github.com/google/uuid v1.5.0
github.com/gookit/goutil v0.6.15
github.com/graphql-go/graphql v0.8.1
github.com/json-iterator/go v1.1.12
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415
github.com/lukaszraczylo/go-ratecounter v0.1.8
github.com/lukaszraczylo/go-simple-graphql v1.1.37
github.com/lukaszraczylo/go-simple-graphql v1.2.8
github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4
github.com/valyala/fasthttp v1.50.0
github.com/valyala/fasthttp v1.51.0
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/avast/retry-go/v4 v4.5.0 // indirect
github.com/avast/retry-go/v4 v4.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -40,10 +40,10 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+24 -24
View File
@@ -1,9 +1,9 @@
github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw=
github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
github.com/VictoriaMetrics/metrics v1.29.1 h1:yTORfGeO1T0C6P/tEeT4Mf7rBU5TUu3kjmHvmlaoeO8=
github.com/VictoriaMetrics/metrics v1.29.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o=
github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -12,23 +12,23 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.14 h1:96elyOG4BvVoDaiT7vx1vHPrVyEtFfYlPPBODR0/FGQ=
github.com/gookit/goutil v0.6.14/go.mod h1:YyDBddefmjS+mU2PDPgCcjVzTDM5WgExiDv5ZA/b8I8=
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -40,8 +40,8 @@ github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 h1:lvI8Wlbg4PxkR
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
github.com/lukaszraczylo/go-ratecounter v0.1.8 h1:ZYm6Wkn58ZAlFWRmC7PaD4oAYHWcu8/0MUDWGe3PnJQ=
github.com/lukaszraczylo/go-ratecounter v0.1.8/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
github.com/lukaszraczylo/go-simple-graphql v1.1.37 h1:8emnfzWTApitNQ+hMHMPeGljL/xlVf/u8oKt26bUXDs=
github.com/lukaszraczylo/go-simple-graphql v1.1.37/go.mod h1:0d/x8hj9Uus4qiv981fjL56jw8DutJ6WAeTsNWClZ/Y=
github.com/lukaszraczylo/go-simple-graphql v1.2.8 h1:VXnCQGrZPQnPXm+dVxGyndbxFru7wc8rAhBnBe1ql7g=
github.com/lukaszraczylo/go-simple-graphql v1.2.8/go.mod h1:fYwnUZ1xJqvJSfbU9k8GMMI9Flan2dNXSvg/arS7rzU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -74,8 +74,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
@@ -86,17 +86,17 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+103 -32
View File
@@ -1,6 +1,7 @@
package main
import (
"flag"
"strconv"
"strings"
@@ -10,7 +11,7 @@ import (
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
)
var retrospection_queries = []string{
var introspection_queries = []string{
"__schema",
"__type",
"__typename",
@@ -34,7 +35,39 @@ var retrospection_queries = []string{
}
// Saving the introspection queries as a map O(1) operation instead of O(n) for a slice.
var retrospectionQuerySet = make(map[string]struct{}, len(retrospection_queries))
var introspectionQuerySet = map[string]struct{}{}
var introspectionAllowedQueries = map[string]struct{}{}
var allowedUrls = map[string]struct{}{}
func prepareQueriesAndExemptions() {
introspectionQuerySet = map[string]struct{}{}
introspectionQuerySet = func() map[string]struct{} {
rsqs := make(map[string]struct{}, len(introspection_queries))
for _, query := range introspection_queries {
rsqs[strings.ToLower(query)] = struct{}{}
}
return rsqs
}()
introspectionAllowedQueries = map[string]struct{}{}
introspectionAllowedQueries = func() map[string]struct{} {
rsqs := make(map[string]struct{}, len(cfg.Security.IntrospectionAllowed))
for _, query := range cfg.Security.IntrospectionAllowed {
rsqs[strings.ToLower(query)] = struct{}{}
}
return rsqs
}()
allowedUrls = map[string]struct{}{}
allowedUrls = func() map[string]struct{} {
rsqs := make(map[string]struct{}, len(cfg.Server.AllowURLs))
for _, query := range cfg.Server.AllowURLs {
rsqs[strings.ToLower(query)] = struct{}{}
}
return rsqs
}()
}
func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool, cache_time int, should_block bool, should_ignore bool) {
should_ignore = true
@@ -42,21 +75,27 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
err := json.Unmarshal(c.Body(), &m)
if err != nil {
cfg.Logger.Debug("Can't unmarshal the request", map[string]interface{}{"error": err.Error(), "body": string(c.Body())})
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
return
}
// get the query
query, ok := m["query"].(string)
if !ok {
cfg.Logger.Error("Can't find the query", map[string]interface{}{"query": query, "m_val": m})
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
return
}
p, err := parser.Parse(parser.ParseParams{Source: query})
if err != nil {
cfg.Logger.Error("Can't parse the query", map[string]interface{}{"query": query, "m_val": m})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return
}
@@ -65,19 +104,21 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
for _, d := range p.Definitions {
if oper, ok := d.(*ast.OperationDefinition); ok {
operationType = oper.Operation
if oper.Name != nil {
operationName = oper.Name.Value
}
if strings.ToLower(operationType) == "mutation" && cfg.Server.ReadOnlyMode {
cfg.Logger.Warning("Mutation blocked", m)
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
c.Status(403).SendString("The server is in read-only mode")
should_block = true
return
}
if oper.Name != nil {
operationName = oper.Name.Value
} else {
operationName = "undefined"
}
for _, dir := range oper.Directives {
if dir.Name.Value == "cached" {
cacheRequest = true
@@ -85,37 +126,67 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
if arg.Name.Value == "ttl" {
cache_time, err = strconv.Atoi(arg.Value.GetValue().(string))
if err != nil {
cfg.Logger.Error("Can't parse the ttl", map[string]interface{}{"ttl": arg.Value.GetValue().(string)})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
cfg.Logger.Error("Can't parse the ttl, using global", map[string]interface{}{"bad_ttl": arg.Value.GetValue().(string)})
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return
}
}
}
}
}
if cfg.Security.BlockIntrospection {
for _, s := range oper.SelectionSet.Selections {
for _, s2 := range s.GetSelectionSet().Selections {
if _, exists := retrospectionQuerySet[strings.ToLower(s2.(*ast.Field).Name.Value)]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
for _, introspectionQueryAllowed := range cfg.Security.IntrospectionAllowed {
if strings.EqualFold(strings.ToLower(introspectionQueryAllowed), strings.ToLower(s2.(*ast.Field).Name.Value)) {
cfg.Logger.Debug("Introspection query allowed, passing through", m)
return
}
}
}
cfg.Logger.Warning("Introspection query blocked", m)
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
c.Status(403).SendString("Introspection queries are not allowed")
should_block = true
return
}
}
should_block = checkSelections(c, oper.GetSelectionSet().Selections)
if should_block {
return
}
}
}
}
return
}
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
for _, s := range selections {
field, ok := s.(*ast.Field)
if !ok {
continue // or handle the case where the type assertion fails
}
shouldBlock := checkIfContainsIntrospection(c, field.Name.Value)
if shouldBlock {
return true
}
if field.SelectionSet != nil {
if checkSelections(c, field.GetSelectionSet().Selections) {
return true
}
}
}
return false
}
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) (should_block bool) {
whateverLower := strings.ToLower(whatever)
got_exemption := false
if _, exists := introspectionQuerySet[whateverLower]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed_exists := introspectionAllowedQueries[whateverLower]; allowed_exists {
cfg.Logger.Debug("Introspection query allowed, passing through", map[string]interface{}{"query": whatever})
got_exemption = true
should_block = false
}
}
if !got_exemption {
should_block = true
}
}
if should_block {
if flag.Lookup("test.v") == nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
c.Status(403).SendString("Introspection queries are not allowed")
}
return
}
+315
View File
@@ -0,0 +1,315 @@
package main
import (
"testing"
fiber "github.com/gofiber/fiber/v2"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/valyala/fasthttp"
)
func (suite *Tests) Test_parseGraphQLQuery() {
type results struct {
op_name string
op_type string
cached_ttl int
returnCode int
is_cached bool
should_block bool
should_ignore bool
}
type queries struct {
headers map[string]string
body string
}
tests := []struct {
name string
suppliedSettings *config
suppliedQuery queries
wantResults results
}{
{
name: "test empty body",
suppliedQuery: queries{
body: "",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: true,
op_name: "",
op_type: "",
},
},
{
name: "test empty json",
suppliedQuery: queries{
body: "{}",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: true,
op_name: "",
op_type: "",
},
},
{
name: "test empty with some random garbage",
suppliedQuery: queries{
body: "{\"variables\": {\"id\": \"1\"}}",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: true,
op_name: "",
op_type: "",
},
},
{
name: "test valid query with op name",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, variables and cache",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
should_block: false,
should_ignore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, cache and ttl",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached(ttl: 60) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
cached_ttl: 60,
should_block: false,
should_ignore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, cache and INVALID ttl",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached(ttl: nope) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
cached_ttl: 0,
should_block: false,
should_ignore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test mutation query with op name",
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: false,
op_name: "MyMutation",
op_type: "mutation",
},
},
{
name: "test mutation query with config: read only",
suppliedSettings: func() *config {
cfg.Server.ReadOnlyMode = true
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
should_block: true,
should_ignore: false,
op_name: "MyMutation",
op_type: "mutation",
returnCode: 403,
},
},
{
name: "test simple query with introspection __schema",
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}",
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: false,
op_name: "MyMutation",
op_type: "mutation",
},
},
{
name: "test simple query with introspection __schema config: block introspection",
suppliedSettings: func() *config {
cfg.Security.BlockIntrospection = true
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"query MyIntroQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}",
},
wantResults: results{
is_cached: false,
should_block: true,
should_ignore: false,
op_name: "MyIntroQuery",
op_type: "query",
returnCode: 403,
},
},
{
name: "test user supplied query with introspection #1 - config: block",
suppliedSettings: func() *config {
parseConfig()
cfg.Security.BlockIntrospection = true
cfg.Security.IntrospectionAllowed = []string{}
prepareQueriesAndExemptions()
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}",
},
wantResults: results{
is_cached: false,
should_block: true,
should_ignore: false,
op_name: "undefined",
op_type: "query",
returnCode: 403,
},
},
{
name: "test user supplied query with introspection #1 - config: block & allow __schema",
suppliedSettings: func() *config {
parseConfig()
cfg.Security.BlockIntrospection = true
cfg.Security.IntrospectionAllowed = []string{"__schema"}
prepareQueriesAndExemptions()
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}",
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: false,
op_name: "undefined",
op_type: "query",
returnCode: 200,
},
},
{
name: "test invalid query",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } \"}",
},
wantResults: results{
is_cached: false,
should_block: false,
should_ignore: true,
op_name: "",
op_type: "",
},
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cfg = &config{}
cfg.Logger = libpack_logging.NewLogger()
defer func() {
cfg = &config{}
}()
app := fiber.New()
ctx_headers := func() *fasthttp.RequestHeader {
h := fasthttp.RequestHeader{}
for k, v := range tt.suppliedQuery.headers {
h.Add(k, v)
}
return &h
}()
ctx_request := fasthttp.Request{
Header: *ctx_headers,
}
ctx_request.AppendBody([]byte(tt.suppliedQuery.body))
ctx := app.AcquireCtx(&fasthttp.RequestCtx{
Request: ctx_request,
})
defer app.ReleaseCtx(ctx)
assert.NotNil(ctx, "Fiber context is nil")
if tt.suppliedSettings != nil {
cfg = tt.suppliedSettings
}
defer func() {
cfg = &config{}
}()
opType, opName, cacheFromQuery, cached_ttl, shouldBlock, should_ignore := parseGraphQLQuery(ctx)
assert.Equal(tt.wantResults.op_type, opType, "Unexpected operation type", tt.name)
assert.Equal(tt.wantResults.op_name, opName, "Unexpected operation name", tt.name)
assert.Equal(tt.wantResults.is_cached, cacheFromQuery, "Unexpected cache value", tt.name)
assert.Equal(tt.wantResults.cached_ttl, cached_ttl, "Unexpected cache TTL value", tt.name)
assert.Equal(tt.wantResults.should_block, shouldBlock, "Unexpected block value", tt.name)
assert.Equal(tt.wantResults.should_ignore, should_ignore, "Unexpected ignore value", tt.name)
if tt.wantResults.returnCode > 0 {
assert.Equal(tt.wantResults.returnCode, ctx.Response().StatusCode(), "Unexpected return code", tt.name)
}
})
}
}
+2 -7
View File
@@ -11,15 +11,9 @@ import (
var cfg *config
func init() {
for _, query := range retrospection_queries {
retrospectionQuerySet[query] = struct{}{}
}
}
func parseConfig() {
libpack_config.PKG_NAME = "graphql_proxy"
var c config
c := config{}
c.Server.PortGraphQL = envutil.GetInt("PORT_GRAPHQL", 8080)
c.Server.PortMonitoring = envutil.GetInt("MONITORING_PORT", 9393)
c.Server.HostGraphQL = envutil.Getenv("HOST_GRAPHQL", "http://localhost/")
@@ -61,6 +55,7 @@ func parseConfig() {
enableCache() // takes close to no resources, but can be used with dynamic query cache
loadRatelimitConfig()
enableApi()
prepareQueriesAndExemptions()
}
func main() {
+1 -1
View File
@@ -15,9 +15,9 @@ import (
)
type MetricsSetup struct {
metrics_prefix string
metrics_set *metrics.Set
metrics_set_custom *metrics.Set
metrics_prefix string
}
var (
+4 -1
View File
@@ -18,9 +18,11 @@ func createFasthttpClient(timeout int) *fasthttp.Client {
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxConnsPerHost: 200,
MaxConnsPerHost: 2048,
ReadTimeout: time.Second * time.Duration(timeout),
WriteTimeout: time.Second * time.Duration(timeout),
MaxIdleConnDuration: time.Second * time.Duration(timeout),
MaxConnDuration: time.Second * time.Duration(timeout),
DisableHeaderNamesNormalizing: true,
}
}
@@ -35,6 +37,7 @@ func proxyTheRequest(c *fiber.Ctx) error {
c.Request().Header.DisableNormalizing()
c.Request().Header.Add("X-Real-IP", c.IP())
c.Request().Header.Add(fiber.HeaderXForwardedFor, string(c.Request().Header.Peek("X-Forwarded-For")))
c.Request().Header.Del(fiber.HeaderAcceptEncoding)
proxy.WithClient(cfg.Client.FastProxyClient)
+2 -2
View File
@@ -8,9 +8,9 @@ import (
)
type RateLimitConfig struct {
Req int `json:"req"`
Interval string `json:"interval"`
RateCounterTicker *goratecounter.RateCounter
Interval string `json:"interval"`
Req int `json:"req"`
}
var rateLimits map[string]RateLimitConfig
+11 -13
View File
@@ -21,6 +21,9 @@ func StartHTTPProxy() {
server := fiber.New(fiber.Config{
DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
})
server.Use(cors.New(cors.Config{
@@ -49,15 +52,11 @@ func AddRequestUUID(c *fiber.Ctx) error {
}
func checkAllowedURLs(c *fiber.Ctx) bool {
if len(cfg.Server.AllowURLs) == 0 {
if len(allowedUrls) == 0 {
return true
}
for _, allowedURL := range cfg.Server.AllowURLs {
if c.Path() == allowedURL {
return true
}
}
return false
_, ok := allowedUrls[c.Path()]
return ok
}
func healthCheck(c *fiber.Ctx) error {
@@ -116,21 +115,20 @@ func processGraphQLRequest(c *fiber.Ctx) error {
}
if should_ignore {
cfg.Logger.Debug("Request passed as-is - not a GraphQL")
cfg.Logger.Debug("Request passed as-is - probably not a GraphQL")
return proxyTheRequest(c)
}
if cache_time > 0 {
cfg.Logger.Debug("Cache time set via query", map[string]interface{}{"cache_time": cache_time})
cache_time = cfg.Cache.CacheTTL
}
if cache_time == 0 && !cacheFromQuery {
} else {
// If not set via query, try setting via header
cacheQuery := c.Request().Header.Peek("X-Cache-Graphql-Query")
if cacheQuery != nil {
cache_time, _ = strconv.Atoi(string(cacheQuery))
cfg.Logger.Debug("Cache time set via header", map[string]interface{}{"cache_time": cache_time})
cacheFromQuery = true
} else {
cache_time = cfg.Cache.CacheTTL
}
}
+25 -33
View File
@@ -10,47 +10,39 @@ import (
// config is a struct that holds the configuration of the application.
type config struct {
Cache struct {
CacheClient *libpack_cache.Cache
CacheTTL int
CacheEnable bool
}
Logger *libpack_logging.LogConfig
Monitoring *libpack_monitoring.MetricsSetup
// Server holds the configuration of the server _ONLY_.
Server struct {
PortGraphQL int
PortMonitoring int
HostGraphQL string
HealthcheckGraphQL string
AccessLog bool
ReadOnlyMode bool
AllowURLs []string
EnableApi bool
ApiPort int
PurgeOnCrawl bool
PurgeEvery int
}
Client struct {
JWTUserClaimPath string
JWTRoleClaimPath string
RoleRateLimit bool
RoleFromHeader string
Api struct{ BannedUsersFile string }
Client struct {
GQLClient *graphql.BaseClient
FastProxyClient *fasthttp.Client
JWTUserClaimPath string
JWTRoleClaimPath string
RoleFromHeader string
proxy string
ClientTimeout int
RoleRateLimit bool
}
Cache struct {
CacheEnable bool
CacheTTL int
CacheClient *libpack_cache.Cache
}
Api struct {
BannedUsersFile string
}
Security struct {
BlockIntrospection bool
IntrospectionAllowed []string
BlockIntrospection bool
}
Server struct {
HostGraphQL string
HealthcheckGraphQL string
AllowURLs []string
PortGraphQL int
PortMonitoring int
ApiPort int
PurgeEvery int
AccessLog bool
ReadOnlyMode bool
EnableApi bool
PurgeOnCrawl bool
}
}