blob: 1bc6479c3e6111289f5723cd78537daa725b405f [file] [log] [blame]
// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package dash
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/mail"
"strings"
"text/template"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
aemail "google.golang.org/appengine/mail"
)
// Email reporting interface.
func init() {
http.HandleFunc("/email_poll", handleEmailPoll)
http.HandleFunc("/_ah/mail/", handleIncomingMail)
mailingLists = make(map[string]bool)
for _, cfg := range config.Namespaces {
for _, reporting := range cfg.Reporting {
if cfg, ok := reporting.Config.(*EmailConfig); ok {
mailingLists[email.CanonicalEmail(cfg.Email)] = true
}
}
}
}
const emailType = "email"
var mailingLists map[string]bool
type EmailConfig struct {
Email string
Moderation bool
MailMaintainers bool
}
func (cfg *EmailConfig) Type() string {
return emailType
}
func (cfg *EmailConfig) NeedMaintainers() bool {
return cfg.MailMaintainers
}
func (cfg *EmailConfig) Validate() error {
if _, err := mail.ParseAddress(cfg.Email); err != nil {
return fmt.Errorf("bad email address %q: %v", cfg.Email, err)
}
if cfg.Moderation && cfg.MailMaintainers {
return fmt.Errorf("both Moderation and MailMaintainers set")
}
return nil
}
// handleEmailPoll is called by cron and sends emails for new bugs, if any.
func handleEmailPoll(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
if err := emailPollBugs(c); err != nil {
log.Errorf(c, "bug poll failed: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := emailPollJobs(c); err != nil {
log.Errorf(c, "job poll failed: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("OK"))
}
func emailPollBugs(c context.Context) error {
reports := reportingPoll(c, emailType)
for _, rep := range reports {
if err := emailReport(c, rep, "mail_bug.txt"); err != nil {
log.Errorf(c, "failed to report bug: %v", err)
continue
}
cmd := &dashapi.BugUpdate{
ID: rep.ID,
Status: dashapi.BugStatusOpen,
ReproLevel: dashapi.ReproLevelNone,
}
if len(rep.ReproC) != 0 {
cmd.ReproLevel = dashapi.ReproLevelC
} else if len(rep.ReproSyz) != 0 {
cmd.ReproLevel = dashapi.ReproLevelSyz
}
ok, reason, err := incomingCommand(c, cmd)
if !ok || err != nil {
log.Errorf(c, "failed to update reported bug: ok=%v reason=%v err=%v", ok, reason, err)
}
}
return nil
}
func emailPollJobs(c context.Context) error {
jobs, err := pollCompletedJobs(c, emailType)
if err != nil {
return err
}
for _, job := range jobs {
if err := emailReport(c, job, "mail_test_result.txt"); err != nil {
log.Errorf(c, "failed to report job: %v", err)
continue
}
if err := jobReported(c, job.JobID); err != nil {
log.Errorf(c, "failed to mark job reported: %v", err)
continue
}
}
return nil
}
func emailReport(c context.Context, rep *dashapi.BugReport, templ string) error {
cfg := new(EmailConfig)
if err := json.Unmarshal(rep.Config, cfg); err != nil {
return fmt.Errorf("failed to unmarshal email config: %v", err)
}
to := []string{cfg.Email}
if cfg.MailMaintainers {
to = append(to, rep.Maintainers...)
}
to = email.MergeEmailLists(to, rep.CC)
var attachments []aemail.Attachment
if len(rep.KernelConfig) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "config.txt",
Data: rep.KernelConfig,
})
}
if len(rep.Patch) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "patch.txt",
Data: rep.Patch,
})
}
if len(rep.Log) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "raw.log",
Data: rep.Log,
})
}
if len(rep.ReproSyz) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "repro.txt",
Data: rep.ReproSyz,
})
}
if len(rep.ReproC) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "repro.c",
Data: rep.ReproC,
})
}
// Build error output and failing VM boot log can be way too long to inline.
const maxInlineError = 16 << 10
errorText, errorTruncated := rep.Error, false
if len(errorText) > maxInlineError {
errorTruncated = true
attachments = append(attachments, aemail.Attachment{
Name: "error.txt",
Data: errorText,
})
errorText = errorText[len(errorText)-maxInlineError:]
}
from, err := email.AddAddrContext(fromAddr(c), rep.ID)
if err != nil {
return err
}
// Data passed to the template.
type BugReportData struct {
First bool
Moderation bool
Maintainers []string
CompilerID string
KernelRepo string
KernelBranch string
KernelCommit string
CrashTitle string
Report []byte
Error []byte
ErrorTruncated bool
HasLog bool
HasKernelConfig bool
ReproSyz bool
ReproC bool
}
data := &BugReportData{
First: rep.First,
Moderation: cfg.Moderation,
Maintainers: rep.Maintainers,
CompilerID: rep.CompilerID,
KernelRepo: rep.KernelRepo,
KernelBranch: rep.KernelBranch,
KernelCommit: rep.KernelCommit,
CrashTitle: rep.CrashTitle,
Report: rep.Report,
Error: errorText,
ErrorTruncated: errorTruncated,
HasLog: len(rep.Log) != 0,
HasKernelConfig: len(rep.KernelConfig) != 0,
ReproSyz: len(rep.ReproSyz) != 0,
ReproC: len(rep.ReproC) != 0,
}
log.Infof(c, "sending email %q to %q", rep.Title, to)
err = sendMailTemplate(c, rep.Title, from, to, rep.ExtID, attachments, templ, data)
if err != nil {
return err
}
return nil
}
// handleIncomingMail is the entry point for incoming emails.
func handleIncomingMail(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
if err := incomingMail(c, r); err != nil {
log.Errorf(c, "%v", err)
}
}
func incomingMail(c context.Context, r *http.Request) error {
msg, err := email.Parse(r.Body, fromAddr(c))
if err != nil {
return err
}
log.Infof(c, "received email: subject %q, from %q, cc %q, msg %q, bug %q, cmd %q, link %q",
msg.Subject, msg.From, msg.Cc, msg.MessageID, msg.BugID, msg.Command, msg.Link)
bug, bugReporting, reporting := loadBugInfo(c, msg)
if bug == nil {
return nil // error was already logged
}
_ = bugReporting
emailConfig := reporting.Config.(*EmailConfig)
mailingList := email.CanonicalEmail(emailConfig.Email)
fromMailingList := email.CanonicalEmail(msg.From) == mailingList
mailingListInCC := checkMailingListInCC(c, msg, mailingList)
// A mailing list can send us a duplicate email, to not process/reply
// to such duplicate emails, we ignore emails coming from our mailing lists.
if msg.Command == "test:" {
if fromMailingList {
if msg.Link != "" {
if err := updateTestJob(c, msg.MessageID, msg.Link); err != nil {
log.Errorf(c, "failed to update job: %v", err)
}
}
return nil
}
args := strings.Split(msg.CommandArgs, " ")
if len(args) != 2 {
return replyTo(c, msg, fmt.Sprintf("want 2 args (repo, branch), got %v",
len(args)), nil)
}
reply := handleTestRequest(c, msg.BugID, email.CanonicalEmail(msg.From),
msg.MessageID, msg.Patch, args[0], args[1])
if reply != "" {
return replyTo(c, msg, reply, nil)
}
if !mailingListInCC {
warnMailingListInCC(c, msg, mailingList)
}
return nil
}
if fromMailingList && msg.Command != "" {
log.Infof(c, "duplicate email from mailing list, ignoring")
return nil
}
cmd := &dashapi.BugUpdate{
ID: msg.BugID,
ExtID: msg.MessageID,
Link: msg.Link,
CC: msg.Cc,
}
switch msg.Command {
case "":
cmd.Status = dashapi.BugStatusUpdate
case "upstream":
cmd.Status = dashapi.BugStatusUpstream
case "invalid":
cmd.Status = dashapi.BugStatusInvalid
case "fix:":
if msg.CommandArgs == "" {
return replyTo(c, msg, fmt.Sprintf("no commit title"), nil)
}
cmd.Status = dashapi.BugStatusOpen
cmd.FixCommits = []string{msg.CommandArgs}
case "dup:":
if msg.CommandArgs == "" {
return replyTo(c, msg, fmt.Sprintf("no dup title"), nil)
}
cmd.Status = dashapi.BugStatusDup
cmd.DupOf = msg.CommandArgs
default:
return replyTo(c, msg, fmt.Sprintf("unknown command %q", msg.Command), nil)
}
ok, reply, err := incomingCommand(c, cmd)
if err != nil {
return nil // the error was already logged
}
if !ok && reply != "" {
return replyTo(c, msg, reply, nil)
}
if !mailingListInCC && msg.Command != "" {
warnMailingListInCC(c, msg, mailingList)
}
return nil
}
func loadBugInfo(c context.Context, msg *email.Email) (bug *Bug, bugReporting *BugReporting, reporting *Reporting) {
if msg.BugID == "" {
log.Warningf(c, "no bug ID (%q)", msg.Subject)
return nil, nil, nil
}
bug, _, err := findBugByReportingID(c, msg.BugID)
if err != nil {
log.Errorf(c, "can't find bug: %v", err)
replyTo(c, msg, "Can't find the corresponding bug.", nil)
return nil, nil, nil
}
bugReporting, _ = bugReportingByID(bug, msg.BugID)
if bugReporting == nil {
log.Errorf(c, "can't find bug reporting: %v", err)
replyTo(c, msg, "Can't find the corresponding bug.", nil)
return nil, nil, nil
}
reporting = config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
if reporting == nil {
log.Errorf(c, "can't find reporting for this bug: namespace=%q reporting=%q",
bug.Namespace, bugReporting.Name)
return nil, nil, nil
}
if reporting.Config.Type() != emailType {
log.Errorf(c, "reporting is not email: namespace=%q reporting=%q config=%q",
bug.Namespace, bugReporting.Name, reporting.Config.Type())
return nil, nil, nil
}
return bug, bugReporting, reporting
}
func checkMailingListInCC(c context.Context, msg *email.Email, mailingList string) bool {
if email.CanonicalEmail(msg.From) == mailingList {
return true
}
for _, cc := range msg.Cc {
if email.CanonicalEmail(cc) == mailingList {
return true
}
}
msg.Cc = append(msg.Cc, mailingList)
return false
}
func warnMailingListInCC(c context.Context, msg *email.Email, mailingList string) {
reply := fmt.Sprintf("Your '%v' command is accepted, but please keep %v mailing list"+
" in CC next time. It serves as a history of what happened with each bug report."+
" Thank you.",
msg.Command, mailingList)
if err := replyTo(c, msg, reply, nil); err != nil {
log.Errorf(c, "failed to send email reply: %v", err)
}
}
var mailTemplates = template.Must(template.New("").ParseGlob("mail_*.txt"))
func sendMailTemplate(c context.Context, subject, from string, to []string, replyTo string,
attachments []aemail.Attachment, template string, data interface{}) error {
body := new(bytes.Buffer)
if err := mailTemplates.ExecuteTemplate(body, template, data); err != nil {
return fmt.Errorf("failed to execute %v template: %v", template, err)
}
msg := &aemail.Message{
Sender: from,
To: to,
Subject: subject,
Body: body.String(),
Attachments: attachments,
}
if replyTo != "" {
msg.Headers = mail.Header{"In-Reply-To": []string{replyTo}}
}
return sendEmail(c, msg)
}
func replyTo(c context.Context, msg *email.Email, reply string, attachment *aemail.Attachment) error {
var attachments []aemail.Attachment
if attachment != nil {
attachments = append(attachments, *attachment)
}
from, err := email.AddAddrContext(fromAddr(c), msg.BugID)
if err != nil {
return err
}
log.Infof(c, "sending reply: to=%q cc=%q subject=%q reply=%q",
msg.From, msg.Cc, msg.Subject, reply)
replyMsg := &aemail.Message{
Sender: from,
To: []string{msg.From},
Cc: msg.Cc,
Subject: msg.Subject,
Body: email.FormReply(msg.Body, reply),
Attachments: attachments,
Headers: mail.Header{"In-Reply-To": []string{msg.MessageID}},
}
return sendEmail(c, replyMsg)
}
// Sends email, can be stubbed for testing.
var sendEmail = func(c context.Context, msg *aemail.Message) error {
if err := aemail.Send(c, msg); err != nil {
return fmt.Errorf("failed to send email: %v", err)
}
return nil
}
func fromAddr(c context.Context) string {
return fmt.Sprintf("\"syzbot\" <bot@%v.appspotmail.com>", appengine.AppID(c))
}