diff --git a/server/server.go b/server/server.go index a3ae29e0fc57eedb6877bad47f2a0a3146420e56..4bbf350ef80e2e9e949fcd1541ca2e559dc4001c 100644 --- a/server/server.go +++ b/server/server.go @@ -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 @@ 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 @@ } 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 @@ 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 { @@ -208,6 +219,51 @@ } w.Header().Add("Content-Type", "application/json") 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 @@ qlisten, err := reuseport.Listen("tcp", addr) 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) }