Skip to content

Commit

Permalink
Add simple webhook proxy to support REST callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
Dennor committed Jul 15, 2022
1 parent 5393a75 commit f0a9578
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 30 deletions.
51 changes: 29 additions & 22 deletions cmd/local/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,37 @@ func NewStartCommand() *cobra.Command {
if err != nil {
return err
}
h = handlers.RecoveryHandler(
httplog.WithLogging(
cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
}).Handler(
handlers.WithProtocolInContext(h),
webhookHandler, err := server.NewWebhookHandler(cfg)
if err != nil {
return err
}
middleware := func(next http.Handler) http.Handler {
return handlers.RecoveryHandler(
httplog.WithLogging(
cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
}).Handler(next),
httplog.DefaultStacktracePred,
),
httplog.DefaultStacktracePred,
),
klogErrorf{},
)
klogErrorf{},
)
}
h = middleware(h)
webhookHandler = middleware(webhookHandler)
srv := server.Server{
Handler: h,
Addr: ":8080",
Handler: h,
WebhookHandler: webhookHandler,
Addr: ":8080",
}
return srv.ListenAndServe()
},
Expand Down
6 changes: 6 additions & 0 deletions example/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ module.exports = {
return input.arguments.review
},

// Mutation.webhook
webhook: input => {
console.log(JSON.stringify(input.protocol));
return 'OK'
},

// Human/Droid.friends
friends: input => friends(input.source.friends),

Expand Down
1 change: 1 addition & 0 deletions example/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Query {
"""The mutation type, represents all updates we can make to our data"""
type Mutation {
createReview(episode: Episode!, review: ReviewInput!): Review
webhook: String
}
"""The subscription type, represents all subscriptions we can make"""
type Subscription {
Expand Down
5 changes: 5 additions & 0 deletions example/stucco.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"name": "function.createReview"
}
},
"Mutation.webhook": {
"resolve": {
"name": "function.webhook"
}
},
"Subscription.randomGreet": {
"resolve": {
"name": "function.randomGreet"
Expand Down
2 changes: 1 addition & 1 deletion pkg/handlers/graphiql.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var graphiql = []byte(`<html>
></script>
<script
crossorigin
src="https://unpkg.com/graphiql/graphiql.min.js"
src="https://unpkg.com/graphiql/graphiql.js"
></script>
<script>
Expand Down
15 changes: 15 additions & 0 deletions pkg/handlers/subscription_handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
Expand Down Expand Up @@ -290,6 +291,20 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Write(buff)
}

type webhookResponseWrapper struct {
bytes.Buffer
http.ResponseWriter
status int
}

func (w *webhookResponseWrapper) Write(b []byte) (int, error) {
return w.Buffer.Write(b)
}

func (w *webhookResponseWrapper) WriteHeader(status int) {
w.status = status
}

// New returns new handler
func New(cfg Config) *Handler {
h := Handler{
Expand Down
69 changes: 69 additions & 0 deletions pkg/handlers/webhook_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package handlers

import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strings"

"github.com/graphql-editor/stucco/pkg/router"
"github.com/graphql-go/graphql"
"k8s.io/klog"
)

func NewWebhookHandler(c Config, gqlHandler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
protocolData := map[string]interface{}{
"method": r.Method,
"url": map[string]string{
"path": r.URL.Path,
"host": r.URL.Host,
"query": r.URL.RawQuery,
},
"host": r.Host,
"remoteAddress": r.RemoteAddr,
"proto": r.Proto,
"headers": r.Header.Clone(),
}
if r.Body != nil {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(rw, "could not read request body", http.StatusInternalServerError)
return
}
r.Body.Close()
if len(body) > 0 {
protocolData["body"] = body
}
}
pathParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/"), "/")
op := strings.ToLower(pathParts[1])
if len(pathParts) != 3 || (op != "query" && op != "mutation") || len(pathParts) == 0 {
http.Error(rw, "Invalid webhook path, must be /webhook/<query|mutation>/<field>", http.StatusBadRequest)
return
}
r.Body = io.NopCloser(strings.NewReader(`{ "query": "` + op + `{` + pathParts[2] + `}" }`))
r.Method = "POST"
r.Header.Set("content-type", "application/json")
r = r.WithContext(context.WithValue(r.Context(), router.ProtocolKey, protocolData))
respProxy := webhookResponseWrapper{
ResponseWriter: rw,
}
gqlHandler.ServeHTTP(&respProxy, r)
status := respProxy.status
if respProxy.status >= 200 && respProxy.status < 300 {
var resp graphql.Result
if err := json.Unmarshal(respProxy.Buffer.Bytes(), &resp); err != nil {
status = http.StatusInternalServerError
} else if resp.HasErrors() {
status = http.StatusBadRequest
}
}
rw.WriteHeader(status)
if _, err := io.Copy(rw, &respProxy.Buffer); err != nil {
klog.Error(err)
}
})
}
49 changes: 42 additions & 7 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,35 @@ func New(c Config) (httpHandler http.Handler, err error) {
return
}

// NewWebhookHandler returns new handler for webhook to graphql server
func NewWebhookHandler(c Config) (httpHandler http.Handler, err error) {
if err == nil {
if c.DefaultEnvironment.Provider != "" || c.DefaultEnvironment.Runtime != "" {
router.SetDefaultEnvironment(c.DefaultEnvironment)
}
}
var rt router.Router
if err == nil {
rt, err = router.NewRouter(c.Config)
}
if err == nil {
cfg := gqlhandler.Config{
Schema: &rt.Schema,
Pretty: checkPointerBoolDefaultTrue(c.Pretty),
}
httpHandler = gqlhandler.NewWebhookHandler(cfg, gqlhandler.New(cfg))
}
return
}

// Server default simple server that has two endpoints. /graphql which uses Handler as a handler
// and /health that uses Health as a handler or just returns 200.
// It handles SIGTERM.
type Server struct {
Handler http.Handler
Health http.Handler
Addr string
Handler http.Handler
WebhookHandler http.Handler
Health http.Handler
Addr string
}

func (s *Server) health(rw http.ResponseWriter, req *http.Request) {
Expand All @@ -302,14 +324,27 @@ func (s *Server) health(rw http.ResponseWriter, req *http.Request) {
fmt.Fprint(rw, "OK")
}

func (s *Server) handler(rw http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/graphql":
s.Handler.ServeHTTP(rw, r)
return
case "/health":
s.health(rw, r)
return
}
if s.WebhookHandler != nil && strings.HasPrefix(r.URL.Path, "/webhook/") {
s.WebhookHandler.ServeHTTP(rw, r)
return
}
http.Error(rw, "404 - not found", http.StatusNotFound)
}

// ListenAndServe is a simple wrapper around http.Server.ListenAndServe with two endpoints. It is blocking
func (s *Server) ListenAndServe() error {
mux := http.ServeMux{}
mux.Handle("/graphql", s.Handler)
mux.HandleFunc("/health", s.health)
srv := http.Server{
Addr: s.Addr,
Handler: &mux,
Handler: http.HandlerFunc(s.handler),
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
Expand Down

0 comments on commit f0a9578

Please sign in to comment.