~bigbes/core-go

67e655222767f9737a564a5cba19d3c268e9b2ac — Simon Martin 4 months ago 32f4828
server: setup router dedicated to webhook processing

This introduces a new chi router to allow processing (in Go) webhook
calls coming from other SourceHut services.

It ensures that incoming messages are signed with the webhook private
key and if so, provides access both to the database and service
configuration.

Signed-off-by: Simon Martin <simon@nasilyan.com>
1 files changed, 80 insertions(+), 24 deletions(-)

M server/server.go
M server/server.go => server/server.go +80 -24
@@ 1,12 1,14 @@
package server

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"


@@ 66,17 68,18 @@ var (
type Server struct {
	Schema graphql.ExecutableSchema

	addr        []string
	metricsAddr string
	pprofAddr   string
	conf        ini.File
	db          *sql.DB
	redis       goRedis.UniversalClient
	root        chi.Router
	router      chi.Router
	service     string
	queues      []*work.Queue
	email       *email.Queue
	addr          []string
	metricsAddr   string
	pprofAddr     string
	conf          ini.File
	db            *sql.DB
	redis         goRedis.UniversalClient
	anonRouter    chi.Router
	webhookRouter chi.Router
	router        chi.Router
	service       string
	queues        []*work.Queue
	email         *email.Queue

	MaxComplexity int
}


@@ 112,29 115,37 @@ func New(service, defaultAddr string, conf ini.File, args []string) *Server {

	crypto.InitCrypto(conf)

	root := chi.NewRouter()
	anonRouter := chi.NewRouter()
	server := &Server{
		addr:        addr,
		metricsAddr: metricsAddr,
		pprofAddr:   pprofAddr,
		conf:        conf,
		root:        root,
		router:      root.Group(func(_ chi.Router) {}),
		service:     service,
		addr:          addr,
		metricsAddr:   metricsAddr,
		pprofAddr:     pprofAddr,
		conf:          conf,
		anonRouter:    anonRouter,
		webhookRouter: anonRouter.Group(func(_ chi.Router) {}),
		router:        anonRouter.Group(func(_ chi.Router) {}),
		service:       service,
	}
	return server
}

// Returns the chi Router being used for this sever. All routes on this router
// Returns the chi Router being used for this server. All routes on this router
// require authentication.
func (server *Server) Router() chi.Router {
	return server.router
}

// Returns the chi Router being used for this sever. All routes on this server
// Returns the chi Router being used for this server. All routes on this server
// are unauthenticated.
func (server *Server) AnonRouter() chi.Router {
	return server.root
	return server.anonRouter
}

// Returns the chi Router being used to process webhooks. All routes on this server
// are unauthenticated but validate that the request is signed with the webhook
// private key.
func (server *Server) WebhookRouter() chi.Router {
	return server.webhookRouter
}

// Adds a GraphQL schema for this server. The second parameter shall be the


@@ 183,7 194,7 @@ func (server *Server) WithSchema(
	server.router.Handle("/query", srv)

	// These don't need auth or any other middleware - just log and process
	server.root.Group(func(r chi.Router) {
	server.anonRouter.Group(func(r chi.Router) {
		r.Use(middleware.RealIP)

		if debug {


@@ 210,6 221,51 @@ func (server *Server) WithSchema(
			w.Write(j)
		})
	})

	// These validate that the payload is signed with the webhook private key,
	// and if so, provide config and database access.
	server.webhookRouter = server.anonRouter.Group(func(r chi.Router) {
		r.Use(middleware.RealIP)
		r.Use(config.Middleware(server.conf, server.service))
		r.Use(database.Middleware(server.db))
		if debug {
			r.Use(middleware.Logger)
		}

		r.Use(func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				bodyBytes, _ := io.ReadAll(r.Body)
				r.Body.Close()
				r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))

				signatures := r.Header["X-Payload-Signature"]
				nonces := r.Header["X-Payload-Nonce"]
				if len(signatures) > 0 && len(nonces) > 0 {
					signature := signatures[0]
					nonce := nonces[0]
					nonceKey := fmt.Sprintf(
						"sr.ht.signature-nonce.%s",
						nonce)
					_, err := server.redis.Get(r.Context(),
						nonceKey).Result()
					if err != nil {
						// It's the first time we see this nonce;
						// remember it and verify the signature.
						server.redis.Set(
							context.Background(),
							nonceKey, "1",
							90*24*time.Hour)
						if crypto.VerifyWebhook(bodyBytes, nonce, signature) {
							// The signature checks out; keep going.
							next.ServeHTTP(w, r)
							return
						}
					}
				}
				w.WriteHeader(http.StatusForbidden)
			})
		})
	})
	return server
}



@@ 374,7 430,7 @@ func (server *Server) Run() {
		if err != nil {
			panic(err)
		}
		qserver := &http.Server{Handler: server.root}
		qserver := &http.Server{Handler: server.anonRouter}
		qservers = append(qservers, qserver)
		go qserver.Serve(qlisten)
	}