@@ 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)
}