Merge branch 'release/congo' into devel

This commit is contained in:
Jakub
2020-09-08 09:37:05 +02:00
286 changed files with 22512 additions and 1057 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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 = ""

View File

@ -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
View 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
}

View File

@ -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.

View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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{

View File

@ -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) {

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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
}

View File

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

View File

@ -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

View File

@ -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-----

View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"}

View File

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

View File

@ -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"
}

View File

@ -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"
}

View File

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

View File

@ -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
}
}