Initial commit for http rest bridge

This commit is contained in:
Xiaonan Shen
2022-05-04 18:15:28 +08:00
parent 22b04d941d
commit 46b3cf35a4
21 changed files with 518 additions and 249 deletions

View File

@@ -0,0 +1,147 @@
// Package cli provides HTTP interface of the bridge
package cli
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/julienschmidt/httprouter"
)
func (f *frontendCLI) loginAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "ParseForm() err: %v", err)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
twoFactor := r.FormValue("two-factor")
mailboxPassword := r.FormValue("mailbox-password")
client, auth, err := f.bridge.Login(username, []byte(password))
if err != nil {
f.processAPIError(err)
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "Server error:", err.Error())
return
}
if auth.HasTwoFactor() {
if twoFactor == "" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "2FA enabled for the account but a 2FA code was not provided.")
return
}
err = client.Auth2FA(context.Background(), twoFactor)
if err != nil {
f.processAPIError(err)
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "Server error:", err.Error())
return
}
}
if auth.HasMailboxPassword() {
if mailboxPassword == "" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "Two password mode enabled but a mailbox password was not provided.")
return
}
} else {
mailboxPassword = password
}
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
f.processAPIError(err)
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "Server error:", err.Error())
return
}
fmt.Fprintf(w, "Account %s was added successfully.\n", user.Username())
f.printAccountInfo(w, user)
}
func (f *frontendCLI) deleteAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
account := params.ByName("account")
user := f.getUserByIndexOrName(account)
if user == nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Account %s does not exist.\n", account)
return
}
account = user.Username()
if err := f.bridge.DeleteUser(user.ID(), true); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Cannot delete account: ", err)
return
}
fmt.Fprintf(w, "Account %s was deleted successfully.\n", account)
}
func (f *frontendCLI) listAccounts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
users := f.bridge.GetUsers()
if len(users) == 0 {
fmt.Fprintln(w, "No account found.")
return
}
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
fmt.Fprintf(w, strings.ReplaceAll(spacing, "d", "s"), "#", "account", "status", "address mode")
for idx, user := range users {
connected := "disconnected"
if user.IsConnected() {
connected = "connected"
}
mode := "split"
if user.IsCombinedAddressMode() {
mode = "combined"
}
fmt.Fprintf(w, spacing, idx, user.Username(), connected, mode)
}
}
func (f *frontendCLI) showAccountInfo(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
account := params.ByName("account")
user := f.getUserByIndexOrName(account)
if user == nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Account %s does not exist.\n", account)
return
}
if !user.IsConnected() {
fmt.Fprintf(w, "Please login to %s to get email client configuration.\n", user.Username())
return
}
f.printAccountInfo(w, user)
}
func (f *frontendCLI) printAccountInfo(w io.Writer, user types.User) {
if user.IsCombinedAddressMode() {
f.printAccountAddressInfo(w, user, user.GetPrimaryAddress())
} else {
for _, address := range user.GetAddresses() {
f.printAccountAddressInfo(w, user, address)
}
}
}
func (f *frontendCLI) printAccountAddressInfo(w io.Writer, user types.User, address string) {
fmt.Fprintln(w, "Configuration for", address)
smtpSecurity := "STARTTLS"
if f.settings.GetBool(settings.SMTPSSLKey) {
smtpSecurity = "SSL"
}
fmt.Fprintf(w, "IMAP port: %d\nIMAP security: %s\nSMTP port: %d\nSMTP security: %s\nUsername: %s\nPassword: %s\n",
143,
"STARTTLS",
25,
smtpSecurity,
address,
user.GetBridgePassword(),
)
fmt.Fprintln(w, "")
}

View File

@@ -0,0 +1,24 @@
package cli
import (
"strconv"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
)
func (f *frontendCLI) getUserByIndexOrName(account string) types.User {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if index, err := strconv.Atoi(account); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == account {
return user
}
}
return nil
}

View File

@@ -0,0 +1,157 @@
// Package cli provides HTTP interface of the bridge
package cli
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
)
type frontendCLI struct {
*httprouter.Router
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updater types.Updater
bridge types.Bridger
restarter types.Restarter
}
func New(
panicHandler types.PanicHandler,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
bridge types.Bridger,
restarter types.Restarter,
) *frontendCLI {
fe := &frontendCLI{
Router: httprouter.New(),
locations: locations,
settings: settings,
eventListener: eventListener,
updater: updater,
bridge: bridge,
restarter: restarter,
}
fe.PUT("/accounts", fe.loginAccount)
fe.GET("/accounts", fe.listAccounts)
fe.GET("/accounts/:account", fe.showAccountInfo)
fe.DELETE("/accounts/:account", fe.deleteAccount)
return fe
}
func (f *frontendCLI) loginWithEnv() {
if len(f.bridge.GetUsers()) > 0 {
fmt.Println("More than 0 accounts found. Skip auto login.")
return
}
username := os.Getenv("PROTON_USERNAME")
password := os.Getenv("PROTON_PASSWORD")
if username == "" {
logrus.Info("PROTON_USERNAME and PROTON_PASSWORD are not set. Skip auto login.")
return
}
client, auth, err := f.bridge.Login(username, []byte(password))
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
if auth.HasTwoFactor() {
twoFactor := os.Getenv("PROTON_2FA")
if twoFactor == "" {
logrus.Warn("Login failed: 2FA enabled for the account but PROTON_2FA was not set.")
return
}
err = client.Auth2FA(context.Background(), twoFactor)
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
}
mailboxPassword := password
if auth.HasMailboxPassword() {
mailboxPassword = os.Getenv("PROTON_MAILBOX_PASSWORD")
if mailboxPassword == "" {
logrus.Warn("Login failed: Two password mode enabled but PROTON_MAILBOX_PASSWORD was not set.")
return
}
}
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
logrus.Infof("Account %s was added successfully.\n", user.Username())
if strings.ToLower(os.Getenv("PROTON_PRINT_ACCOUNT_INFO")) != "off" {
f.printAccountInfo(os.Stdout, user)
}
}
func (f *frontendCLI) watchEvents() {
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
fmt.Println("Bridge failed:", errorDetails)
case <-credentialsErrorCh:
f.notifyCredentialsError()
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
f.notifyInternetOn()
case address := <-addressChangedCh:
fmt.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.bridge.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) Loop() error {
f.loginWithEnv()
http.ListenAndServe(":1080", f)
return nil
}
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {}
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
func (f *frontendCLI) NotifySilentUpdateError(err error) {}

View File

@@ -0,0 +1,57 @@
package cli
import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
)
func (f *frontendCLI) processAPIError(err error) {
switch err {
case pmapi.ErrNoConnection:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
}
}
func (f *frontendCLI) notifyInternetOff() {
logrus.Warn("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
logrus.Info("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
logrus.Infof("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
logrus.Info("Upgrade needed. Please download and install the newest version of application.")
}
func (f *frontendCLI) notifyCredentialsError() {
logrus.Error(`ProtonMail Bridge is not able to detect a supported password manager
(secret-service or pass). Please install and set up a supported password manager
and restart the application.
`)
}
func (f *frontendCLI) notifyCertIssue() {
// Print in 80-column width.
logrus.Error(`Connection security error: Your network connection to Proton services may
be insecure.
Description:
ProtonMail Bridge was not able to establish a secure connection to Proton
servers due to a TLS certificate error. This means your connection may
potentially be insecure and susceptible to monitoring by third parties.
Recommendation:
* If you trust your network operator, you can continue to use ProtonMail
as usual.
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
(such as ProtonVPN) which encrypts your Internet connection, or use
a different network to access ProtonMail.
`)
}