forked from Silverfish/proton-bridge
Merge branch 'release/congo' into devel
This commit is contained in:
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package args
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FilterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
|
||||
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
|
||||
func FilterProcessSerialNumberFromArgs() {
|
||||
tmp := os.Args[:0]
|
||||
for _, arg := range os.Args {
|
||||
if !strings.Contains(arg, "-psn_") {
|
||||
tmp = append(tmp, arg)
|
||||
}
|
||||
}
|
||||
os.Args = tmp
|
||||
}
|
||||
@ -200,7 +200,7 @@ func (c *Config) GetTLSKeyPath() string {
|
||||
|
||||
// GetDBDir returns folder for db files.
|
||||
func (c *Config) GetDBDir() string {
|
||||
return filepath.Join(c.appDirsVersion.UserCache())
|
||||
return c.appDirsVersion.UserCache()
|
||||
}
|
||||
|
||||
// GetEventsPath returns path to events file containing the last processed event IDs.
|
||||
@ -228,6 +228,11 @@ func (c *Config) GetPreferencesPath() string {
|
||||
return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json")
|
||||
}
|
||||
|
||||
// GetTransferDir returns folder for import-export rules files.
|
||||
func (c *Config) GetTransferDir() string {
|
||||
return c.appDirsVersion.UserCache()
|
||||
}
|
||||
|
||||
// GetDefaultAPIPort returns default Bridge local API port.
|
||||
func (c *Config) GetDefaultAPIPort() int {
|
||||
return 1042
|
||||
|
||||
@ -29,9 +29,6 @@ var (
|
||||
// BuildTime stamp of the build.
|
||||
BuildTime = ""
|
||||
|
||||
// AppShortName to make setup.
|
||||
AppShortName = "bridge"
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att
|
||||
dr = r
|
||||
err = nil
|
||||
att.Name += ".gpg"
|
||||
att.MIMEType = "application/pgp-encrypted"
|
||||
att.MIMEType = "application/pgp-encrypted" //nolint
|
||||
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
err = fmt.Errorf("cannot decrypt attachment: %v", err)
|
||||
return
|
||||
|
||||
348
pkg/message/build.go
Normal file
348
pkg/message/build.go
Normal file
@ -0,0 +1,348 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-textwrapper"
|
||||
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
||||
)
|
||||
|
||||
// Builder for converting PM message to RFC822. Builder will directly write
|
||||
// changes to message when fetching or building message.
|
||||
type Builder struct {
|
||||
cl pmapi.Client
|
||||
msg *pmapi.Message
|
||||
|
||||
EncryptedToHTML bool
|
||||
successfullyDecrypted bool
|
||||
}
|
||||
|
||||
// NewBuilder initiated with client and message meta info.
|
||||
func NewBuilder(client pmapi.Client, message *pmapi.Message) *Builder {
|
||||
return &Builder{cl: client, msg: message, EncryptedToHTML: true, successfullyDecrypted: false}
|
||||
}
|
||||
|
||||
// fetchMessage will update original PM message if successful
|
||||
func (bld *Builder) fetchMessage() (err error) {
|
||||
if bld.msg.Body != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
complete, err := bld.cl.GetMessage(bld.msg.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
*bld.msg = *complete
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bld *Builder) writeMessageBody(w io.Writer) error {
|
||||
if err := bld.fetchMessage(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := bld.WriteBody(w)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
if bld.EncryptedToHTML {
|
||||
_ = CustomMessage(bld.msg, err, true)
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bld *Builder) writeAttachmentBody(w io.Writer, att *pmapi.Attachment) error {
|
||||
// Retrieve encrypted attachment
|
||||
r, err := bld.cl.GetAttachment(att.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close() //nolint[errcheck]
|
||||
|
||||
if err := bld.WriteAttachmentBody(w, att, r); err != nil {
|
||||
// Returning an error here makes e-mail clients like Thunderbird behave
|
||||
// badly, trying to retrieve the message again and again
|
||||
log.Warnln("Cannot write attachment body:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bld *Builder) writeRelatedPart(p io.Writer, inlines []*pmapi.Attachment) error {
|
||||
related := multipart.NewWriter(p)
|
||||
|
||||
_ = related.SetBoundary(GetRelatedBoundary(bld.msg))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := bld.writeMessageBody(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
h := GetBodyHeader(bld.msg)
|
||||
|
||||
var err error
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(p)
|
||||
|
||||
for _, inline := range inlines {
|
||||
buf = &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, inline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := GetAttachmentHeader(inline)
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = buf.WriteTo(p)
|
||||
}
|
||||
|
||||
_ = related.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildMessage converts PM message to body structure (not RFC3501) and bytes
|
||||
// of RC822 message. If successful the original PM message will contain decrypted body.
|
||||
func (bld *Builder) BuildMessage() (structure *BodyStructure, message []byte, err error) { //nolint[funlen]
|
||||
if err = bld.fetchMessage(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
|
||||
mainHeader := GetHeader(bld.msg)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(bld.msg))
|
||||
if err = WriteHeader(bodyBuf, mainHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n")
|
||||
|
||||
// NOTE: Do we really need extra encapsulation? i.e. Bridge-IMAP message is always multipart/mixed
|
||||
|
||||
if bld.msg.MIMEType == pmapi.ContentTypeMultipartMixed {
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"\r\n")
|
||||
if err = bld.writeMessageBody(bodyBuf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"--\r\n")
|
||||
} else {
|
||||
mw := multipart.NewWriter(bodyBuf)
|
||||
_ = mw.SetBoundary(GetBoundary(bld.msg))
|
||||
|
||||
var partWriter io.Writer
|
||||
atts, inlines := SeparateInlineAttachments(bld.msg)
|
||||
|
||||
if len(inlines) > 0 {
|
||||
relatedHeader := GetRelatedHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_ = bld.writeRelatedPart(partWriter, inlines)
|
||||
} else {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeMessageBody(buf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
bodyHeader := GetBodyHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
|
||||
// Write the attachments parts
|
||||
for _, att := range atts {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, att); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
attachmentHeader := GetAttachmentHeader(att)
|
||||
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
|
||||
_ = mw.Close()
|
||||
}
|
||||
|
||||
// wee need to copy buffer before building body structure
|
||||
message = bodyBuf.Bytes()
|
||||
structure, err = NewBodyStructure(bodyBuf)
|
||||
return structure, message, err
|
||||
}
|
||||
|
||||
// SuccessfullyDecrypted is true when message was fetched and decrypted successfully
|
||||
func (bld *Builder) SuccessfullyDecrypted() bool { return bld.successfullyDecrypted }
|
||||
|
||||
// WriteBody decrypts PM message and writes main body section. The external PGP
|
||||
// message is written as is (including attachments)
|
||||
func (bld *Builder) WriteBody(w io.Writer) error {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// decrypt body
|
||||
if err := bld.msg.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
return err
|
||||
}
|
||||
bld.successfullyDecrypted = true
|
||||
if bld.msg.MIMEType != pmapi.ContentTypeMultipartMixed {
|
||||
// transfer encoding
|
||||
qp := quotedprintable.NewWriter(w)
|
||||
if _, err := io.WriteString(qp, bld.msg.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteAttachmentBody decrypts and writes the attachments
|
||||
func (bld *Builder) WriteAttachmentBody(w io.Writer, att *pmapi.Attachment, attReader io.Reader) (err error) {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Decrypt it
|
||||
var dr io.Reader
|
||||
dr, err = att.Decrypt(attReader, kr)
|
||||
if err == openpgperrors.ErrKeyIncorrect {
|
||||
// Do not fail if attachment is encrypted with a different key
|
||||
dr = attReader
|
||||
err = nil
|
||||
att.Name += ".gpg"
|
||||
att.MIMEType = "application/pgp-encrypted"
|
||||
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
err = fmt.Errorf("cannot decrypt attachment: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// transfer encoding
|
||||
ww := textwrapper.NewRFC822(w)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
var n int64
|
||||
if n, err = io.Copy(bw, dr); err != nil {
|
||||
err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
|
||||
}
|
||||
|
||||
_ = bw.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint[funlen]
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
// Overwrite content for main header for import.
|
||||
// Even if message has just simple body we should upload as multipart/mixed.
|
||||
// Each part has encrypted body and header reflects the original header.
|
||||
mainHeader := GetHeader(m)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(m))
|
||||
mainHeader.Del("Content-Disposition")
|
||||
mainHeader.Del("Content-Transfer-Encoding")
|
||||
if err := WriteHeader(b, mainHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mw := multipart.NewWriter(b)
|
||||
if err := mw.SetBoundary(GetBoundary(m)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the body part.
|
||||
bodyHeader := make(textproto.MIMEHeader)
|
||||
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
|
||||
bodyHeader.Set("Content-Disposition", "inline")
|
||||
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
|
||||
|
||||
p, err := mw.CreatePart(bodyHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// First, encrypt the message body.
|
||||
if err := m.Encrypt(kr, kr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(p, m.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the attachments parts.
|
||||
for i := 0; i < len(m.Attachments); i++ {
|
||||
att := m.Attachments[i]
|
||||
r := readers[i]
|
||||
h := GetAttachmentHeader(att)
|
||||
p, err := mw.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create line wrapper writer.
|
||||
ww := textwrapper.NewRFC822(p)
|
||||
|
||||
// Create base64 writer.
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create encrypted writer.
|
||||
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := mw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
@ -23,8 +23,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals]
|
||||
|
||||
func GetBoundary(m *pmapi.Message) string {
|
||||
// The boundary needs to be deterministic because messages are not supposed to
|
||||
// change.
|
||||
|
||||
@ -37,7 +37,6 @@ import (
|
||||
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/jaytaylor/html2text"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseAttachment(filename string, mediaType string, h textproto.MIMEHeader) (att *pmapi.Attachment) {
|
||||
|
||||
85
pkg/message/utils.go
Normal file
85
pkg/message/utils.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func WriteHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
|
||||
if err = http.Header(h).Write(w); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.WriteString(w, "\r\n")
|
||||
return
|
||||
}
|
||||
|
||||
const customMessageTemplate = `
|
||||
<html>
|
||||
<head></head>
|
||||
<body style="font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px;">
|
||||
<div style="color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;">
|
||||
<strong>Decryption error</strong><br/>
|
||||
Decryption of this message's encrypted content failed.
|
||||
<pre>{{.Error}}</pre>
|
||||
</div>
|
||||
|
||||
{{if .AttachBody}}
|
||||
<div style="color:#333; background-color:#f4f4f4; border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;">
|
||||
<pre>{{.Body}}</pre>
|
||||
</div>
|
||||
{{- end}}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
type customMessageData struct {
|
||||
Error string
|
||||
AttachBody bool
|
||||
Body string
|
||||
}
|
||||
|
||||
func CustomMessage(m *pmapi.Message, decodeError error, attachBody bool) error {
|
||||
t := template.Must(template.New("customMessage").Parse(customMessageTemplate))
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
if err := t.Execute(b, customMessageData{
|
||||
Error: decodeError.Error(),
|
||||
AttachBody: attachBody,
|
||||
Body: m.Body,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.MIMEType = pmapi.ContentTypeHTML
|
||||
m.Body = b.String()
|
||||
|
||||
// NOTE: we need to set header in custom message header, so we check that is non-nil.
|
||||
if m.Header == nil {
|
||||
m.Header = make(mail.Header)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -95,6 +95,15 @@ func (l AddressList) ByID(id string) *Address {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllEmails returns all emails.
|
||||
func (l AddressList) AllEmails() (addresses []string) {
|
||||
for _, a := range l {
|
||||
addresses = append(addresses, a.Email)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ActiveEmails returns only active emails.
|
||||
func (l AddressList) ActiveEmails() (addresses []string) {
|
||||
for _, a := range l {
|
||||
if a.Receive == CanReceive {
|
||||
|
||||
@ -173,26 +173,6 @@ func (c *client) Report(rep ReportReq) (err error) {
|
||||
return res.Err()
|
||||
}
|
||||
|
||||
// ReportBug is old. Use Report instead.
|
||||
func (c *client) ReportBug(os, osVersion, title, description, username, email string) (err error) {
|
||||
return c.ReportBugWithEmailClient(os, osVersion, title, description, username, email, "")
|
||||
}
|
||||
|
||||
// ReportBugWithEmailClient is old. Use Report instead.
|
||||
func (c *client) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) (err error) {
|
||||
bugReq := ReportReq{
|
||||
OS: os,
|
||||
OSVersion: osVersion,
|
||||
Browser: emailClient,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Username: username,
|
||||
Email: email,
|
||||
}
|
||||
|
||||
return c.Report(bugReq)
|
||||
}
|
||||
|
||||
// ReportCrash is old. Use sentry instead.
|
||||
func (c *client) ReportCrash(stacktrace string) (err error) {
|
||||
crashReq := ReportReq{
|
||||
|
||||
@ -27,19 +27,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testBugsReportReq = ReportReq{
|
||||
OS: "Mac OSX",
|
||||
OSVersion: "10.11.6",
|
||||
Client: "demoapp",
|
||||
ClientVersion: "GoPMAPI_1.0.14",
|
||||
ClientType: 1,
|
||||
Title: "Big Bug",
|
||||
Description: "Cannot fetch new messages",
|
||||
Username: "apple",
|
||||
Email: "apple@gmail.com",
|
||||
}
|
||||
|
||||
var testBugsReportReqWithEmailClient = ReportReq{
|
||||
var testBugReportReq = ReportReq{
|
||||
OS: "Mac OSX",
|
||||
OSVersion: "10.11.6",
|
||||
Browser: "AppleMail",
|
||||
@ -67,31 +55,6 @@ const testBugsBody = `{
|
||||
|
||||
const testAttachmentJSONZipped = "PK\x03\x04\x14\x00\b\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00last.log\\Rَ\xaaH\x00}ﯨ\xf8r\x1f\xeeܖED;\xe9\ap\x03\x11\x11\x97\x0e8\x99L\xb0(\xa1\xa0\x16\x85b\x91I\xff\xfbD{\x99\xc9}\xab:K\x9d\xa4\xce\xf9\xe7\t\x00\x00z\xf6\xb4\xf7\x02z\xb7a\xe5\xd8\x04*V̭\x8d\xd1lvE}\xd6\xe3\x80\x1f\xd7nX\x9bI[\xa6\xe1a=\xd4a\xa8M\x97\xd9J\xf1F\xeb\x105U\xbd\xb0`XO\xce\xf1hu\x99q\xc3\xfe{\x11ߨ'-\v\x89Z\xa4\x9c5\xaf\xaf\xbd?>R\xd6\x11E\xf7\x1cX\xf0JpF#L\x9eE+\xbe\xe8\x1d\xee\ued2e\u007f\xde]\u06dd\xedo\x97\x87E\xa0V\xf4/$\xc2\xecK\xed\xa0\xdb&\x829\x12\xe5\x9do\xa0\xe9\x1a\xd2\x19\x1e\xf5`\x95гb\xf8\x89\x81\xb7\xa5G\x18\x95\xf3\x9d9\xe8\x93B\x17!\x1a^\xccr\xbb`\xb2\xb4\xb86\x87\xb4h\x0e\xda\xc6u<+\x9e$̓\x95\xccSo\xea\xa4\xdbH!\xe9g\x8b\xd4\b\xb3hܬ\xa6Wk\x14He\xae\x8aPU\xaa\xc1\xee$\xfbH\xb3\xab.I\f<\x89\x06q\xe3-3-\x99\xcdݽ\xe5v\x99\xedn\xac\xadn\xe8Rp=\xb4nJ\xed\xd5\r\x8d\xde\x06Ζ\xf6\xb3\x01\x94\xcb\xf6\xd4\x19r\xe1\xaa$4+\xeaW\xa6F\xfa0\x97\x9cD\f\x8e\xd7\xd6z\v,G\xf3e2\xd4\xe6V\xba\v\xb6\xd9\xe8\xca*\x16\x95V\xa4J\xfbp\xddmF\x8c\x9a\xc6\xc8Č-\xdb\v\xf6\xf5\xf9\x02*\x15e\x874\xc9\xe7\"\xa3\x1an\xabq}ˊq\x957\xd3\xfd\xa91\x82\xe0Lß\\\x17\x8e\x9e_\xed`\t\xe9~5̕\x03\x9a\f\xddN6\xa2\xc4\x17\xdb\xc9V\x1c~\x9e\xea\xbe\xda-xv\xed\x8b\xe2\xc8DŽS\x95E6\xf2\xc3H\x1d:HPx\xc9\x14\xbfɒ\xff\xea\xb4P\x14\xa3\xe2\xfe\xfd\x1f+z\x80\x903\x81\x98\xf8\x15\xa3\x12\x16\xf8\"0g\xf7~B^\xfd \x040T\xa3\x02\x9c\x10\xc1\xa8F\xa0I#\xf1\xa3\x04\x98\x01\x91\xe2\x12\xdc;\x06gL\xd0g\xc0\xe3\xbd\xf6\xd7}&\xa8轀?\xbfяy`X\xf0\x92\x9f\x05\xf0*A8ρ\xac=K\xff\xf3\xfe\xa6Z\xe1\x1a\x017\xc2\x04\f\x94g\xa9\xf7-\xfb\xebqz\u007fz\u007f\xfa7\x00\x00\xff\xffPK\a\b\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00PK\x01\x02\x14\x00\x14\x00\b\x00\b\x00\x00\x00\x00\x00\xf5\\\v\xe5I\x02\x00\x00\r\x03\x00\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00last.logPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x006\x00\x00\x00\u007f\x02\x00\x00\x00\x00" //nolint[misspell]
|
||||
|
||||
func TestClient_BugReport(t *testing.T) {
|
||||
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
|
||||
Ok(t, isAuthReq(r, testUID, testAccessToken))
|
||||
|
||||
var bugsReportReq ReportReq
|
||||
Ok(t, json.NewDecoder(r.Body).Decode(&bugsReportReq))
|
||||
Equals(t, testBugsReportReq, bugsReportReq)
|
||||
|
||||
fmt.Fprint(w, testBugsBody)
|
||||
}))
|
||||
defer s.Close()
|
||||
c.uid = testUID
|
||||
c.accessToken = testAccessToken
|
||||
|
||||
Ok(t, c.ReportBug(
|
||||
testBugsReportReq.OS,
|
||||
testBugsReportReq.OSVersion,
|
||||
testBugsReportReq.Title,
|
||||
testBugsReportReq.Description,
|
||||
testBugsReportReq.Username,
|
||||
testBugsReportReq.Email,
|
||||
))
|
||||
}
|
||||
|
||||
func TestClient_BugReportWithAttachment(t *testing.T) {
|
||||
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
|
||||
@ -100,15 +63,15 @@ func TestClient_BugReportWithAttachment(t *testing.T) {
|
||||
Ok(t, r.ParseMultipartForm(10*1024))
|
||||
|
||||
for field, expected := range map[string]string{
|
||||
"OS": testBugsReportReq.OS,
|
||||
"OSVersion": testBugsReportReq.OSVersion,
|
||||
"Client": testBugsReportReq.Client,
|
||||
"ClientVersion": testBugsReportReq.ClientVersion,
|
||||
"ClientType": fmt.Sprintf("%d", testBugsReportReq.ClientType),
|
||||
"Title": testBugsReportReq.Title,
|
||||
"Description": testBugsReportReq.Description,
|
||||
"Username": testBugsReportReq.Username,
|
||||
"Email": testBugsReportReq.Email,
|
||||
"OS": testBugReportReq.OS,
|
||||
"OSVersion": testBugReportReq.OSVersion,
|
||||
"Client": testBugReportReq.Client,
|
||||
"ClientVersion": testBugReportReq.ClientVersion,
|
||||
"ClientType": fmt.Sprintf("%d", testBugReportReq.ClientType),
|
||||
"Title": testBugReportReq.Title,
|
||||
"Description": testBugReportReq.Description,
|
||||
"Username": testBugReportReq.Username,
|
||||
"Email": testBugReportReq.Email,
|
||||
} {
|
||||
if r.PostFormValue(field) != expected {
|
||||
t.Errorf("Field %q has %q but expected %q", field, r.PostFormValue(field), expected)
|
||||
@ -129,20 +92,20 @@ func TestClient_BugReportWithAttachment(t *testing.T) {
|
||||
c.uid = testUID
|
||||
c.accessToken = testAccessToken
|
||||
|
||||
rep := testBugsReportReq
|
||||
rep := testBugReportReq
|
||||
rep.AddAttachment("log", "last.log", strings.NewReader(testAttachmentJSON))
|
||||
|
||||
Ok(t, c.Report(rep))
|
||||
}
|
||||
|
||||
func TestClient_BugReportWithEmailClient(t *testing.T) {
|
||||
func TestClient_BugReport(t *testing.T) {
|
||||
s, c := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Ok(t, checkMethodAndPath(r, "POST", "/reports/bug"))
|
||||
Ok(t, isAuthReq(r, testUID, testAccessToken))
|
||||
|
||||
var bugsReportReq ReportReq
|
||||
Ok(t, json.NewDecoder(r.Body).Decode(&bugsReportReq))
|
||||
Equals(t, testBugsReportReqWithEmailClient, bugsReportReq)
|
||||
Equals(t, testBugReportReq, bugsReportReq)
|
||||
|
||||
fmt.Fprint(w, testBugsBody)
|
||||
}))
|
||||
@ -150,15 +113,17 @@ func TestClient_BugReportWithEmailClient(t *testing.T) {
|
||||
c.uid = testUID
|
||||
c.accessToken = testAccessToken
|
||||
|
||||
Ok(t, c.ReportBugWithEmailClient(
|
||||
testBugsReportReqWithEmailClient.OS,
|
||||
testBugsReportReqWithEmailClient.OSVersion,
|
||||
testBugsReportReqWithEmailClient.Title,
|
||||
testBugsReportReqWithEmailClient.Description,
|
||||
testBugsReportReqWithEmailClient.Username,
|
||||
testBugsReportReqWithEmailClient.Email,
|
||||
testBugsReportReqWithEmailClient.Browser,
|
||||
))
|
||||
r := ReportReq{
|
||||
OS: testBugReportReq.OS,
|
||||
OSVersion: testBugReportReq.OSVersion,
|
||||
Browser: testBugReportReq.Browser,
|
||||
Title: testBugReportReq.Title,
|
||||
Description: testBugReportReq.Description,
|
||||
Username: testBugReportReq.Username,
|
||||
Email: testBugReportReq.Email,
|
||||
}
|
||||
|
||||
Ok(t, c.Report(r))
|
||||
}
|
||||
|
||||
func TestClient_BugsCrash(t *testing.T) {
|
||||
|
||||
@ -67,7 +67,7 @@ type Client interface {
|
||||
DeleteLabel(labelID string) error
|
||||
EmptyFolder(labelID string, addressID string) error
|
||||
|
||||
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
|
||||
Report(report ReportReq) error
|
||||
SendSimpleMetric(category, action, label string) error
|
||||
|
||||
GetMailSettings() (MailSettings, error)
|
||||
|
||||
@ -317,9 +317,8 @@ func (cm *ClientManager) CheckConnection() error {
|
||||
retStatus := make(chan error)
|
||||
retAPI := make(chan error)
|
||||
|
||||
// Check protonstatus.com without SSL for performance reasons. vpn_status endpoint is fast and
|
||||
// returns only OK; this endpoint is not known by the public. We check the connection only.
|
||||
go checkConnection(client, "http://protonstatus.com/vpn_status", retStatus)
|
||||
// vpn_status endpoint is fast and returns only OK. We check the connection only.
|
||||
go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
|
||||
|
||||
// Check of API reachability also uses a fast endpoint.
|
||||
go checkConnection(client, cm.GetRootURL()+"/tests/ping", retAPI)
|
||||
@ -347,6 +346,14 @@ func (cm *ClientManager) CheckConnection() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckConnection returns an error if there is no internet connection.
|
||||
func CheckConnection() error {
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
retStatus := make(chan error)
|
||||
go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
|
||||
return <-retStatus
|
||||
}
|
||||
|
||||
func checkConnection(client *http.Client, url string, errorChannel chan error) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
@ -445,11 +452,10 @@ func (cm *ClientManager) HandleAuth(ca ClientAuth) {
|
||||
if ca.Auth == nil {
|
||||
cm.clearToken(ca.UserID)
|
||||
go cm.LogoutClient(ca.UserID)
|
||||
return
|
||||
} else {
|
||||
cm.setToken(ca.UserID, ca.Auth.GenToken(), time.Duration(ca.Auth.ExpiresIn)*time.Second)
|
||||
}
|
||||
|
||||
cm.setToken(ca.UserID, ca.Auth.GenToken(), time.Duration(ca.Auth.ExpiresIn)*time.Second)
|
||||
|
||||
logrus.Debug("ClientManager is forwarding auth update...")
|
||||
cm.authUpdates <- ca
|
||||
logrus.Debug("Auth update was forwarded")
|
||||
|
||||
@ -95,6 +95,11 @@ type ImportMsgReq struct {
|
||||
LabelIDs []string
|
||||
}
|
||||
|
||||
func (req ImportMsgReq) String() string {
|
||||
data, _ := json.Marshal(req)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ImportRes is a response to an import request.
|
||||
type ImportRes struct {
|
||||
Res
|
||||
|
||||
@ -175,3 +175,21 @@ func (c *client) DeleteLabel(id string) (err error) {
|
||||
err = res.Err()
|
||||
return
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label
|
||||
func LeastUsedColor(colors []string) (color string) {
|
||||
color = LabelColors[0]
|
||||
frequency := map[string]int{}
|
||||
|
||||
for _, c := range colors {
|
||||
frequency[c]++
|
||||
}
|
||||
|
||||
for _, c := range LabelColors {
|
||||
if frequency[color] > frequency[c] {
|
||||
color = c
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testLabelsBody = `{
|
||||
@ -184,3 +186,17 @@ func TestClient_DeleteLabel(t *testing.T) {
|
||||
t.Fatal("Expected no error while deleting label, got:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeastUsedColor(t *testing.T) {
|
||||
// No colors at all, should use first available color
|
||||
colors := []string{}
|
||||
r.Equal(t, "#7272a7", LeastUsedColor(colors))
|
||||
|
||||
// All colors have same frequency, should use first available color
|
||||
colors = []string{"#7272a7", "#cf5858", "#c26cc7", "#7569d1", "#69a9d1", "#5ec7b7", "#72bb75", "#c3d261", "#e6c04c", "#e6984c", "#8989ac", "#cf7e7e", "#c793ca", "#9b94d1", "#a8c4d5", "#97c9c1", "#9db99f", "#c6cd97", "#e7d292", "#dfb286"}
|
||||
r.Equal(t, "#7272a7", LeastUsedColor(colors))
|
||||
|
||||
// First three colors already used, but others wasn't. Should use first non-used one.
|
||||
colors = []string{"#7272a7", "#cf5858", "#c26cc7"}
|
||||
r.Equal(t, "#7569d1", LeastUsedColor(colors))
|
||||
}
|
||||
|
||||
@ -5,12 +5,11 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
|
||||
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockClient is a mock of Client interface
|
||||
@ -601,18 +600,18 @@ func (mr *MockClientMockRecorder) ReorderAddresses(arg0 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderAddresses", reflect.TypeOf((*MockClient)(nil).ReorderAddresses), arg0)
|
||||
}
|
||||
|
||||
// ReportBugWithEmailClient mocks base method
|
||||
func (m *MockClient) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error {
|
||||
// Report mocks base method
|
||||
func (m *MockClient) Report(arg0 pmapi.ReportReq) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
|
||||
ret := m.ctrl.Call(m, "Report", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient
|
||||
func (mr *MockClientMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
|
||||
// Report indicates an expected call of Report
|
||||
func (mr *MockClientMockRecorder) Report(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockClient)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Report", reflect.TypeOf((*MockClient)(nil).Report), arg0)
|
||||
}
|
||||
|
||||
// SendMessage mocks base method
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFo9OeEBEAC+fPrLcUBY+YUc5YiMrYJQ6ogrJWMGC00h9fAv3PsrHkBz0z7c
|
||||
QFDyNdNatokFDtZDX115M0vzDwk5NkcjmO7CWbf6nCZcwYqOSrBoH8wNT9uTS/6p
|
||||
R3AHk1r3C/36QG3iWx6Wg4ycRkXWYToT3/yh5waE5BbLi/9TSBAdfJzTyxt4IpZG
|
||||
3OTMnOwuz6eNRWVHkA48CJydWS6M8z+jIsBwFq4nOIChvLjIF42PuAT1VaiCYSmy
|
||||
4sU1YxxWof5z9HY0XghRpd7aUIgzAIsXUbaEXh/3iCZDUMN5LwkyAn+r5j3SMNzk
|
||||
2htF8V7qWE8ldYNVrpeEwyor0x1wMzpbb/C4Y8wXe8rP01d0ApiHVRETzsQk2esf
|
||||
XuSrBCtpyLc6ET1lluiL2sVUUelAPueUQlOyYXfL2X958i0TgBCi6QRPXxbPjCPs
|
||||
d1UzLPCSUNUO+/7fslZCax26d1r1kbHzJLAN1Jer6rxoEDaEiVSCUTnHgykCq5rO
|
||||
C3PScGEdOaIi4H5c6YFZrLmdz409YmJEWLKIPV/u5DpI+YGmAfAevrjkMBgQBOmZ
|
||||
D8Gp19LnRtmqjVh2rVdr8yc5nAjoNOZwanMwD5vCWPUVELWXubNFBv8hqZMxHZqW
|
||||
GrB8x8hkdgiNmuyqsxzBmOEJHWLlvbFhvHhIedT8paU/spL/qJmWp3EB4QARAQAB
|
||||
tExQcm90b24gVGVjaG5vbG9naWVzIEFHIChQcm90b25NYWlsIEJyaWRnZSBkZXZl
|
||||
bG9wZXJzKSA8YnJpZGdlQHByb3Rvbm1haWwuY2g+iQJUBBMBCAA+AhsDBQsJCAcC
|
||||
BhUICQoLAgQWAgMBAh4BAheAFiEE1R5k0+Y+3D7veGTO4sddaOYjSwcFAlv377wF
|
||||
CQO83tsACgkQ4sddaOYjSwfhng//WNhZqr0StuN4KbYdQG+FY+aLijLhiVI3i4j6
|
||||
wUis+7UWFNMUGePsBUrF7zOrzo4Vp16FSRhhpveIbDMVJg4yGlzwN+jZr9FBvF8z
|
||||
kbOqjajkTF3rOyqSQCpZVgeamRt6c4gGQTOwfwxB4K5mVg4rv65ISIKjLUtCZ27g
|
||||
pD6eJs25LhyZQnI65JHpHDkVar7oQ2nbWv0tn2wrrUKBE9hRM5Jn1xGaHYkrYxPe
|
||||
HNDHrqxJUDbPfJhca54M99bs9Qum3KkT1WWU5/0trA0V8eUZa93zydLNynJJcqbq
|
||||
KUYBvOnpzL/0l3hdffmolpUXWFrlFPlOLVQlK4Kc6oQqS2KWBySQHg9klTto1p9c
|
||||
pNZE3sO5+UfleyXW0dN6DcU/xiwoYKJ/+x4JZYtvqH/kP7gve2oznEsLMw6k2QZo
|
||||
O1GihEpoXpOezs46+ER/YGx4ZF2ne2bmYnzoOOZBbGXwsMZTNaa9QJHbc1bz9jjj
|
||||
IFBc1zmrdi0nsbjlvLugEYIbSb/WP0wKwG66zTatslRIQ2unlUJNnWb0E4VLgz9y
|
||||
q57QpvxS7D312dZV0NnAwhyDI+54XAivXTQb0fAGfcgbtKdKpJb1dcAMb9WOBnpr
|
||||
BK7XLsWbJj5v5nB3AuWer7NhUyJB/ogWQtqRUY1bAcI4cB1zFwYq/PL0sbfAHDxx
|
||||
ZEF6Xhi5Ag0EWj054QEQALdPQOlRT1omHljxnN64jFuDXXSIb6zqaBvUwdYoDpV2
|
||||
dfRmzGklsCVA7WHXBmDWbUe9avgO3OO7ANw6/JzzYjP+jwImpJg7cSqTqW8A1U6T
|
||||
YfGXVUV3a/obIEttl7bI9BsUNgmLsBYIwHov+gl/ajKQdALYHCmq3Bj6o7BBeWPp
|
||||
Vpk9dzjcsLVbmNszNGP1Ik5dKE0jZUi6h+YoVuJE9o/+T+jxoqFRpXNsZqWOEKmC
|
||||
HDz6TTs1iTp+CoZ/5g0eKph6XJ+TuNoqF9491IYEFn9oxzsoIBkewTY/fJWmXf++
|
||||
cnpBODrZLF/GoRFc7MW9Kael9vmQ0J7mjM2bFs308lH0rRrfmdlLAU5iKgPv0akx
|
||||
nnnUqvCcoekFMURDtP3z09KZXuOMnt834utd7WLe+LZD6dxs+rPhyDiW80E8Bdlz
|
||||
1Jo+c2g6toIN+uD7/f5gwaZaXhJB0oO7fWSVVo+HJprWBnmf9frgKq1OcS0BNvA+
|
||||
4Aip2hhFqWJAbUQXCyMaeU2WTWIzy0FQ6SEFFy/RM8O5O1HHsDYjtIic9QJ/PqSD
|
||||
0qN7LMlkjR8AdWvAxm95i5GpxDZODldsOneeummvsn3I1jCoULTik7iJVdRuY1V3
|
||||
vfsYAkefGN/n2ga3MvatCJipwoCGsMgUXGTdokXOqKBgMBuBLCkxj2wlol2R9p8R
|
||||
ABEBAAGJAjwEGAEIACYCGwwWIQTVHmTT5j7cPu94ZM7ix11o5iNLBwUCW/fygQUJ
|
||||
A7zhoAAKCRDix11o5iNLB7eTD/4x8I7I7MQV63Z8hDShJixSi49bfXeykzlrZyrA
|
||||
bqNr7JrIKzgX5F1HTU0JF3m+VGkhlpMIlTF/jLq9f1vzmRuiPvux/jItXYbnHFhh
|
||||
lFekwZkXx4nS5iwjpMDt6C1ERftv+Z5yHK91mZsr6eNcfA6VeIdKBQenltZvDVsq
|
||||
HSVEsDhhsKJ473tauwuPXks7cqq8tsSgVzHzRO+CV6HV1b3Muiy5ZA73RC1oIGYT
|
||||
l5zIk1M0h2FIyCfffTBEhZ/dAMErzwcogTA+EAq+OlypTiw2SXZDRx5sQ8T+018k
|
||||
d3zuJZ4PhzJDpzQ627zhy+1M4HPYOHM/nipOkoGl9D8qrFb/DEcoQ6B4FKVRWugJ
|
||||
7ZdtBpnrzh9eVmH9Z1LyKvhSHMSF6iklvIxlCGXas5j71kRg/Yc/aH/St9tV0ZIP
|
||||
1XhwEAY+ul1LCP2YgunCJEJwiG+MZBEZTU5V0gfjdNa/nqNGPOTbLy5oGPV6yWT3
|
||||
b3mx3wudw+aI8MXXPzMBCAn57S7/xuQ4fODx62NOeme/BOnjASbeE3mZ5/3qBbnu
|
||||
YIgVTYNp5frIG3wK8W1r6NY2vYQ0iBIzOCIxnNDjYqsGlpAytX+SM+YY7J9n1dZa
|
||||
UsUfX5Qs+D9VIr/j3jurObPehn9fahCOC2YXicKgSbmQyBLysbFyLT5AMpn5aes0
|
||||
qdwhrw==
|
||||
=B6/F
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var nonVersionChars = regexp.MustCompile(`([^0-9.]+)`) //nolint[gochecknoglobals]
|
||||
|
||||
// sanitizeVersion returns only numbers and periods.
|
||||
func sanitizeVersion(version string) string {
|
||||
return nonVersionChars.ReplaceAllString(version, "")
|
||||
}
|
||||
|
||||
// Result can be false positive, but must not be false negative.
|
||||
// Assuming
|
||||
// * dot separated integers format e.g. "A.B.C.…" where A,B,C,… are integers
|
||||
// * `1.1` == `1.1.0` (i.e. first is not newer)
|
||||
// * `1.1.1` > `1.1` (i.e. first is newer)
|
||||
func isFirstVersionNewer(first, second string) (firstIsNewer bool, err error) {
|
||||
first = sanitizeVersion(first)
|
||||
second = sanitizeVersion(second)
|
||||
|
||||
firstIsNewer, err = false, nil
|
||||
if first == second {
|
||||
return
|
||||
}
|
||||
|
||||
firstIsNewer = true
|
||||
var firstArr, secondArr []int
|
||||
if firstArr, err = versionStrToInts(first); err != nil {
|
||||
return
|
||||
}
|
||||
if secondArr, err = versionStrToInts(second); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
verLength := max(len(firstArr), len(secondArr))
|
||||
firstArr = appendZeros(firstArr, verLength)
|
||||
secondArr = appendZeros(secondArr, verLength)
|
||||
|
||||
for i := 0; i < verLength; i++ {
|
||||
if firstArr[i] == secondArr[i] {
|
||||
continue
|
||||
}
|
||||
return firstArr[i] > secondArr[i], nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func versionStrToInts(version string) (intArr []int, err error) {
|
||||
strArr := strings.Split(version, ".")
|
||||
intArr = make([]int, len(strArr))
|
||||
for index, item := range strArr {
|
||||
if item == "" {
|
||||
intArr[index] = 0
|
||||
continue
|
||||
}
|
||||
intArr[index], err = strconv.Atoi(item)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func appendZeros(ints []int, newsize int) []int {
|
||||
size := len(ints)
|
||||
if size >= newsize {
|
||||
return ints
|
||||
}
|
||||
zeros := make([]int, newsize-size)
|
||||
return append(ints, zeros...)
|
||||
}
|
||||
|
||||
func max(ints ...int) (max int) {
|
||||
max = ints[0]
|
||||
for _, a := range ints {
|
||||
if max < a {
|
||||
max = a
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testDataValues struct {
|
||||
expectErr, expectedNewer bool
|
||||
first, second string
|
||||
}
|
||||
type testDataList []testDataValues
|
||||
|
||||
func (tdl *testDataList) add(err, newer bool, first, second string) { //nolint[unparam]
|
||||
*tdl = append(*tdl, testDataValues{err, newer, first, second})
|
||||
}
|
||||
|
||||
func (tdl *testDataList) addFirstIsNewer(first, second string) {
|
||||
tdl.add(false, true, first, second)
|
||||
tdl.add(false, false, second, first)
|
||||
}
|
||||
|
||||
func TestCompareVersion(t *testing.T) {
|
||||
testData := testDataList{}
|
||||
// same is never newer
|
||||
testData.add(false, false, "1.1.1", "1.1.1")
|
||||
testData.add(false, false, "1.1.0", "1.1")
|
||||
testData.add(false, false, "1.0.0", "1")
|
||||
testData.add(false, false, ".1.1", "0.1.1")
|
||||
testData.add(false, false, "0.1.1", ".1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.10", "1.1.1")
|
||||
testData.addFirstIsNewer("1.10.1", "1.1.1")
|
||||
testData.addFirstIsNewer("10.1.1", "1.1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1", "0.1.1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.0.1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.1.0")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1", "1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.1")
|
||||
testData.addFirstIsNewer("1.1.1.1", "1.1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1 beta", "1.1.0")
|
||||
testData.addFirstIsNewer("1z.1z.1z", "1.1.0")
|
||||
testData.addFirstIsNewer("1a.1b.1c", "1.1.0")
|
||||
|
||||
for _, td := range testData {
|
||||
t.Log(td)
|
||||
isNewer, err := isFirstVersionNewer(td.first, td.second)
|
||||
if td.expectErr {
|
||||
require.True(t, err != nil, "expected error but got nil for %#v", td)
|
||||
require.True(t, true == isNewer, "error expected but first is not newer for %#v", td)
|
||||
continue
|
||||
}
|
||||
|
||||
require.True(t, err == nil, "expected no error but have %v for %#v", err, td)
|
||||
require.True(t, isNewer == td.expectedNewer, "expected %v but have %v for %#v", td.expectedNewer, isNewer, err, td)
|
||||
}
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/dialer"
|
||||
)
|
||||
|
||||
func mkdirAllClear(path string) error {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(path, 0750)
|
||||
}
|
||||
|
||||
func downloadToBytes(path string) (out []byte, err error) {
|
||||
var (
|
||||
client *http.Client
|
||||
response *http.Response
|
||||
)
|
||||
client = dialer.DialTimeoutClient()
|
||||
log.WithField("path", path).Trace("Downloading")
|
||||
|
||||
response, err = client.Get(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err = ioutil.ReadAll(response.Body)
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode < http.StatusOK || http.StatusIMUsed < response.StatusCode {
|
||||
err = errors.New(path + " " + response.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func downloadWithProgress(status *Progress, sourceURL, targetPath string) (err error) {
|
||||
targetFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
log.Warnf("Cannot create update file %s: %v", targetPath, err)
|
||||
return
|
||||
}
|
||||
defer targetFile.Close() //nolint[errcheck]
|
||||
|
||||
var (
|
||||
client *http.Client
|
||||
response *http.Response
|
||||
)
|
||||
client = dialer.DialTimeoutClient()
|
||||
response, err = client.Get(sourceURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close() //nolint[errcheck]
|
||||
|
||||
contentLength, _ := strconv.ParseUint(response.Header.Get("Content-Length"), 10, 64)
|
||||
|
||||
wc := WriteCounter{
|
||||
Status: status,
|
||||
Target: targetFile,
|
||||
Size: contentLength,
|
||||
}
|
||||
|
||||
err = wc.ReadAll(response.Body)
|
||||
return
|
||||
}
|
||||
|
||||
func downloadWithSignature(status *Progress, sourceURL, targetDir string) (localPath string, err error) {
|
||||
localPath = filepath.Join(targetDir, filepath.Base(sourceURL))
|
||||
|
||||
if err = downloadWithProgress(nil, sourceURL+sigExtension, localPath+sigExtension); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = downloadWithProgress(status, sourceURL, localPath); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type WriteCounter struct {
|
||||
Status *Progress
|
||||
Target io.Writer
|
||||
processed, Size, counter uint64
|
||||
}
|
||||
|
||||
func (s *WriteCounter) ReadAll(source io.Reader) (err error) {
|
||||
s.counter = uint64(0)
|
||||
if s.Target == nil {
|
||||
return errors.New("can not read all, target unset")
|
||||
}
|
||||
if source == nil {
|
||||
return errors.New("can not read all, source unset")
|
||||
}
|
||||
_, err = io.Copy(s.Target, io.TeeReader(source, s))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *WriteCounter) Write(p []byte) (int, error) {
|
||||
if s.Status != nil && s.Size != 0 {
|
||||
s.processed += uint64(len(p))
|
||||
fraction := float32(s.processed) / float32(s.Size)
|
||||
if s.counter%uint64(100) == 0 || fraction == 1. {
|
||||
s.Status.UpdateProcessed(fraction)
|
||||
}
|
||||
}
|
||||
s.counter++
|
||||
return len(p), nil
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
const (
|
||||
InfoCurrentVersion = 1 + iota
|
||||
InfoDownloading
|
||||
InfoVerifying
|
||||
InfoUnpacking
|
||||
InfoUpgrading
|
||||
InfoQuitApp
|
||||
InfoRestartApp
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
Processed float32 // fraction of finished procedure [0.0-1.0]
|
||||
Description int // description by code (needs to be translated anyway)
|
||||
Err error // occurred error
|
||||
channel chan<- Progress
|
||||
}
|
||||
|
||||
func (s *Progress) Update() {
|
||||
s.channel <- *s
|
||||
}
|
||||
|
||||
func (s *Progress) UpdateDescription(description int) {
|
||||
s.Description = description
|
||||
s.Processed = 0
|
||||
s.Update()
|
||||
}
|
||||
|
||||
func (s *Progress) UpdateProcessed(processed float32) {
|
||||
s.Processed = processed
|
||||
s.Update()
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
)
|
||||
|
||||
// gpg --export D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07 | xxd -p | tr -d '\n' | xclip
|
||||
const (
|
||||
keyID = "D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07"
|
||||
pubkeyHex = "99020d045a3d39e1011000be7cfacb714058f9851ce5888cad8250ea882b2563060b4d21f5f02fdcfb2b1e4073d33edc4050f235d35ab689050ed6435f5d79334bf30f093936472398eec259b7fa9c265cc18a8e4ab0681fcc0d4fdb934bfea9477007935af70bfdfa406de25b1e96838c9c4645d6613a13dffca1e70684e416cb8bff5348101d7c9cd3cb1b78229646dce4cc9cec2ecfa78d456547900e3c089c9d592e8cf33fa322c07016ae273880a1bcb8c8178d8fb804f555a8826129b2e2c535631c56a1fe73f476345e0851a5deda508833008b1751b6845e1ff788264350c3792f0932027fabe63dd230dce4da1b45f15eea584f25758355ae9784c32a2bd31d70333a5b6ff0b863cc177bcacfd35774029887551113cec424d9eb1f5ee4ab042b69c8b73a113d6596e88bdac55451e9403ee7944253b26177cbd97f79f22d138010a2e9044f5f16cf8c23ec7755332cf09250d50efbfedfb256426b1dba775af591b1f324b00dd497abeabc681036848954825139c7832902ab9ace0b73d270611d39a222e07e5ce98159acb99dcf8d3d62624458b2883d5feee43a48f981a601f01ebeb8e430181004e9990fc1a9d7d2e746d9aa8d5876ad576bf327399c08e834e6706a73300f9bc258f51510b597b9b34506ff21a993311d9a961ab07cc7c86476088d9aecaab31cc198e1091d62e5bdb161bc784879d4fca5a53fb292ffa89996a77101e10011010001b44c50726f746f6e20546563686e6f6c6f67696573204147202850726f746f6e4d61696c2042726964676520646576656c6f7065727329203c6272696467654070726f746f6e6d61696c2e63683e89025404130108003e021b03050b09080702061508090a0b020416020301021e01021780162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7efbc050903bcdedb000a0910e2c75d68e6234b07e19e0fff58d859aabd12b6e37829b61d406f8563e68b8a32e18952378b88fac148acfbb51614d31419e3ec054ac5ef33abce8e15a75e85491861a6f7886c3315260e321a5cf037e8d9afd141bc5f3391b3aa8da8e44c5deb3b2a92402a5956079a991b7a7388064133b07f0c41e0ae66560e2bbfae484882a32d4b42676ee0a43e9e26cdb92e1c9942723ae491e91c39156abee84369db5afd2d9f6c2bad428113d851339267d7119a1d892b6313de1cd0c7aeac495036cf7c985c6b9e0cf7d6ecf50ba6dca913d56594e7fd2dac0d15f1e5196bddf3c9d2cdca724972a6ea294601bce9e9ccbff497785d7df9a8969517585ae514f94e2d54252b829cea842a4b62960724901e0f64953b68d69f5ca4d644dec3b9f947e57b25d6d1d37a0dc53fc62c2860a27ffb1e09658b6fa87fe43fb82f7b6a339c4b0b330ea4d906683b51a2844a685e939ecece3af8447f606c78645da77b66e6627ce838e6416c65f0b0c65335a6bd4091db7356f3f638e320505cd739ab762d27b1b8e5bcbba011821b49bfd63f4c0ac06ebacd36adb25448436ba795424d9d66f413854b833f72ab9ed0a6fc52ec3df5d9d655d0d9c0c21c8323ee785c08af5d341bd1f0067dc81bb4a74aa496f575c00c6fd58e067a6b04aed72ec59b263e6fe6707702e59eafb361532241fe881642da91518d5b01c238701d7317062afcf2f4b1b7c01c3c7164417a5e18b9020d045a3d39e1011000b74f40e9514f5a261e58f19cdeb88c5b835d74886facea681bd4c1d6280e957675f466cc6925b02540ed61d70660d66d47bd6af80edce3bb00dc3afc9cf36233fe8f0226a4983b712a93a96f00d54e9361f1975545776bfa1b204b6d97b6c8f41b1436098bb01608c07a2ffa097f6a32907402d81c29aadc18faa3b0417963e956993d7738dcb0b55b98db333463f5224e5d284d236548ba87e62856e244f68ffe4fe8f1a2a151a5736c66a58e10a9821c3cfa4d3b35893a7e0a867fe60d1e2a987a5c9f93b8da2a17de3dd48604167f68c73b2820191ec1363f7c95a65dffbe727a41383ad92c5fc6a1115cecc5bd29a7a5f6f990d09ee68ccd9b16cdf4f251f4ad1adf99d94b014e622a03efd1a9319e79d4aaf09ca1e905314443b4fdf3d3d2995ee38c9edf37e2eb5ded62def8b643e9dc6cfab3e1c83896f3413c05d973d49a3e73683ab6820dfae0fbfdfe60c1a65a5e1241d283bb7d6495568f87269ad606799ff5fae02aad4e712d0136f03ee008a9da1845a962406d44170b231a794d964d6233cb4150e92105172fd133c3b93b51c7b03623b4889cf5027f3ea483d2a37b2cc9648d1f00756bc0c66f798b91a9c4364e0e576c3a779eba69afb27dc8d630a850b4e293b88955d46e635577bdfb1802479f18dfe7da06b732f6ad0898a9c28086b0c8145c64dda245cea8a060301b812c29318f6c25a25d91f69f11001101000189023c041801080026021b0c162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7f281050903bce1a0000a0910e2c75d68e6234b07b7930ffe31f08ec8ecc415eb767c8434a1262c528b8f5b7d77b293396b672ac06ea36bec9ac82b3817e45d474d4d091779be54692196930895317f8cbabd7f5bf3991ba23efbb1fe322d5d86e71c58619457a4c19917c789d2e62c23a4c0ede82d4445fb6ff99e721caf75999b2be9e35c7c0e9578874a0507a796d66f0d5b2a1d2544b03861b0a278ef7b5abb0b8f5e4b3b72aabcb6c4a05731f344ef8257a1d5d5bdccba2cb9640ef7442d68206613979cc8935334876148c827df7d3044859fdd00c12bcf072881303e100abe3a5ca94e2c36497643471e6c43c4fed35f24777cee259e0f873243a7343adbbce1cbed4ce073d838733f9e2a4e9281a5f43f2aac56ff0c472843a07814a5515ae809ed976d0699ebce1f5e5661fd6752f22af8521cc485ea2925bc8c650865dab398fbd64460fd873f687fd2b7db55d1920fd5787010063eba5d4b08fd9882e9c2244270886f8c6411194d4e55d207e374d6bf9ea3463ce4db2f2e6818f57ac964f76f79b1df0b9dc3e688f0c5d73f33010809f9ed2effc6e4387ce0f1eb634e7a67bf04e9e30126de137999e7fdea05b9ee6088154d8369e5fac81b7c0af16d6be8d636bd84348812333822319cd0e362ab06969032b57f9233e618ec9f67d5d65a52c51f5f942cf83f5522bfe3de3bab39b3de867f5f6a108e0b661789c2a049b990c812f2b1b1722d3e403299f969eb34a9dc21af"
|
||||
)
|
||||
|
||||
var (
|
||||
pubkeyRing = openpgp.EntityList{} //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func singAndVerify(pathToFile string) (err error) {
|
||||
err = signFile(pathToFile)
|
||||
if err != nil {
|
||||
err = verifyFile(pathToFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func signFile(pathToFile string) (err error) {
|
||||
if runtime.GOOS != "linux" { //nolint[goconst]
|
||||
return errors.New("tar not implemented only for linux")
|
||||
}
|
||||
// assuming gpg detach-sign creates file with suffix .sig by default.
|
||||
// Lstat does not follow the link i.e. only link is deleted (not link target).
|
||||
if _, err := os.Lstat(pathToFile + sigExtension); !os.IsNotExist(err) {
|
||||
_ = os.Remove(pathToFile + sigExtension)
|
||||
}
|
||||
cmd := exec.Command("gpg", "--local-user", keyID, "--detach-sign", pathToFile) //nolint[gosec]
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func verifyFile(pathToFile string) error {
|
||||
fileReader, err := os.Open(pathToFile) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close() //nolint[errcheck]
|
||||
|
||||
signatureReader, err := os.Open(pathToFile + sigExtension) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer signatureReader.Close() //nolint[errcheck]
|
||||
|
||||
return verifyBytes(fileReader, signatureReader)
|
||||
}
|
||||
|
||||
func verifyBytes(fileReader, signatureReader io.Reader) (err error) {
|
||||
if _, err = getPubKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = openpgp.CheckDetachedSignature(pubkeyRing, fileReader, signatureReader, nil)
|
||||
/*
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signer == nil || signer.PrimaryKey.KeyId != keyID {
|
||||
return errors.New("Signer with wrong key ID")
|
||||
}
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
// from opengpg/read_test.go
|
||||
func getPubKey() (el openpgp.EntityList, err error) {
|
||||
if pubkeyRing != nil && len(pubkeyRing) != 0 {
|
||||
return pubkeyRing, nil
|
||||
}
|
||||
data, err := hex.DecodeString(pubkeyHex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubkeyRing, err = openpgp.ReadKeyRing(bytes.NewBuffer(data))
|
||||
return pubkeyRing, err
|
||||
}
|
||||
@ -1,239 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func syncFolders(localPath, updatePath string) (err error) {
|
||||
backupDir := filepath.Join(filepath.Dir(updatePath), "backup")
|
||||
if err = createBackup(localPath, backupDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = removeMissing(localPath, updatePath); err != nil {
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
}
|
||||
|
||||
if err = copyRecursively(updatePath, localPath); err != nil {
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
|
||||
log.Debug("remove missing")
|
||||
// Create list of files.
|
||||
existingRelPaths := map[string]bool{}
|
||||
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis)
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
log.Debug("path to keep ", relPath)
|
||||
existingRelPaths[relPath] = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
delList := []string{}
|
||||
err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis)
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
log.Debug("check path ", relPath)
|
||||
if !existingRelPaths[relPath] {
|
||||
log.Debug("path not in list, removing ", removeThis)
|
||||
delList = append(delList, removeThis)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, removeThis := range delList {
|
||||
if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) {
|
||||
log.Error("remove error ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFromBackup(backupDir, localPath string) {
|
||||
log.Error("recovering from ", backupDir, " to ", localPath)
|
||||
_ = copyRecursively(backupDir, localPath)
|
||||
}
|
||||
|
||||
func createBackup(srcFile, dstDir string) (err error) {
|
||||
log.Debug("backup ", srcFile, " in ", dstDir)
|
||||
if err = mkdirAllClear(dstDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return copyRecursively(srcFile, dstDir)
|
||||
}
|
||||
|
||||
// checksum assumes the file is a regular file and that it exists.
|
||||
func checksum(path string) (hash string) {
|
||||
file, err := os.Open(path) //nolint[gosec]
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close() //nolint[errcheck]
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return string(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// srcDir including app folder.
|
||||
// dstDir including app folder.
|
||||
func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
|
||||
return filepath.Walk(srcDir, func(srcPath string, srcInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
srcIsDir := srcInfo.IsDir()
|
||||
|
||||
// Non regular source (e.g. named pipes, sockets, devices...).
|
||||
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
|
||||
log.Error("File ", srcPath, " with mode ", srcInfo.Mode())
|
||||
return errors.New("irregular source file. Copy not implemented")
|
||||
}
|
||||
|
||||
// Destination path.
|
||||
srcRelPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstPath := filepath.Join(dstDir, srcRelPath)
|
||||
log.Debug("src: ", srcPath, " dst: ", dstPath)
|
||||
|
||||
// Destination exists.
|
||||
dstInfo, err := os.Lstat(dstPath)
|
||||
if err == nil {
|
||||
dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
dstIsDir := dstInfo.IsDir()
|
||||
|
||||
// Non regular destination (e.g. named pipes, sockets, devices...).
|
||||
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
|
||||
log.Error("File ", dstPath, " with mode ", dstInfo.Mode())
|
||||
return errors.New("irregular target file. Copy not implemented")
|
||||
}
|
||||
|
||||
if dstIsLink {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !dstIsLink && dstIsDir && !srcIsDir {
|
||||
if err = os.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Do not return if !dstIsLink && dstIsDir && srcIsDir: the permissions might change.
|
||||
|
||||
if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create symbolic link and return.
|
||||
if srcIsLink {
|
||||
log.Debug("It is a symlink")
|
||||
linkPath, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("link to ", linkPath)
|
||||
return os.Symlink(linkPath, dstPath)
|
||||
}
|
||||
|
||||
// Create dir and return.
|
||||
if srcIsDir {
|
||||
log.Debug("It is a dir")
|
||||
return os.MkdirAll(dstPath, srcInfo.Mode())
|
||||
}
|
||||
|
||||
// Regular files only.
|
||||
// If files are same return.
|
||||
if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) {
|
||||
log.Debug("Same files, skip copy")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create/overwrite regular file.
|
||||
srcReader, err := os.Open(srcPath) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcReader.Close() //nolint[errcheck]
|
||||
return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode())
|
||||
})
|
||||
}
|
||||
|
||||
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
log.Debug("Tmp and rename ", dstPath)
|
||||
tmpPath := dstPath + ".tmp"
|
||||
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, dstPath)
|
||||
}
|
||||
|
||||
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
log.Debug("Copy and truncate ", dstPath)
|
||||
dstWriter, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstWriter.Close() //nolint[errcheck]
|
||||
_, err = io.Copy(dstWriter, srcReader)
|
||||
return err
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
FileType = "File"
|
||||
SymlinkType = "Symlink"
|
||||
DirType = "Dir"
|
||||
EmptyType = "Empty"
|
||||
NewType = "New"
|
||||
)
|
||||
|
||||
func TestSyncFolder(t *testing.T) {
|
||||
for _, srcType := range []string{EmptyType, FileType, SymlinkType, DirType} {
|
||||
for _, dstType := range []string{EmptyType, FileType, SymlinkType, DirType} {
|
||||
require.NoError(t, checkCopyWorks(srcType, dstType))
|
||||
log.Warn("OK: from ", srcType, " to ", dstType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkCopyWorks(srcType, dstType string) error {
|
||||
dirName := "from_" + srcType + "_to_" + dstType
|
||||
AppCacheDir := "/tmp"
|
||||
srcDir := filepath.Join(AppCacheDir, "sync_src", dirName)
|
||||
destDir := filepath.Join(AppCacheDir, "sync_dst", dirName)
|
||||
|
||||
// clear before
|
||||
log.Info("remove all ", srcDir)
|
||||
err := os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("remove all ", destDir)
|
||||
err = os.RemoveAll(destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create
|
||||
err = createTestFolder(srcDir, srcType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createTestFolder(destDir, dstType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy
|
||||
log.Info("Sync from ", srcDir, " to ", destDir)
|
||||
err = syncFolders(destDir, srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check
|
||||
log.Info("check ", srcDir, " and ", destDir)
|
||||
err = checkThatFilesAreSame(srcDir, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear after
|
||||
log.Info("remove all ", srcDir)
|
||||
err = os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("remove all ", destDir)
|
||||
err = os.RemoveAll(destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func checkThatFilesAreSame(src, dst string) error {
|
||||
cmd := exec.Command("diff", "-qr", src, dst) //nolint[gosec]
|
||||
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
|
||||
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func createTestFolder(dirPath, dirType string) error {
|
||||
log.Info("creating folder ", dirPath, " type ", dirType)
|
||||
if dirType == NewType {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := mkdirAllClear(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dirType == EmptyType {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := filepath.Join(dirPath, "testpath")
|
||||
switch dirType {
|
||||
case FileType:
|
||||
err = ioutil.WriteFile(path, []byte("This is a test"), 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case SymlinkType:
|
||||
err = os.Symlink("../../", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case DirType:
|
||||
err = os.MkdirAll(path, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(path, "another_file"), []byte("This is a test"), 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createTar(tarPath, sourcePath string) error { //nolint[unused]
|
||||
if runtime.GOOS != "linux" {
|
||||
return errors.New("tar not implemented only for linux")
|
||||
}
|
||||
// Check whether it exists and is a directory.
|
||||
if _, err := os.Lstat(sourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(tarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("tar", "-zvcf", absPath, filepath.Base(sourcePath)) //nolint[gosec]
|
||||
cmd.Dir = filepath.Dir(sourcePath)
|
||||
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
|
||||
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func untarToDir(tarPath, targetDir string, status *Progress) error { //nolint[funlen]
|
||||
// Check whether it exists and is a directory.
|
||||
if ls, err := os.Lstat(targetDir); err == nil {
|
||||
if !ls.IsDir() {
|
||||
return errors.New("not a dir")
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
tgzReader, err := os.Open(tarPath) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tgzReader.Close() //nolint[errcheck]
|
||||
|
||||
size := uint64(0)
|
||||
if info, err := tgzReader.Stat(); err == nil {
|
||||
size = uint64(info.Size())
|
||||
}
|
||||
|
||||
wc := &WriteCounter{
|
||||
Status: status,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
tarReader, err := gzip.NewReader(io.TeeReader(tgzReader, wc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileReader := tar.NewReader(tarReader)
|
||||
for {
|
||||
header, err := fileReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if header == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(targetDir, header.Name)
|
||||
info := header.FileInfo()
|
||||
|
||||
// Create symlink.
|
||||
if header.Typeflag == tar.TypeSymlink {
|
||||
if header.Linkname == "" {
|
||||
return errors.New("missing linkname")
|
||||
}
|
||||
if err := os.Symlink(header.Linkname, targetFile); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle case that it is a directory.
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(targetFile, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle case that it is a regular file.
|
||||
if err := copyToFileTruncate(fileReader, targetFile, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
{"Version":"1.1.6","ReleaseDate":"10 Jul 19 11:02 +0200","ReleaseNotes":"• Necessary updates reflecting API changes\n• Report wrongly formated messages\n","ReleaseFixedBugs":"• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n","FixedBugs":["• Fixed verification for contacts signed by older or missing key","• Outlook always shows attachment icon",""],"URL":"https://protonmail.com/download/Bridge-Installer.sh","LandingPage":"https://protonmail.com/bridge/download","UpdateFile":"https://protonmail.com/download/bridge_upgrade_linux.tgz","InstallerFile":"https://protonmail.com/download/Bridge-Installer.sh","DebFile":"https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb","RpmFile":"https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm","PkgFile":"https://protonmail.com/download/PKGBUILD"}
|
||||
BIN
pkg/updates/testdata/current_version_linux.json.sig
vendored
BIN
pkg/updates/testdata/current_version_linux.json.sig
vendored
Binary file not shown.
@ -1,310 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
sigExtension = ".sig"
|
||||
)
|
||||
|
||||
var (
|
||||
Host = "https://protonmail.com" //nolint[gochecknoglobals]
|
||||
DownloadPath = "download" //nolint[gochecknoglobals]
|
||||
|
||||
// BuildType specifies type of build (e.g. QA or beta).
|
||||
BuildType = "" //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "bridgeUtils/updates") //nolint[gochecknoglobals]
|
||||
|
||||
installFileSuffix = map[string]string{ //nolint[gochecknoglobals]
|
||||
"darwin": ".dmg",
|
||||
"windows": ".exe",
|
||||
"linux": ".sh",
|
||||
}
|
||||
|
||||
ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals]
|
||||
ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
type Updates struct {
|
||||
appName string
|
||||
version string
|
||||
revision string
|
||||
buildTime string
|
||||
releaseNotes string
|
||||
releaseFixedBugs string
|
||||
updateTempDir string
|
||||
landingPagePath string // Based on Host/; default landing page for download.
|
||||
installerFileBaseName string // File for initial install or manual reinstall. per goos [exe, dmg, sh].
|
||||
versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file).
|
||||
updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file).
|
||||
macAppBundleName string // For update procedure.
|
||||
cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops.
|
||||
}
|
||||
|
||||
// New inits Updates struct.
|
||||
// `appName` should be in camelCase format for file names. For installer files is converted to CamelCase.
|
||||
func New(appName, version, revision, buildTime, releaseNotes, releaseFixedBugs, updateTempDir string) *Updates {
|
||||
return &Updates{
|
||||
appName: appName,
|
||||
version: version,
|
||||
revision: revision,
|
||||
buildTime: buildTime,
|
||||
releaseNotes: releaseNotes,
|
||||
releaseFixedBugs: releaseFixedBugs,
|
||||
updateTempDir: updateTempDir,
|
||||
landingPagePath: appName + "/download",
|
||||
installerFileBaseName: strings.Title(appName) + "-Installer",
|
||||
versionFileBaseName: "current_version",
|
||||
updateFileBaseName: appName + "_upgrade",
|
||||
macAppBundleName: "ProtonMail " + strings.Title(appName) + ".app", // For update procedure.
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updates) CreateJSONAndSign(deployDir, goos string) error {
|
||||
versionInfo := u.getLocalVersion(goos)
|
||||
versionInfo.Version = sanitizeVersion(versionInfo.Version)
|
||||
|
||||
versionFileName := filepath.Base(u.versionFileURL(goos))
|
||||
versionFilePath := filepath.Join(deployDir, versionFileName)
|
||||
|
||||
txt, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(versionFilePath, txt, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := singAndVerify(versionFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFileName := filepath.Base(versionInfo.UpdateFile)
|
||||
updateFilePath := filepath.Join(deployDir, updateFileName)
|
||||
if err := singAndVerify(updateFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updates) CheckIsBridgeUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) {
|
||||
localVersion := u.GetLocalVersion()
|
||||
latestVersion, err = u.getLatestVersion()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
localIsOld, err := isFirstVersionNewer(latestVersion.Version, localVersion.Version)
|
||||
return !localIsOld, latestVersion, err
|
||||
}
|
||||
|
||||
func (u *Updates) GetDownloadLink() string {
|
||||
latestVersion, err := u.getLatestVersion()
|
||||
if err != nil || latestVersion.InstallerFile == "" {
|
||||
localVersion := u.GetLocalVersion()
|
||||
return localVersion.GetDownloadLink()
|
||||
}
|
||||
return latestVersion.GetDownloadLink()
|
||||
}
|
||||
|
||||
func (u *Updates) GetLocalVersion() VersionInfo {
|
||||
return u.getLocalVersion(runtime.GOOS)
|
||||
}
|
||||
|
||||
func (u *Updates) getLocalVersion(goos string) VersionInfo {
|
||||
version := u.version
|
||||
if BuildType != "" {
|
||||
version += " " + BuildType
|
||||
}
|
||||
|
||||
versionInfo := VersionInfo{
|
||||
Version: version,
|
||||
Revision: u.revision,
|
||||
ReleaseDate: u.buildTime,
|
||||
ReleaseNotes: u.releaseNotes,
|
||||
ReleaseFixedBugs: u.releaseFixedBugs,
|
||||
FixedBugs: strings.Split(u.releaseFixedBugs, "\n"),
|
||||
URL: u.installerFileURL(goos),
|
||||
|
||||
LandingPage: u.landingPageURL(),
|
||||
UpdateFile: u.updateFileURL(goos),
|
||||
InstallerFile: u.installerFileURL(goos),
|
||||
}
|
||||
|
||||
if goos == "linux" {
|
||||
pkgName := "protonmail-" + u.appName
|
||||
pkgRel := "1"
|
||||
pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/")
|
||||
|
||||
versionInfo.DebFile = pkgBase + "_" + u.version + "-" + pkgRel + "_amd64.deb"
|
||||
versionInfo.RpmFile = pkgBase + "-" + u.version + "-" + pkgRel + ".x86_64.rpm"
|
||||
versionInfo.PkgFile = strings.Join([]string{Host, DownloadPath, "PKGBUILD"}, "/")
|
||||
}
|
||||
|
||||
return versionInfo
|
||||
}
|
||||
|
||||
func (u *Updates) getLatestVersion() (latestVersion VersionInfo, err error) {
|
||||
version, err := downloadToBytes(u.versionFileURL(runtime.GOOS))
|
||||
if err != nil {
|
||||
if u.cachedNewerVersion != nil {
|
||||
return *u.cachedNewerVersion, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := downloadToBytes(u.signatureFileURL(runtime.GOOS))
|
||||
if err != nil {
|
||||
if u.cachedNewerVersion != nil {
|
||||
return *u.cachedNewerVersion, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = verifyBytes(bytes.NewReader(version), bytes.NewReader(signature)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(bytes.NewReader(version)).Decode(&latestVersion); err != nil {
|
||||
return
|
||||
}
|
||||
if localIsOld, _ := isFirstVersionNewer(latestVersion.Version, u.version); localIsOld {
|
||||
u.cachedNewerVersion = &latestVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (u *Updates) landingPageURL() string {
|
||||
return strings.Join([]string{Host, u.landingPagePath}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) signatureFileURL(goos string) string {
|
||||
return u.versionFileURL(goos) + sigExtension
|
||||
}
|
||||
|
||||
func (u *Updates) versionFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.versionFileBaseName + "_" + goos + ".json"}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) installerFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.installerFileBaseName + installFileSuffix[goos]}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) updateFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.updateFileBaseName + "_" + goos + ".tgz"}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen]
|
||||
status := &Progress{channel: currentStatus}
|
||||
defer status.Update()
|
||||
|
||||
// Get latest version.
|
||||
var verInfo VersionInfo
|
||||
status.UpdateDescription(InfoCurrentVersion)
|
||||
if verInfo, status.Err = u.getLatestVersion(); status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if verInfo.UpdateFile == "" {
|
||||
log.Warn("Empty update URL. Update manually.")
|
||||
status.Err = ErrDownloadFailed
|
||||
return
|
||||
}
|
||||
|
||||
// Download.
|
||||
status.UpdateDescription(InfoDownloading)
|
||||
if status.Err = mkdirAllClear(u.updateTempDir); status.Err != nil {
|
||||
return
|
||||
}
|
||||
var updateTar string
|
||||
updateTar, status.Err = downloadWithSignature(
|
||||
status,
|
||||
verInfo.UpdateFile,
|
||||
u.updateTempDir,
|
||||
)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check signature.
|
||||
status.UpdateDescription(InfoVerifying)
|
||||
status.Err = verifyFile(updateTar)
|
||||
if status.Err != nil {
|
||||
log.Warnf("Cannot verify update file %s: %v", updateTar, status.Err)
|
||||
status.Err = ErrUpdateVerifyFailed
|
||||
return
|
||||
}
|
||||
|
||||
// Untar.
|
||||
status.UpdateDescription(InfoUnpacking)
|
||||
status.Err = untarToDir(updateTar, u.updateTempDir, status)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Run upgrade (OS specific).
|
||||
status.UpdateDescription(InfoUpgrading)
|
||||
switch runtime.GOOS {
|
||||
case "windows": //nolint[goconst]
|
||||
cmd := exec.Command("./" + u.installerFileBaseName) // nolint[gosec]
|
||||
cmd.Dir = u.updateTempDir
|
||||
status.Err = cmd.Start()
|
||||
case "darwin":
|
||||
// current path is better then appDir = filepath.Join("/Applications")
|
||||
var exePath string
|
||||
exePath, status.Err = osext.Executable()
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
localPath := filepath.Dir(exePath) // Macos
|
||||
localPath = filepath.Dir(localPath) // Contents
|
||||
localPath = filepath.Dir(localPath) // .app
|
||||
|
||||
updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName)
|
||||
log.Warn("localPath ", localPath)
|
||||
log.Warn("updatePath ", updatePath)
|
||||
status.Err = syncFolders(localPath, updatePath)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
status.UpdateDescription(InfoRestartApp)
|
||||
return
|
||||
default:
|
||||
status.Err = errors.New("upgrade for " + runtime.GOOS + " not implemented")
|
||||
}
|
||||
|
||||
status.UpdateDescription(InfoQuitApp)
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build build_beta
|
||||
|
||||
package updates
|
||||
|
||||
func init() {
|
||||
DownloadPath = "download/beta"
|
||||
BuildType = "beta"
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build build_qa
|
||||
|
||||
package updates
|
||||
|
||||
func init() {
|
||||
Host = "https://bridgeteam.protontech.ch"
|
||||
DownloadPath = "download/qa"
|
||||
BuildType = "QA"
|
||||
}
|
||||
@ -1,178 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testServerPort = "8999"
|
||||
|
||||
var testUpdateDir string //nolint[gochecknoglobals]
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
setup()
|
||||
code := m.Run()
|
||||
shutdown()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setup() {
|
||||
var err error
|
||||
testUpdateDir, err = ioutil.TempDir("", "upgrade")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Host = "http://localhost:" + testServerPort
|
||||
go startServer()
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
_ = os.RemoveAll(testUpdateDir)
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
http.HandleFunc("/download/current_version_linux.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_linux.json.sig", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_darwin.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_darwin.json.sig", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
|
||||
})
|
||||
panic(http.ListenAndServe(":"+testServerPort, nil))
|
||||
}
|
||||
|
||||
func TestCheckBridgeIsUpToDate(t *testing.T) {
|
||||
updates := newTestUpdates("1.1.6")
|
||||
isUpToDate, _, err := updates.CheckIsBridgeUpToDate()
|
||||
require.NoError(t, err)
|
||||
require.True(t, isUpToDate, "Bridge should be up to date")
|
||||
}
|
||||
|
||||
func TestCheckBridgeIsNotUpToDate(t *testing.T) {
|
||||
updates := newTestUpdates("1.1.5")
|
||||
isUpToDate, _, err := updates.CheckIsBridgeUpToDate()
|
||||
require.NoError(t, err)
|
||||
require.True(t, !isUpToDate, "Bridge should not be up to date")
|
||||
}
|
||||
|
||||
func TestGetLocalVersion(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test because local version for windows is currently not supported by tests.")
|
||||
}
|
||||
updates := newTestUpdates("1")
|
||||
expectedVersion := VersionInfo{
|
||||
Version: "1",
|
||||
Revision: "rev123",
|
||||
ReleaseDate: "42",
|
||||
ReleaseNotes: "• new feature",
|
||||
ReleaseFixedBugs: "• fixed foo",
|
||||
FixedBugs: []string{"• fixed foo"},
|
||||
URL: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
|
||||
|
||||
LandingPage: Host + "/bridge/download",
|
||||
UpdateFile: Host + "/" + DownloadPath + "/bridge_upgrade_linux.tgz",
|
||||
InstallerFile: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
|
||||
|
||||
DebFile: Host + "/" + DownloadPath + "/protonmail-bridge_1-1_amd64.deb",
|
||||
RpmFile: Host + "/" + DownloadPath + "/protonmail-bridge-1-1.x86_64.rpm",
|
||||
PkgFile: Host + "/" + DownloadPath + "/PKGBUILD",
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
expectedVersion.URL = Host + "/" + DownloadPath + "/Bridge-Installer.dmg"
|
||||
expectedVersion.UpdateFile = Host + "/" + DownloadPath + "/bridge_upgrade_darwin.tgz"
|
||||
expectedVersion.InstallerFile = expectedVersion.URL
|
||||
expectedVersion.DebFile = ""
|
||||
expectedVersion.RpmFile = ""
|
||||
expectedVersion.PkgFile = ""
|
||||
}
|
||||
version := updates.GetLocalVersion()
|
||||
require.Equal(t, expectedVersion, version)
|
||||
}
|
||||
|
||||
func TestGetLatestVersion(t *testing.T) {
|
||||
updates := newTestUpdates("1")
|
||||
expectedVersion := VersionInfo{
|
||||
Version: "1.1.6",
|
||||
Revision: "",
|
||||
ReleaseDate: "10 Jul 19 11:02 +0200",
|
||||
ReleaseNotes: "• Necessary updates reflecting API changes\n• Report wrongly formated messages\n",
|
||||
ReleaseFixedBugs: "• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n",
|
||||
FixedBugs: []string{
|
||||
"• Fixed verification for contacts signed by older or missing key",
|
||||
"• Outlook always shows attachment icon",
|
||||
"",
|
||||
},
|
||||
URL: "https://protonmail.com/download/Bridge-Installer.sh",
|
||||
|
||||
LandingPage: "https://protonmail.com/bridge/download",
|
||||
UpdateFile: "https://protonmail.com/download/bridge_upgrade_linux.tgz",
|
||||
InstallerFile: "https://protonmail.com/download/Bridge-Installer.sh",
|
||||
|
||||
DebFile: "https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb",
|
||||
RpmFile: "https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm",
|
||||
PkgFile: "https://protonmail.com/download/PKGBUILD",
|
||||
}
|
||||
version, err := updates.getLatestVersion()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedVersion, version)
|
||||
}
|
||||
|
||||
func TestStartUpgrade(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping test because only upgrading on windows is currently supported by tests.")
|
||||
}
|
||||
|
||||
updates := newTestUpdates("1")
|
||||
progress := make(chan Progress, 1)
|
||||
done := make(chan error)
|
||||
|
||||
go func() {
|
||||
for current := range progress {
|
||||
log.Infof("progress descr: %d processed %f err %v", current.Description, current.Processed, current.Err)
|
||||
if current.Err != nil {
|
||||
done <- current.Err
|
||||
break
|
||||
}
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
updates.StartUpgrade(progress)
|
||||
close(progress)
|
||||
require.NoError(t, <-done)
|
||||
}
|
||||
|
||||
func newTestUpdates(version string) *Updates {
|
||||
return New("bridge", version, "rev123", "42", "• new feature", "• fixed foo", testUpdateDir)
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
Version string
|
||||
Revision string
|
||||
ReleaseDate string // Timestamp generated automatically
|
||||
ReleaseNotes string // List of features, new line separated with leading dot e.g. `• example\n`
|
||||
ReleaseFixedBugs string // List of fixed bugs, same usage as release notes
|
||||
FixedBugs []string // Deprecated list of fixed bugs keeping for backward compatibility (mandatory for working versions up to 1.1.5)
|
||||
URL string // Open browser and download (obsolete replaced by InstallerFile)
|
||||
|
||||
LandingPage string // landing page for manual download
|
||||
UpdateFile string // automatic update file
|
||||
InstallerFile string `json:",omitempty"` // manual update file
|
||||
DebFile string `json:",omitempty"` // debian package file
|
||||
RpmFile string `json:",omitempty"` // red hat package file
|
||||
PkgFile string `json:",omitempty"` // arch PKGBUILD file
|
||||
}
|
||||
|
||||
func (info *VersionInfo) GetDownloadLink() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return strings.Join([]string{info.DebFile, info.RpmFile, info.PkgFile}, "\n")
|
||||
default:
|
||||
return info.InstallerFile
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user