// Copyright (c) 2021 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ProtonMail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . package pmapi import ( "archive/zip" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "runtime" "strings" ) // ClientType is required by API. const ( EmailClientType = iota + 1 VPNClientType ) type reportAtt struct { name, filename string body io.Reader } // ReportReq stores data for report. type ReportReq struct { OS string `json:",omitempty"` OSVersion string `json:",omitempty"` Browser string `json:",omitempty"` BrowserVersion string `json:",omitempty"` BrowserExtensions string `json:",omitempty"` Resolution string `json:",omitempty"` DisplayMode string `json:",omitempty"` Client string `json:",omitempty"` ClientVersion string `json:",omitempty"` ClientType int `json:",omitempty"` Title string `json:",omitempty"` Description string `json:",omitempty"` Username string `json:",omitempty"` Email string `json:",omitempty"` Country string `json:",omitempty"` ISP string `json:",omitempty"` Debug string `json:",omitempty"` Attachments []reportAtt `json:",omitempty"` } // AddAttachment to report. func (rep *ReportReq) AddAttachment(name, filename string, r io.Reader) { rep.Attachments = append(rep.Attachments, reportAtt{name: name, filename: filename, body: r}) } func writeMultipartReport(w *multipart.Writer, rep *ReportReq) error { // nolint[funlen] fieldData := map[string]string{ "OS": rep.OS, "OSVersion": rep.OSVersion, "Browser": rep.Browser, "BrowserVersion": rep.BrowserVersion, "BrowserExtensions": rep.BrowserExtensions, "Resolution": rep.Resolution, "DisplayMode": rep.DisplayMode, "Client": rep.Client, "ClientVersion": rep.ClientVersion, "ClientType": "1", "Title": rep.Title, "Description": rep.Description, "Username": rep.Username, "Email": rep.Email, "Country": rep.Country, "ISP": rep.ISP, "Debug": rep.Debug, } for field, data := range fieldData { if data == "" { continue } if err := w.WriteField(field, data); err != nil { return err } } quoteEscaper := strings.NewReplacer("\\", "\\\\", `"`, "\\\"") for _, att := range rep.Attachments { h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, quoteEscaper.Replace(att.name), quoteEscaper.Replace(att.filename+".zip"))) h.Set("Content-Type", "application/octet-stream") // h.Set("Content-Transfer-Encoding", "base64") attWr, err := w.CreatePart(h) if err != nil { return err } zipArch := zip.NewWriter(attWr) zipWr, err := zipArch.Create(att.filename) // b64 := base64.NewEncoder(base64.StdEncoding, zipWr) if err != nil { return err } _, err = io.Copy(zipWr, att.body) if err != nil { return err } err = zipArch.Close() // err = b64.Close() if err != nil { return err } } return nil } // Report sends request as json or multipart (if has attachment). func (c *client) Report(rep ReportReq) (err error) { rep.Client = c.cm.config.ClientID rep.ClientVersion = c.cm.config.AppVersion rep.ClientType = EmailClientType var req *http.Request var w *MultipartWriter if len(rep.Attachments) > 0 { req, w, err = c.NewMultipartRequest("POST", "/reports/bug") } else { req, err = c.NewJSONRequest("POST", "/reports/bug", rep) } if err != nil { return } var res Res done := make(chan error, 1) go func() { done <- c.DoJSON(req, &res) }() if w != nil { err = writeMultipartReport(w.Writer, &rep) if err != nil { c.log.Errorln("report write: ", err) return } err = w.Close() if err != nil { c.log.Errorln("report close: ", err) return } } if err = <-done; err != nil { return } return res.Err() } // ReportCrash is old. Use sentry instead. func (c *client) ReportCrash(stacktrace string) (err error) { crashReq := ReportReq{ Client: c.cm.config.ClientID, ClientVersion: c.cm.config.AppVersion, ClientType: EmailClientType, OS: runtime.GOOS, Debug: stacktrace, } req, err := c.NewJSONRequest("POST", "/reports/crash", crashReq) if err != nil { return } var res Res if err = c.DoJSON(req, &res); err != nil { return } err = res.Err() return }