package server 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/core-go/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, srv string) 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(origErr.Error()) 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( "[%s] GraphQL query error: %v", srv, 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) }