-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add GraphQL support * Add documentation content
- Loading branch information
Showing
62 changed files
with
4,694 additions
and
545 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
cmd/api-firewall/internal/handlers/graphql/httpHandler.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package graphql | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/fasthttp/websocket" | ||
"github.com/savsgio/gotils/strconv" | ||
"github.com/sirupsen/logrus" | ||
"github.com/valyala/fasthttp" | ||
"github.com/valyala/fastjson" | ||
"github.com/wallarm/api-firewall/internal/config" | ||
"github.com/wallarm/api-firewall/internal/platform/proxy" | ||
"github.com/wallarm/api-firewall/internal/platform/validator" | ||
"github.com/wallarm/api-firewall/internal/platform/web" | ||
"github.com/wundergraph/graphql-go-tools/pkg/graphql" | ||
) | ||
|
||
type Handler struct { | ||
cfg *config.GraphQLMode | ||
serverURL *url.URL | ||
proxyPool proxy.Pool | ||
logger *logrus.Logger | ||
schema *graphql.Schema | ||
parserPool *fastjson.ParserPool | ||
wsClient proxy.WebSocketClient | ||
upgrader *websocket.FastHTTPUpgrader | ||
mu sync.Mutex | ||
} | ||
|
||
var ErrNetworkConnection = errors.New("network connection error") | ||
|
||
// GraphQLHandle performs complexity checks to the GraphQL query and proxy request to the backend if all checks are passed | ||
func (h *Handler) GraphQLHandle(ctx *fasthttp.RequestCtx) error { | ||
|
||
// handle WS | ||
if websocket.FastHTTPIsWebSocketUpgrade(ctx) { | ||
return h.HandleWebSocketProxy(ctx) | ||
} | ||
|
||
// respond with 403 status code in case of content-type of POST request is not application/json | ||
if strconv.B2S(ctx.Request.Header.Method()) == fasthttp.MethodPost && | ||
!bytes.EqualFold(ctx.Request.Header.ContentType(), []byte("application/json")) { | ||
h.logger.WithFields(logrus.Fields{ | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Debug("POST request without application/json content-type is received") | ||
|
||
return web.RespondError(ctx, fasthttp.StatusForbidden, "") | ||
} | ||
|
||
// respond with 403 status code in case of lack of "query" query parameter in GET request | ||
if strconv.B2S(ctx.Request.Header.Method()) == fasthttp.MethodGet && | ||
len(ctx.Request.URI().QueryArgs().Peek("query")) == 0 { | ||
h.logger.WithFields(logrus.Fields{ | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Debug("GET request without \"query\" query parameter is received") | ||
|
||
return web.RespondError(ctx, fasthttp.StatusForbidden, "") | ||
} | ||
|
||
// Proxy request if the validation is disabled | ||
if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationDisable) { | ||
if err := proxy.Perform(ctx, h.proxyPool); err != nil { | ||
h.logger.WithFields(logrus.Fields{ | ||
"error": err, | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Error("request proxying") | ||
|
||
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError) | ||
return web.RespondGraphQLErrors(&ctx.Response, ErrNetworkConnection) | ||
} | ||
return nil | ||
} | ||
|
||
gqlRequest, err := validator.ParseGraphQLRequest(ctx) | ||
if err != nil { | ||
h.logger.WithFields(logrus.Fields{ | ||
"error": err, | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Error("GraphQL request unmarshal") | ||
|
||
if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) { | ||
return web.RespondGraphQLErrors(&ctx.Response, err) | ||
} | ||
} | ||
|
||
// validate request | ||
if gqlRequest != nil { | ||
validationResult, err := validator.ValidateGraphQLRequest(&h.cfg.Graphql, h.schema, gqlRequest) | ||
// internal errors | ||
if err != nil { | ||
h.logger.WithFields(logrus.Fields{ | ||
"error": err, | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Error("GraphQL query validation") | ||
|
||
if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) { | ||
return web.RespondGraphQLErrors(&ctx.Response, err) | ||
} | ||
} | ||
|
||
// validation failed | ||
if !validationResult.Valid && validationResult.Errors != nil { | ||
h.logger.WithFields(logrus.Fields{ | ||
"error": validationResult.Errors, | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Error("GraphQL query validation") | ||
|
||
if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) { | ||
return web.RespondGraphQLErrors(&ctx.Response, validationResult.Errors) | ||
} | ||
} | ||
} | ||
|
||
if err := proxy.Perform(ctx, h.proxyPool); err != nil { | ||
h.logger.WithFields(logrus.Fields{ | ||
"error": err, | ||
"protocol": "HTTP", | ||
"request_id": fmt.Sprintf("#%016X", ctx.ID()), | ||
}).Error("request proxying") | ||
|
||
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError) | ||
return web.RespondGraphQLErrors(&ctx.Response, ErrNetworkConnection) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package graphql | ||
|
||
import ( | ||
"github.com/savsgio/gotils/strconv" | ||
"github.com/savsgio/gotils/strings" | ||
"net/url" | ||
"os" | ||
"sync" | ||
|
||
"github.com/fasthttp/websocket" | ||
"github.com/sirupsen/logrus" | ||
"github.com/valyala/fasthttp" | ||
"github.com/valyala/fastjson" | ||
"github.com/wallarm/api-firewall/internal/config" | ||
"github.com/wallarm/api-firewall/internal/mid" | ||
"github.com/wallarm/api-firewall/internal/platform/denylist" | ||
"github.com/wallarm/api-firewall/internal/platform/proxy" | ||
"github.com/wallarm/api-firewall/internal/platform/web" | ||
"github.com/wundergraph/graphql-go-tools/pkg/graphql" | ||
"github.com/wundergraph/graphql-go-tools/pkg/playground" | ||
) | ||
|
||
func Handlers(cfg *config.GraphQLMode, schema *graphql.Schema, serverURL *url.URL, shutdown chan os.Signal, logger *logrus.Logger, proxy proxy.Pool, wsClient proxy.WebSocketClient, deniedTokens *denylist.DeniedTokens) fasthttp.RequestHandler { | ||
|
||
// Construct the web.App which holds all routes as well as common Middleware. | ||
appOptions := web.AppAdditionalOptions{ | ||
Mode: cfg.Mode, | ||
PassOptions: false, | ||
} | ||
|
||
proxyOptions := mid.ProxyOptions{ | ||
Mode: web.GraphQLMode, | ||
RequestValidation: cfg.Graphql.RequestValidation, | ||
DeleteAcceptEncoding: cfg.Server.DeleteAcceptEncoding, | ||
ServerURL: serverURL, | ||
} | ||
|
||
denylistOptions := mid.DenylistOptions{ | ||
Mode: web.GraphQLMode, | ||
Config: &cfg.Denylist, | ||
CustomBlockStatusCode: fasthttp.StatusUnauthorized, | ||
DeniedTokens: deniedTokens, | ||
Logger: logger, | ||
} | ||
app := web.NewApp(&appOptions, shutdown, logger, mid.Logger(logger), mid.Errors(logger), mid.Panics(logger), mid.Proxy(&proxyOptions), mid.Denylist(&denylistOptions)) | ||
|
||
// define FastJSON parsers pool | ||
var parserPool fastjson.ParserPool | ||
|
||
var upgrader = websocket.FastHTTPUpgrader{ | ||
Subprotocols: []string{"graphql-ws"}, | ||
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool { | ||
if !cfg.Graphql.WSCheckOrigin { | ||
return true | ||
} | ||
return strings.Include(cfg.Graphql.WSOrigin, strconv.B2S(ctx.Request.Header.Peek("Origin"))) | ||
}, | ||
} | ||
|
||
s := Handler{ | ||
cfg: cfg, | ||
serverURL: serverURL, | ||
proxyPool: proxy, | ||
logger: logger, | ||
schema: schema, | ||
parserPool: &parserPool, | ||
wsClient: wsClient, | ||
upgrader: &upgrader, | ||
mu: sync.Mutex{}, | ||
} | ||
|
||
graphqlPath := serverURL.Path | ||
if graphqlPath == "" { | ||
graphqlPath = "/" | ||
} | ||
|
||
app.Handle(fasthttp.MethodGet, graphqlPath, s.GraphQLHandle) | ||
app.Handle(fasthttp.MethodPost, graphqlPath, s.GraphQLHandle) | ||
|
||
// enable playground | ||
if cfg.Graphql.Playground { | ||
p := playground.New(playground.Config{ | ||
PathPrefix: "", | ||
PlaygroundPath: cfg.Graphql.PlaygroundPath, | ||
GraphqlEndpointPath: graphqlPath, | ||
GraphQLSubscriptionEndpointPath: graphqlPath, | ||
}) | ||
|
||
handlers, err := p.Handlers() | ||
if err != nil { | ||
logger.Fatalf("playground handlers error: %v", err) | ||
return nil | ||
} | ||
|
||
for i := range handlers { | ||
app.Handle(fasthttp.MethodGet, handlers[i].Path, web.NewFastHTTPHandler(handlers[i].Handler, true)) | ||
} | ||
} | ||
|
||
return app.Router.Handler | ||
} |
Oops, something went wrong.