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