From fb5c5da69908b77db27394500e5e1967dfcc5b99 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 18 May 2020 17:00:20 -0400 Subject: [PATCH] API: move recover function into separate file --- api/go.mod | 4 +- api/graph/recover.go | 127 +++++++++++++++++++++++++++++++++++++++++++ api/server.go | 108 +----------------------------------- 3 files changed, 130 insertions(+), 109 deletions(-) create mode 100644 api/graph/recover.go diff --git a/api/go.mod b/api/go.mod index 2fe176c..437aae6 100644 --- a/api/go.mod +++ b/api/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/lib/pq v1.3.0 - github.com/martinlindhe/base36 v1.0.0 // indirect + github.com/martinlindhe/base36 v1.0.0 github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 // indirect github.com/mitchellh/mapstructure v1.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -25,6 +25,6 @@ require ( github.com/vektah/gqlparser/v2 v2.0.1 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97 // indirect - gopkg.in/mail.v2 v2.3.1 // indirect + gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/api/graph/recover.go b/api/graph/recover.go new file mode 100644 index 0000000..0f8b363 --- /dev/null +++ b/api/graph/recover.go @@ -0,0 +1,127 @@ +package graph + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "log" + "net/mail" + "os" + "runtime" + "strconv" + "time" + + "github.com/99designs/gqlgen/graphql" + "github.com/martinlindhe/base36" + "github.com/vaughan0/go-ini" + gomail "gopkg.in/mail.v2" + + "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" +) + +// Provides a graphql.RecoverFunc which will print the stack trace, and if +// debug mode is not enabled, email it to the administrator. +func EmailRecover(config ini.File, debug bool) graphql.RecoverFunc { + return func (ctx context.Context, _origErr interface{}) error { + var ( + ok bool + origErr error + ) + if origErr, ok = _origErr.(error); !ok { + log.Printf("Unexpected error in recover: %v\n", origErr) + return fmt.Errorf("internal system error") + } + + if errors.Is(origErr, context.Canceled) { + return origErr + } + + if errors.Is(origErr, context.DeadlineExceeded) { + return origErr + } + + if origErr.Error() == "pq: canceling statement due to user request" { + return origErr + } + + stack := make([]byte, 32768) // 32 KiB + i := runtime.Stack(stack, false) + log.Println(string(stack[:i])) + if debug { + return fmt.Errorf("internal system error") + } + + to, ok := config.Get("mail", "error-to") + if !ok { + return fmt.Errorf("internal system error") + } + from, _ := config.Get("mail", "error-from") + portStr, ok := config.Get("mail", "smtp-port") + if !ok { + return fmt.Errorf("internal system error") + } + port, _ := strconv.Atoi(portStr) + host, _ := config.Get("mail", "smtp-host") + user, _ := config.Get("mail", "smtp-user") + pass, _ := config.Get("mail", "smtp-password") + + m := gomail.NewMessage() + sender, err := mail.ParseAddress(from) + if err != nil { + log.Fatalf("Failed to parse sender address") + } + m.SetAddressHeader("From", sender.Address, sender.Name) + recipient, err := mail.ParseAddress(to) + if err != nil { + log.Fatalf("Failed to parse recipient address") + } + m.SetAddressHeader("To", recipient.Address, recipient.Name) + m.SetHeader("Message-ID", GenerateMessageID()) + m.SetHeader("Subject", fmt.Sprintf( + "[git.sr.ht] GraphQL query error: %v", origErr)) + + quser := auth.ForContext(ctx) + octx := graphql.GetOperationContext(ctx) + + m.SetBody("text/plain", fmt.Sprintf(`Error occured processing GraphQL request: + + %v + + When running the following query on behalf of %s <%s>: + + %s + + The following stack trace was produced: + + %s`, origErr, quser.Username, quser.Email, octx.RawQuery, string(stack[:i]))) + + d := gomail.NewDialer(host, port, user, pass) + if err := d.DialAndSend(m); err != nil { + log.Printf("Error sending email: %v\n", err) + } + return fmt.Errorf("internal system error") + } +} + +// Generates an RFC 2822-compliant Message-Id based on the informational draft +// "Recommendations for generating Message IDs", for lack of a better +// authoritative source. +func GenerateMessageID() string { + var ( + now bytes.Buffer + nonce []byte = make([]byte, 8) + ) + binary.Write(&now, binary.BigEndian, time.Now().UnixNano()) + rand.Read(nonce) + hostname, err := os.Hostname() + if err != nil { + hostname = "localhost" + } + return fmt.Sprintf("<%s.%s@%s>", + base36.EncodeBytes(now.Bytes()), + base36.EncodeBytes(nonce), + hostname) +} diff --git a/api/server.go b/api/server.go index da36707..e267447 100644 --- a/api/server.go +++ b/api/server.go @@ -1,31 +1,20 @@ package main import ( - "bytes" - "context" - "crypto/rand" "database/sql" - "encoding/binary" - "errors" - "fmt" "log" "net/http" - "net/mail" "os" - "runtime" "strconv" "time" "git.sr.ht/~sircmpwn/getopt" - "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/handler" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" - "github.com/martinlindhe/base36" "github.com/vaughan0/go-ini" _ "github.com/lib/pq" - gomail "gopkg.in/mail.v2" "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" "git.sr.ht/~sircmpwn/git.sr.ht/api/crypto" @@ -112,82 +101,7 @@ func main() { srv := handler.GraphQL( api.NewExecutableSchema(gqlConfig), handler.ComplexityLimit(complexity), - handler.RecoverFunc(func(ctx context.Context, _origErr interface{}) error { - var origErr error - if origErr, ok = _origErr.(error); !ok { - log.Printf("Unexpected error in recover: %v\n", origErr) - return fmt.Errorf("internal system error") - } - - if errors.Is(origErr, context.Canceled) { - return origErr - } - - if errors.Is(origErr, context.DeadlineExceeded) { - return origErr - } - - if origErr.Error() == "pq: canceling statement due to user request" { - return origErr - } - - stack := make([]byte, 32768) // 32 KiB - i := runtime.Stack(stack, false) - log.Println(string(stack[:i])) - if debug { - return fmt.Errorf("internal system error") - } - - to, ok := config.Get("mail", "error-to") - if !ok { - return fmt.Errorf("internal system error") - } - from, _ := config.Get("mail", "error-from") - portStr, ok := config.Get("mail", "smtp-port") - if !ok { - return fmt.Errorf("internal system error") - } - port, _ := strconv.Atoi(portStr) - host, _ := config.Get("mail", "smtp-host") - user, _ := config.Get("mail", "smtp-user") - pass, _ := config.Get("mail", "smtp-password") - - m := gomail.NewMessage() - sender, err := mail.ParseAddress(from) - if err != nil { - log.Fatalf("Failed to parse sender address") - } - m.SetAddressHeader("From", sender.Address, sender.Name) - recipient, err := mail.ParseAddress(to) - if err != nil { - log.Fatalf("Failed to parse recipient address") - } - m.SetAddressHeader("To", recipient.Address, recipient.Name) - m.SetHeader("Message-ID", GenerateMessageID()) - m.SetHeader("Subject", fmt.Sprintf( - "[git.sr.ht] GraphQL query error: %v", origErr)) - - quser := auth.ForContext(ctx) - octx := graphql.GetOperationContext(ctx) - - m.SetBody("text/plain", fmt.Sprintf(`Error occured processing GraphQL request: - -%v - -When running the following query on behalf of %s <%s>: - -%s - -The following stack trace was produced: - -%s`, origErr, quser.Username, quser.Email, octx.RawQuery, string(stack[:i]))) - - d := gomail.NewDialer(host, port, user, pass) - if err := d.DialAndSend(m); err != nil { - log.Printf("Error sending email: %v\n", err) - } - return fmt.Errorf("internal system error") - })) + handler.RecoverFunc(graph.EmailRecover(config, debug))) router.Handle("/query", srv) @@ -198,23 +112,3 @@ The following stack trace was produced: log.Printf("running on %s", addr) log.Fatal(http.ListenAndServe(addr, router)) } - -// Generates an RFC 2822-compliant Message-Id based on the informational draft -// "Recommendations for generating Message IDs", for lack of a better -// authoritative source. -func GenerateMessageID() string { - var ( - now bytes.Buffer - nonce []byte = make([]byte, 8) - ) - binary.Write(&now, binary.BigEndian, time.Now().UnixNano()) - rand.Read(nonce) - hostname, err := os.Hostname() - if err != nil { - hostname = "localhost" - } - return fmt.Sprintf("<%s.%s@%s>", - base36.EncodeBytes(now.Bytes()), - base36.EncodeBytes(nonce), - hostname) -} -- 2.38.4