diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..0ad2fe0 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,17 @@ +name: Test and release + +on: + workflow_dispatch: + push: + paths-ignore: + - '**.md' + branches: + - "*" + +jobs: + shared: + uses: telegram-bot-app/ci-scripts/.github/workflows/build-test-publish-inject.yaml@main + with: + enable-code-scans: false + secrets: + ghcr-token: ${{ secrets.GHCR_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e3245b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +graphql-proxy +test.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..012004f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:latest +RUN apk add --no-cache ca-certificates +WORKDIR /go/src/app +ARG TARGETARCH +ARG TARGETOS +ADD dist/bot-$TARGETOS-$TARGETARCH /go/src/app/graphql-proxy +RUN chmod +x /go/src/app/graphql-proxy +ENTRYPOINT ["/go/src/app/graphql-proxy"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70fa6f3 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +CI_RUN?=false +ADDITIONAL_BUILD_FLAGS="" + +ifeq ($(CI_RUN), true) + ADDITIONAL_BUILD_FLAGS="-test.short" +endif + +.PHONY: help +help: ## display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + +.PHONY: run +run: ## run application + @LOG_LEVEL=debug JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/v1/graphql go run *.go + +.PHONY: build +build: ## build the binary + go build -o graphql-proxy *.go + +.PHONY: test +test: ## run tests on library + @LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./... -race + +.PHONY: test-packages +test-packages: ## run tests on packages + @go test -v -cover ./pkg/... + +.PHONY: all +all: test-packages test + +.PHONY: update +update: ## update dependencies + @go get -u -v ./... + @go mod tidy -v diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..4ff1819 --- /dev/null +++ b/cache.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "time" + + "github.com/akyoto/cache" + fiber "github.com/gofiber/fiber/v2" + "github.com/gookit/goutil/strutil" +) + +func calculateHash(c *fiber.Ctx) string { + return strutil.Md5(fmt.Sprintf("%s", c.Body())) +} + +func enableCache() { + var err error + cfg.Cache.CacheClient = cache.New(time.Duration(cfg.Cache.CacheTTL) * time.Second * 2) + if err != nil { + fmt.Println(">> Error while creating cache client;", "error", err.Error()) + panic(err) + } +} + +func cacheLookup(hash string) []byte { + if cfg.Cache.CacheClient != nil { + obj, found := cfg.Cache.CacheClient.Get(hash) + if found { + return obj.([]byte) + } + } + return nil +} diff --git a/details.go b/details.go new file mode 100644 index 0000000..9b770c7 --- /dev/null +++ b/details.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/base64" + "strings" + + "github.com/lukaszraczylo/ask" + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +func extractClaimsFromJWTHeader(authorization string) (usr string) { + tokenParts := strings.Split(authorization, ".") + if len(tokenParts) != 3 { + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) + cfg.Logger.Error("Can't split the token", map[string]interface{}{"token": authorization}) + return + } + claim, err := base64.RawURLEncoding.DecodeString(tokenParts[1]) + if err != nil { + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) + cfg.Logger.Error("Can't decode the token", map[string]interface{}{"token": authorization}) + return + } + var claimMap map[string]interface{} + err = json.Unmarshal(claim, &claimMap) + if err != nil { + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) + cfg.Logger.Error("Can't unmarshal the claim", map[string]interface{}{"token": authorization}) + return + } + usr, ok := ask.For(claimMap, cfg.Client.JWTUserClaimPath).String("-") + if !ok { + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) + cfg.Logger.Error("Can't find the user id", map[string]interface{}{"claim_map": claimMap, "path": cfg.Client.JWTUserClaimPath}) + return + } + return usr +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b694688 --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module github.com/lukaszraczylo/graphql-monitoring-proxy + +go 1.21 + +require ( + github.com/akyoto/cache v1.0.6 + github.com/gofiber/fiber/v2 v2.49.2 + github.com/gookit/goutil v0.6.12 + 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/telegram-bot-app/libpack v0.0.0-20231007021518-909ce2741a36 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/VictoriaMetrics/metrics v1.24.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/lukaszraczylo/pandati v0.0.29 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/telegram-bot-app/lib-logging v0.0.19 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/fastrand v1.1.0 // indirect + github.com/valyala/histogram v1.2.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/wI2L/jsondiff v0.4.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a70a9d4 --- /dev/null +++ b/go.sum @@ -0,0 +1,93 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw= +github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys= +github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k= +github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +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= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs= +github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/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.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc= +github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM= +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.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 h1:lvI8Wlbg4PxkRcg2f10wgoaRpfN19v+YdRek3+dLtlM= +github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c= +github.com/lukaszraczylo/pandati v0.0.29 h1:WUEWm1+hWjE5KJbIL8OctG00x2dk4XKGJSlrjhxZ55k= +github.com/lukaszraczylo/pandati v0.0.29/go.mod h1:+DyTWKFaXd+jIfe7GW5w2S5PyTko/RXxMyOa+Vl713A= +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= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/telegram-bot-app/lib-logging v0.0.19 h1:zbyFr2ygeBY+yuaB9moXyOGk8dIBCn0jPJQjvx7YvLE= +github.com/telegram-bot-app/lib-logging v0.0.19/go.mod h1:n8d29fRUTdgJhC4RZ8s4lP2RHiGCCRYEj2ENEClUGc8= +github.com/telegram-bot-app/libpack v0.0.0-20231007021518-909ce2741a36 h1:DqXg0y57Q7BziHDu85OXgo/b8OlP7/+gDZvASQCkaW0= +github.com/telegram-bot-app/libpack v0.0.0-20231007021518-909ce2741a36/go.mod h1:W2kWHcfNNS0r++dJ1T2XX/C4cTSxI3MsoiMbOtyqu+I= +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/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= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/wI2L/jsondiff v0.4.0 h1:iP56F9tK83eiLttg3YdmEENtZnwlYd3ezEpNNnfZVyM= +github.com/wI2L/jsondiff v0.4.0/go.mod h1:nR/vyy1efuDeAtMwc3AF6nZf/2LD1ID8GTyyJ+K8YB0= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +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/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/graphql.go b/graphql.go new file mode 100644 index 0000000..84157eb --- /dev/null +++ b/graphql.go @@ -0,0 +1,46 @@ +package main + +import ( + fiber "github.com/gofiber/fiber/v2" + "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/parser" + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool) { + m := make(map[string]interface{}) + err := json.Unmarshal(c.Body(), &m) + if err != nil { + cfg.Logger.Error("Can't unmarshal the request", map[string]interface{}{"error": err.Error(), "body": string(c.Body())}) + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, 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) + 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) + return + } + + operationName = "undefined" + for _, d := range p.Definitions { + if oper, ok := d.(*ast.OperationDefinition); ok { + operationType = oper.Operation + operationName = oper.Name.Value + for _, dir := range oper.Directives { + if dir.Name.Value == "cached" { + cacheRequest = true + } + } + } + } + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..bc68cc7 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/gookit/goutil/envutil" + libpack_config "github.com/telegram-bot-app/libpack/config" + libpack_logging "github.com/telegram-bot-app/libpack/logging" +) + +var cfg *config + +func parseConfig() { + libpack_config.PKG_NAME = "graphql_proxy" + var 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", "localhost/v1/graphql") + c.Client.JWTUserClaimPath = envutil.Getenv("JWT_USER_CLAIM_PATH", "") + c.Cache.CacheEnable = envutil.GetBool("CACHE_ENABLE", false) + c.Cache.CacheTTL = envutil.GetInt("CACHE_TTL", 60) + c.Logger = libpack_logging.NewLogger() + cfg = &c + enableCache() // takes close to no resources, but can be used with dynamic query cache +} + +func main() { + parseConfig() + StartMonitoringServer() + StartHTTPProxy() +} diff --git a/monitoring.go b/monitoring.go new file mode 100644 index 0000000..a563bff --- /dev/null +++ b/monitoring.go @@ -0,0 +1,11 @@ +package main + +import ( + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +func StartMonitoringServer() { + cfg.Monitoring = libpack_monitoring.NewMonitoring() + cfg.Monitoring.AddMetricsPrefix("graphql_proxy") + cfg.Monitoring.RegisterDefaultMetrics() +} diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..5ac2afb --- /dev/null +++ b/proxy.go @@ -0,0 +1,29 @@ +package main + +import ( + "crypto/tls" + "fmt" + + fiber "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/proxy" + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +func proxyTheRequest(c *fiber.Ctx) error { + c.Request().Header.Add("X-Real-IP", c.IP()) + c.Request().Header.Add("X-Forwarded-For", c.IP()) + + proxy.WithTlsConfig(&tls.Config{ + InsecureSkipVerify: true, + }) + + err := proxy.DoRedirects(c, cfg.Server.HostGraphQL, 3) + if err != nil { + fmt.Println("Can't proxy the request: ", err) + cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) + return err + } + + c.Response().Header.Del(fiber.HeaderServer) + return nil +} diff --git a/semver.yaml b/semver.yaml new file mode 100644 index 0000000..f00ee7a --- /dev/null +++ b/semver.yaml @@ -0,0 +1,14 @@ +version: 1 +force: + existing: true + strict: false +wording: + patch: + - update + - initial + - fix + minor: + - change + - improve + major: + - breaking \ No newline at end of file diff --git a/server.go b/server.go new file mode 100644 index 0000000..0cd901e --- /dev/null +++ b/server.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "time" + + fiber "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + jsoniter "github.com/json-iterator/go" + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// StartHTTPProxy starts the HTTP and points it to the GraphQL server. +func StartHTTPProxy() { + server := fiber.New() + + server.Use(cors.New(cors.Config{ + AllowOrigins: "*", + })) + + server.Post("/v1/graphql", processGraphQLRequest) + + server.Get("/healthz", healthCheck) + err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)) + if err != nil { + fmt.Println("Can't start the service: ", err) + } +} + +func healthCheck(c *fiber.Ctx) error { + return c.SendString("OK") +} + +func processGraphQLRequest(c *fiber.Ctx) error { + t := time.Now() + + var extracted_user_id string = "-" + var query_cache_hash string = "" + + authorization := c.Request().Header.Peek("Authorization") + if authorization != nil && len(cfg.Client.JWTUserClaimPath) > 0 { + extracted_user_id = extractClaimsFromJWTHeader(string(authorization)) + } + opType, opName, cache_from_query := parseGraphQLQuery(c) + + was_cached := false + + if cache_from_query || cfg.Cache.CacheEnable { + cfg.Logger.Debug("Cache enabled", map[string]interface{}{"via_query": cache_from_query, "via_env": cfg.Cache.CacheEnable}) + query_cache_hash = calculateHash(c) + cachedResponse := cacheLookup(query_cache_hash) + if cachedResponse != nil { + cfg.Logger.Debug("Cache hit", map[string]interface{}{"hash": query_cache_hash, "user_id": extracted_user_id}) + c.Send(cachedResponse) + was_cached = true + } else { + cfg.Logger.Debug("Cache miss", map[string]interface{}{"hash": query_cache_hash, "user_id": extracted_user_id}) + proxyTheRequest(c) + cfg.Cache.CacheClient.Set(query_cache_hash, c.Response().Body(), time.Duration(cfg.Cache.CacheTTL)*time.Second) + c.Send(c.Response().Body()) + } + } else { + proxyTheRequest(c) + } + time_taken := time.Since(t) + + cfg.Logger.Info("Request processed", map[string]interface{}{"ip": c.IP(), "user_id": extracted_user_id, "op_type": opType, "op_name": opName, "time": time_taken, "cache": was_cached}) + cfg.Monitoring.Increment(libpack_monitoring.MetricsSucceeded, nil) + + labels := map[string]string{ + "op_type": opType, + "op_name": opName, + "cached": fmt.Sprintf("%t", was_cached), + "user_id": extracted_user_id, + } + + cfg.Monitoring.Increment("executed_query", labels) + + if !was_cached { + cfg.Monitoring.UpdateDuration("timed_query", labels, t) + cfg.Monitoring.Update("timed_query", labels, float64(time_taken.Milliseconds())) + } + // // cfg.Monitoring.Set("timed_query", time_taken.Milliseconds()) + return nil +} diff --git a/struct_config.go b/struct_config.go new file mode 100644 index 0000000..1ab544b --- /dev/null +++ b/struct_config.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/akyoto/cache" + libpack_logging "github.com/telegram-bot-app/libpack/logging" + libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" +) + +// config is a struct that holds the configuration of the application. +type config struct { + Logger *libpack_logging.LogConfig + Monitoring *libpack_monitoring.MetricsSetup + + // Server holds the configuration of the server _ONLY_. + Server struct { + PortGraphQL int + PortMonitoring int + HostGraphQL string + } + + Client struct { + JWTUserClaimPath string + } + + Cache struct { + CacheEnable bool + CacheTTL int + CacheClient *cache.Cache + } +}