forked from Silverfish/proton-bridge
feat(BRIDGE-14): HV3 implementation - GUI & CLI; ownership verification & CAPTCHA are supported
This commit is contained in:
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd
|
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -40,6 +40,8 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek
|
|||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9 h1:tcQpGQljNsZmfuA6L4hAzio8/AIx5OXcU2JUdyX/qxw=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9 h1:tcQpGQljNsZmfuA6L4hAzio8/AIx5OXcU2JUdyX/qxw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436 h1:ej+W9+UQlb2owkT5arCegmUFkicwesMyFHgBp/wwNg8=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
@ -123,15 +124,20 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
||||||
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
|
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte, hvDetails *proton.APIHVDetails) (*proton.Client, proton.Auth, error) {
|
||||||
logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
||||||
|
|
||||||
if username == "crash@bandicoot" {
|
if username == "crash@bandicoot" {
|
||||||
panic("Your wish is my command.. I crash!")
|
panic("Your wish is my command.. I crash!")
|
||||||
}
|
}
|
||||||
|
client, auth, err := bridge.api.NewClientWithLoginWithHVToken(ctx, username, password, hvDetails)
|
||||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hv.IsHvRequest(err) {
|
||||||
|
logUser.WithFields(logrus.Fields{"username": logging.Sensitive(username),
|
||||||
|
"loginError": err.Error()}).Info("Human Verification requested for login")
|
||||||
|
return nil, proton.Auth{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,12 +160,13 @@ func (bridge *Bridge) LoginUser(
|
|||||||
client *proton.Client,
|
client *proton.Client,
|
||||||
auth proton.Auth,
|
auth proton.Auth,
|
||||||
keyPass []byte,
|
keyPass []byte,
|
||||||
|
hvDetails *proton.APIHVDetails,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
||||||
|
|
||||||
userID, err := try.CatchVal(
|
userID, err := try.CatchVal(
|
||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass, hvDetails)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -192,7 +199,8 @@ func (bridge *Bridge) LoginFull(
|
|||||||
) (string, error) {
|
) (string, error) {
|
||||||
logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
||||||
|
|
||||||
client, auth, err := bridge.LoginAuth(ctx, username, password)
|
// (atanas) the following may need to be modified once HV is merged (its used only for testing; and depends on whether we will test HV related logic)
|
||||||
|
client, auth, err := bridge.LoginAuth(ctx, username, password, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to begin login process: %w", err)
|
return "", fmt.Errorf("failed to begin login process: %w", err)
|
||||||
}
|
}
|
||||||
@ -225,7 +233,7 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
logUser.WithError(err).Error("Failed to delete auth")
|
logUser.WithError(err).Error("Failed to delete auth")
|
||||||
@ -374,8 +382,8 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte, hvDetails *proton.APIHVDetails) (string, error) {
|
||||||
apiUser, err := client.GetUser(ctx)
|
apiUser, err := client.GetUserWithHV(ctx, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get API user: %w", err)
|
return "", fmt.Errorf("failed to get API user: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,13 +29,11 @@ using namespace bridgepp;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
|
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
|
||||||
|
QString const HV_ERROR_TEMPLATE = "failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: CAPTCHA validation failed (Code=12087, Status=422)";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -349,6 +347,7 @@ Status GRPCService::ForceLauncher(ServerContext *, StringValue const *request, E
|
|||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *request, Empty *) {
|
Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *request, Empty *) {
|
||||||
|
resetHv();
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
app().log().info(QString("SetMainExecutable: %1").arg(QString::fromStdString(request->value())));
|
app().log().info(QString("SetMainExecutable: %1").arg(QString::fromStdString(request->value())));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -418,7 +417,19 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usersTab.nextUserHvRequired() && !hvWasRequested_ && previousHvUsername_ != QString::fromStdString(request->username())) {
|
||||||
|
hvWasRequested_ = true;
|
||||||
|
previousHvUsername_ = QString::fromStdString(request->username());
|
||||||
|
qtProxy_.sendDelayedEvent(newLoginHvRequestedEvent());
|
||||||
|
return Status::OK;
|
||||||
|
} else {
|
||||||
|
hvWasRequested_ = false;
|
||||||
|
previousHvUsername_ = "";
|
||||||
|
}
|
||||||
|
if (usersTab.nextUserHvError()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::HV_ERROR, HV_ERROR_TEMPLATE));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
if (usersTab.nextUserUsernamePasswordError()) {
|
if (usersTab.nextUserUsernamePasswordError()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -495,6 +506,7 @@ Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::LoginAbort(ServerContext *, LoginAbortRequest const *request, Empty *) {
|
Status GRPCService::LoginAbort(ServerContext *, LoginAbortRequest const *request, Empty *) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
|
this->resetHv();
|
||||||
loginUsername_ = QString();
|
loginUsername_ = QString();
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
@ -953,3 +965,11 @@ void GRPCService::finishLogin() {
|
|||||||
|
|
||||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void GRPCService::resetHv() {
|
||||||
|
hvWasRequested_ = false;
|
||||||
|
previousHvUsername_ = "";
|
||||||
|
}
|
||||||
|
|||||||
@ -106,6 +106,7 @@ public: // member functions.
|
|||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
void finishLogin(); ///< finish the login procedure once the credentials have been validated.
|
void finishLogin(); ///< finish the login procedure once the credentials have been validated.
|
||||||
|
void resetHv(); ///< Resets the human verification state.
|
||||||
|
|
||||||
private: // data member
|
private: // data member
|
||||||
mutable QMutex eventStreamMutex_; ///< Mutex used to access eventQueue_, isStreaming_ and shouldStopStreaming_;
|
mutable QMutex eventStreamMutex_; ///< Mutex used to access eventQueue_, isStreaming_ and shouldStopStreaming_;
|
||||||
@ -113,6 +114,8 @@ private: // data member
|
|||||||
bool isStreaming_; ///< Is the gRPC stream running. Access protected by eventStreamMutex_;
|
bool isStreaming_; ///< Is the gRPC stream running. Access protected by eventStreamMutex_;
|
||||||
bool eventStreamShouldStop_; ///< Should the stream be stopped? Access protected by eventStreamMutex
|
bool eventStreamShouldStop_; ///< Should the stream be stopped? Access protected by eventStreamMutex
|
||||||
QString loginUsername_; ///< The username used for the current login procedure.
|
QString loginUsername_; ///< The username used for the current login procedure.
|
||||||
|
QString previousHvUsername_; ///< The previous username used for HV.
|
||||||
|
bool hvWasRequested_ {false}; ///< Was human verification requested.
|
||||||
GRPCQtProxy qtProxy_; ///< Qt Proxy used to send signals, as this class is not a QObject.
|
GRPCQtProxy qtProxy_; ///< Qt Proxy used to send signals, as this class is not a QObject.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -277,6 +277,22 @@ bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true if the next login attempt should trigger a human verification request
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool UsersTab::nextUserHvRequired() const {
|
||||||
|
return ui_.checkHV3Required->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true if the next login attempt should trigger a human verification error
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool UsersTab::nextUserHvError() const {
|
||||||
|
return ui_.checkHV3Error->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return true iff the next login attempt should trigger a username/password error.
|
/// \return true iff the next login attempt should trigger a username/password error.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -39,6 +39,8 @@ public: // member functions.
|
|||||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
||||||
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
||||||
|
bool nextUserHvRequired() const; ///< Check if next user login should trigger HV
|
||||||
|
bool nextUserHvError() const; ///< Check if next user login should trigger HV error
|
||||||
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
||||||
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||||
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
||||||
|
|||||||
@ -290,6 +290,20 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkHV3Required">
|
||||||
|
<property name="text">
|
||||||
|
<string>HV3 required</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkHV3Error">
|
||||||
|
<property name="text">
|
||||||
|
<string>HV3 error</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="checkFreeUserError">
|
<widget class="QCheckBox" name="checkFreeUserError">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
|||||||
@ -810,6 +810,18 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QMLBackend::loginHv(QString const &username, QString const &password) const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||||
|
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||||
|
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||||
|
}
|
||||||
|
app().grpc().loginHv(username, password);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
@ -1334,6 +1346,8 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
|
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
|
||||||
connect(client, &GRPCClient::loginFinished, this, &QMLBackend::onLoginFinished);
|
connect(client, &GRPCClient::loginFinished, this, &QMLBackend::onLoginFinished);
|
||||||
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, &QMLBackend::onLoginAlreadyLoggedIn);
|
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, &QMLBackend::onLoginAlreadyLoggedIn);
|
||||||
|
connect(client, &GRPCClient::loginHvRequested, this, &QMLBackend::loginHvRequested);
|
||||||
|
connect(client, &GRPCClient::loginHvError, this, &QMLBackend::loginHvError);
|
||||||
|
|
||||||
// update events
|
// update events
|
||||||
connect(client, &GRPCClient::updateManualError, this, &QMLBackend::updateManualError);
|
connect(client, &GRPCClient::updateManualError, this, &QMLBackend::updateManualError);
|
||||||
|
|||||||
@ -183,6 +183,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
|
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
|
||||||
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
|
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
|
||||||
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
|
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
|
||||||
|
void loginHv(QString const &username, QString const &password) const; ///< Slot for the login button (after HV challenge completed).
|
||||||
void login2FA(QString const &username, QString const &code) const; ///< Slot for the login button (2FA login).
|
void login2FA(QString const &username, QString const &code) const; ///< Slot for the login button (2FA login).
|
||||||
void login2Password(QString const &username, QString const &password) const; ///< Slot for the login button (mailbox password login).
|
void login2Password(QString const &username, QString const &password) const; ///< Slot for the login button (mailbox password login).
|
||||||
void loginAbort(QString const &username) const; ///< Slot for the login abort procedure.
|
void loginAbort(QString const &username) const; ///< Slot for the login abort procedure.
|
||||||
@ -238,6 +239,8 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
||||||
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
||||||
void loginAlreadyLoggedIn(int index); ///< Signal for the 'loginAlreadyLoggedIn' gRPC stream event.
|
void loginAlreadyLoggedIn(int index); ///< Signal for the 'loginAlreadyLoggedIn' gRPC stream event.
|
||||||
|
void loginHvRequested(QString const &hvUrl); ///< Signal for the 'loginHvRequested' gRPC stream event.
|
||||||
|
void loginHvError(QString const &errorMsg); ///< Signal for the 'loginHvError' gRPC stream event.
|
||||||
void updateManualReady(QString const &version); ///< Signal for the 'updateManualReady' gRPC stream event.
|
void updateManualReady(QString const &version); ///< Signal for the 'updateManualReady' gRPC stream event.
|
||||||
void updateManualRestartNeeded(); ///< Signal for the 'updateManualRestartNeeded' gRPC stream event.
|
void updateManualRestartNeeded(); ///< Signal for the 'updateManualRestartNeeded' gRPC stream event.
|
||||||
void updateManualError(); ///< Signal for the 'updateManualError' gRPC stream event.
|
void updateManualError(); ///< Signal for the 'updateManualError' gRPC stream event.
|
||||||
|
|||||||
@ -60,7 +60,7 @@ QtObject {
|
|||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion]
|
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent]
|
||||||
property Notification alreadyLoggedIn: Notification {
|
property Notification alreadyLoggedIn: Notification {
|
||||||
brief: qsTr("Already signed in")
|
brief: qsTr("Already signed in")
|
||||||
description: qsTr("This account is already signed in.")
|
description: qsTr("This account is already signed in.")
|
||||||
@ -1130,6 +1130,27 @@ QtObject {
|
|||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
property Notification hvErrorEvent: Notification {
|
||||||
|
group: Notifications.Group.Configuration
|
||||||
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
|
type: Notification.NotificationType.Danger
|
||||||
|
|
||||||
|
action: Action {
|
||||||
|
text: qsTr("OK")
|
||||||
|
onTriggered: {
|
||||||
|
root.hvErrorEvent.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onLoginHvError(errorMsg) {
|
||||||
|
root.hvErrorEvent.active = true;
|
||||||
|
root.hvErrorEvent.description = errorMsg;
|
||||||
|
}
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||||
signal askDeleteAccount(var user)
|
signal askDeleteAccount(var user)
|
||||||
|
|||||||
@ -20,12 +20,14 @@ FocusScope {
|
|||||||
enum RootStack {
|
enum RootStack {
|
||||||
Login,
|
Login,
|
||||||
TOTP,
|
TOTP,
|
||||||
MailboxPassword
|
MailboxPassword,
|
||||||
|
HV
|
||||||
}
|
}
|
||||||
|
|
||||||
property alias currentIndex: stackLayout.currentIndex
|
property alias currentIndex: stackLayout.currentIndex
|
||||||
property alias username: usernameTextField.text
|
property alias username: usernameTextField.text
|
||||||
property var wizard
|
property var wizard
|
||||||
|
property string hvLinkUrl: ""
|
||||||
|
|
||||||
signal loginAbort(string username, bool wasSignedOut)
|
signal loginAbort(string username, bool wasSignedOut)
|
||||||
|
|
||||||
@ -47,6 +49,14 @@ FocusScope {
|
|||||||
passwordTextField.hidePassword();
|
passwordTextField.hidePassword();
|
||||||
secondPasswordTextField.hidePassword();
|
secondPasswordTextField.hidePassword();
|
||||||
}
|
}
|
||||||
|
function resetViaHv() {
|
||||||
|
usernameTextField.enabled = false;
|
||||||
|
passwordTextField.enabled = false;
|
||||||
|
signInButton.loading = true;
|
||||||
|
secondPasswordButton.loading = false;
|
||||||
|
secondPasswordTextField.enabled = true;
|
||||||
|
totpLayout.reset();
|
||||||
|
}
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: stackLayout
|
id: stackLayout
|
||||||
@ -124,6 +134,18 @@ FocusScope {
|
|||||||
else
|
else
|
||||||
errorLabel.text = qsTr("Incorrect login credentials");
|
errorLabel.text = qsTr("Incorrect login credentials");
|
||||||
}
|
}
|
||||||
|
function onLoginHvRequested(hvUrl) {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected loginHvRequested");
|
||||||
|
stackLayout.currentIndex = Login.RootStack.HV;
|
||||||
|
hvUsernameLabel.text = usernameTextField.text;
|
||||||
|
hvLinkUrl = hvUrl;
|
||||||
|
}
|
||||||
|
function onLoginHvError(_) {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected onLoginHvInvalidTokenError");
|
||||||
|
stackLayout.currentIndex = Login.RootStack.Login;
|
||||||
|
root.resetViaHv();
|
||||||
|
root.reset()
|
||||||
|
}
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
@ -475,5 +497,112 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
|
id: hvLayout
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: ProtonStyle.wizard_spacing_extra_large
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_small
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Human verification")
|
||||||
|
type: Label.LabelType.Title
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hvUsernameLabel
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: wizard.colorScheme.text_weak
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Please open the following link in your favourite web browser to verify you are human.")
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hvRequestedUrlText
|
||||||
|
type: Label.LabelType.Lead
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
text: "<a href='" + hvLinkUrl + "'>" + hvLinkUrl.replace("&", "&")+ "</a>"
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
Qt.openUrlExternally(hvLinkUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: hVContinueButton
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
text: qsTr("Continue")
|
||||||
|
|
||||||
|
function checkAndSignInHv() {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv")
|
||||||
|
stackLayout.currentIndex = Login.RootStack.Login
|
||||||
|
usernameTextField.validate();
|
||||||
|
passwordTextField.validate();
|
||||||
|
if (usernameTextField.error || passwordTextField.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.resetViaHv();
|
||||||
|
Backend.loginHv(usernameTextField.text, Qt.btoa(passwordTextField.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
checkAndSignInHv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
secondary: true
|
||||||
|
secondaryIsOpaque: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
onClicked: {
|
||||||
|
root.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -302,6 +302,18 @@ SPStreamEvent newLoginTfaRequestedEvent(QString const &username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The event.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
SPStreamEvent newLoginHvRequestedEvent() {
|
||||||
|
auto event = new ::grpc::LoginHvRequestedEvent;
|
||||||
|
event->set_hvurl("https://verify.proton.me/?methods=captcha&token=SOME_RANDOM_TOKEN");
|
||||||
|
auto loginEvent = new grpc::LoginEvent;
|
||||||
|
loginEvent->set_allocated_hvrequested(event);
|
||||||
|
return wrapLoginEvent(loginEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
/// \return The event.
|
/// \return The event.
|
||||||
|
|||||||
@ -48,6 +48,7 @@ SPStreamEvent newLoginTfaRequestedEvent(QString const &username); ///< Create a
|
|||||||
SPStreamEvent newLoginTwoPasswordsRequestedEvent(QString const &username); ///< Create a new LoginTwoPasswordsRequestedEvent event.
|
SPStreamEvent newLoginTwoPasswordsRequestedEvent(QString const &username); ///< Create a new LoginTwoPasswordsRequestedEvent event.
|
||||||
SPStreamEvent newLoginFinishedEvent(QString const &userID, bool wasSignedOut); ///< Create a new LoginFinishedEvent event.
|
SPStreamEvent newLoginFinishedEvent(QString const &userID, bool wasSignedOut); ///< Create a new LoginFinishedEvent event.
|
||||||
SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID); ///< Create a new LoginAlreadyLoggedInEvent event.
|
SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID); ///< Create a new LoginAlreadyLoggedInEvent event.
|
||||||
|
SPStreamEvent newLoginHvRequestedEvent(); ///< Create a new LoginHvRequestedEvent
|
||||||
|
|
||||||
// Update related events
|
// Update related events
|
||||||
SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create a new UpdateErrorEvent event.
|
SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create a new UpdateErrorEvent event.
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
#include "../ProcessMonitor.h"
|
#include "../ProcessMonitor.h"
|
||||||
#include "../Log/LogUtils.h"
|
#include "../Log/LogUtils.h"
|
||||||
|
|
||||||
|
|
||||||
using namespace google::protobuf;
|
using namespace google::protobuf;
|
||||||
using namespace grpc;
|
using namespace grpc;
|
||||||
|
|
||||||
@ -607,6 +606,20 @@ grpc::Status GRPCClient::login(QString const &username, QString const &password)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] username The username.
|
||||||
|
/// \param[in] password The password.
|
||||||
|
/// \return the status for the gRPC call.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
grpc::Status GRPCClient::loginHv(QString const &username, QString const &password) {
|
||||||
|
LoginRequest request;
|
||||||
|
request.set_username(username.toStdString());
|
||||||
|
request.set_password(password.toStdString());
|
||||||
|
request.set_usehvdetails(true);
|
||||||
|
return this->logGRPCCallStatus(stub_->Login(this->clientContext().get(), request, &empty), __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
/// \param[in] code The The 2FA code.
|
/// \param[in] code The The 2FA code.
|
||||||
@ -1221,6 +1234,9 @@ void GRPCClient::processLoginEvent(LoginEvent const &event) {
|
|||||||
case TWO_PASSWORDS_ABORT:
|
case TWO_PASSWORDS_ABORT:
|
||||||
emit login2PasswordErrorAbort(QString::fromStdString(error.message()));
|
emit login2PasswordErrorAbort(QString::fromStdString(error.message()));
|
||||||
break;
|
break;
|
||||||
|
case HV_ERROR:
|
||||||
|
emit loginHvError(QString::fromStdString(error.message()));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown login error event received.");
|
this->logError("Unknown login error event received.");
|
||||||
break;
|
break;
|
||||||
@ -1245,6 +1261,10 @@ void GRPCClient::processLoginEvent(LoginEvent const &event) {
|
|||||||
this->logTrace("Login event received: AlreadyLoggedIn.");
|
this->logTrace("Login event received: AlreadyLoggedIn.");
|
||||||
emit loginAlreadyLoggedIn(QString::fromStdString(event.finished().userid()));
|
emit loginAlreadyLoggedIn(QString::fromStdString(event.finished().userid()));
|
||||||
break;
|
break;
|
||||||
|
case LoginEvent::kHvRequested:
|
||||||
|
this->logTrace("Login event Received: HvRequested");
|
||||||
|
emit loginHvRequested(QString::fromStdString(event.hvrequested().hvurl()));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown Login event received.");
|
this->logError("Unknown Login event received.");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -155,6 +155,7 @@ public: // login related calls
|
|||||||
grpc::Status login2FA(QString const &username, QString const &code); ///< Performs the 'login2FA' call.
|
grpc::Status login2FA(QString const &username, QString const &code); ///< Performs the 'login2FA' call.
|
||||||
grpc::Status login2Passwords(QString const &username, QString const &password); ///< Performs the 'login2Passwords' call.
|
grpc::Status login2Passwords(QString const &username, QString const &password); ///< Performs the 'login2Passwords' call.
|
||||||
grpc::Status loginAbort(QString const &username); ///< Performs the 'loginAbort' call.
|
grpc::Status loginAbort(QString const &username); ///< Performs the 'loginAbort' call.
|
||||||
|
grpc::Status loginHv(QString const &username, QString const &password); ///< Performs the 'login' call with additional useHv flag
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void loginUsernamePasswordError(QString const &errMsg);
|
void loginUsernamePasswordError(QString const &errMsg);
|
||||||
@ -168,6 +169,8 @@ signals:
|
|||||||
void login2PasswordErrorAbort(QString const &errMsg);
|
void login2PasswordErrorAbort(QString const &errMsg);
|
||||||
void loginFinished(QString const &userID, bool wasSignedOut);
|
void loginFinished(QString const &userID, bool wasSignedOut);
|
||||||
void loginAlreadyLoggedIn(QString const &userID);
|
void loginAlreadyLoggedIn(QString const &userID);
|
||||||
|
void loginHvRequested(QString const &hvUrl);
|
||||||
|
void loginHvError(QString const &errMsg);
|
||||||
|
|
||||||
public: // Update related calls
|
public: // Update related calls
|
||||||
grpc::Status checkUpdate();
|
grpc::Status checkUpdate();
|
||||||
|
|||||||
@ -19,12 +19,14 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/abiosoft/ishell"
|
"github.com/abiosoft/ishell"
|
||||||
)
|
)
|
||||||
@ -116,6 +118,13 @@ func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address strin
|
|||||||
f.Println("")
|
f.Println("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) promptHvURL(details *proton.APIHVDetails) {
|
||||||
|
hvURL := hv.FormatHvURL(details)
|
||||||
|
fmt.Print("\nHuman Verification requested. Please open the URL below in a browser and press ENTER when the challenge has been completed.\n\n", hvURL+"\n")
|
||||||
|
f.ReadLine()
|
||||||
|
fmt.Println("Authenticating ...")
|
||||||
|
}
|
||||||
|
|
||||||
func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
||||||
f.ShowPrompt(false)
|
f.ShowPrompt(false)
|
||||||
defer f.ShowPrompt(true)
|
defer f.ShowPrompt(true)
|
||||||
@ -144,7 +153,19 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
|||||||
|
|
||||||
f.Println("Authenticating ... ")
|
f.Println("Authenticating ... ")
|
||||||
|
|
||||||
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password))
|
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), nil)
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvErr != nil || hvDetails != nil {
|
||||||
|
if hvErr != nil {
|
||||||
|
f.printAndLogError("Cannot login", hvErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.promptHvURL(hvDetails)
|
||||||
|
client, auth, err = f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.printAndLogError("Cannot login: ", err)
|
f.printAndLogError("Cannot login: ", err)
|
||||||
return
|
return
|
||||||
@ -175,7 +196,55 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
|||||||
keyPass = []byte(password)
|
keyPass = []byte(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass)
|
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
|
||||||
|
|
||||||
|
hvDetails, hvErr = hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvDetails != nil || hvErr != nil {
|
||||||
|
if hvErr != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", hvErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.loginAccountHv(c, loginName, password, keyPass, hvDetails)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := f.bridge.GetUserInfo(userID)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Printf("Account %s was added successfully.\n", bold(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) loginAccountHv(c *ishell.Context, loginName string, password string, keyPass []byte, hvDetails *proton.APIHVDetails) {
|
||||||
|
f.promptHvURL(hvDetails)
|
||||||
|
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
||||||
|
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
|
||||||
|
if code == "" {
|
||||||
|
f.printAndLogError("Cannot login: need two factor code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Auth2FA(context.Background(), proton.Auth2FAReq{TwoFactorCode: code}); err != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.processAPIError(err)
|
f.processAPIError(err)
|
||||||
return
|
return
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -168,6 +168,7 @@ message ReportBugRequest {
|
|||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
string username = 1;
|
string username = 1;
|
||||||
bytes password = 2;
|
bytes password = 2;
|
||||||
|
optional bool useHvDetails = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginAbortRequest {
|
message LoginAbortRequest {
|
||||||
@ -308,6 +309,7 @@ message LoginEvent {
|
|||||||
LoginTwoPasswordsRequestedEvent twoPasswordRequested = 3;
|
LoginTwoPasswordsRequestedEvent twoPasswordRequested = 3;
|
||||||
LoginFinishedEvent finished = 4;
|
LoginFinishedEvent finished = 4;
|
||||||
LoginFinishedEvent alreadyLoggedIn = 5;
|
LoginFinishedEvent alreadyLoggedIn = 5;
|
||||||
|
LoginHvRequestedEvent hvRequested = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +321,7 @@ enum LoginErrorType {
|
|||||||
TFA_ABORT = 4;
|
TFA_ABORT = 4;
|
||||||
TWO_PASSWORDS_ERROR = 5;
|
TWO_PASSWORDS_ERROR = 5;
|
||||||
TWO_PASSWORDS_ABORT = 6;
|
TWO_PASSWORDS_ABORT = 6;
|
||||||
|
HV_ERROR = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginErrorEvent {
|
message LoginErrorEvent {
|
||||||
@ -339,6 +342,10 @@ message LoginFinishedEvent {
|
|||||||
bool wasSignedOut = 2;
|
bool wasSignedOut = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LoginHvRequestedEvent {
|
||||||
|
string hvUrl = 1;
|
||||||
|
}
|
||||||
|
|
||||||
//**********************************************************
|
//**********************************************************
|
||||||
// Update related events
|
// Update related events
|
||||||
//**********************************************************
|
//**********************************************************
|
||||||
|
|||||||
@ -100,6 +100,10 @@ func NewLoginAlreadyLoggedInEvent(userID string) *StreamEvent {
|
|||||||
return loginEvent(&LoginEvent{Event: &LoginEvent_AlreadyLoggedIn{AlreadyLoggedIn: &LoginFinishedEvent{UserID: userID}}})
|
return loginEvent(&LoginEvent{Event: &LoginEvent_AlreadyLoggedIn{AlreadyLoggedIn: &LoginFinishedEvent{UserID: userID}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewLoginHvRequestedEvent(hvChallengeURL string) *StreamEvent {
|
||||||
|
return loginEvent(&LoginEvent{Event: &LoginEvent_HvRequested{HvRequested: &LoginHvRequestedEvent{HvUrl: hvChallengeURL}}})
|
||||||
|
}
|
||||||
|
|
||||||
func NewUpdateErrorEvent(errorType UpdateErrorType) *StreamEvent {
|
func NewUpdateErrorEvent(errorType UpdateErrorType) *StreamEvent {
|
||||||
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Error{Error: &UpdateErrorEvent{Type: errorType}}})
|
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Error{Error: &UpdateErrorEvent{Type: errorType}}})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
@ -95,6 +96,9 @@ type Service struct { // nolint:structcheck
|
|||||||
parentPID int
|
parentPID int
|
||||||
parentPIDDoneCh chan struct{}
|
parentPIDDoneCh chan struct{}
|
||||||
showOnStartup bool
|
showOnStartup bool
|
||||||
|
|
||||||
|
hvDetails *proton.APIHVDetails
|
||||||
|
useHvDetails bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a new instance of the service.
|
// NewService returns a new instance of the service.
|
||||||
@ -412,6 +416,7 @@ func (s *Service) loginClean() {
|
|||||||
s.password[i] = '\x00'
|
s.password[i] = '\x00'
|
||||||
}
|
}
|
||||||
s.password = s.password[0:0]
|
s.password = s.password[0:0]
|
||||||
|
s.useHvDetails = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) finishLogin() {
|
func (s *Service) finishLogin() {
|
||||||
@ -424,6 +429,11 @@ func (s *Service) finishLogin() {
|
|||||||
|
|
||||||
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
if s.useHvDetails {
|
||||||
|
hvDetails = s.hvDetails
|
||||||
|
}
|
||||||
|
|
||||||
if len(s.password) == 0 || s.auth.UID == "" || s.authClient == nil {
|
if len(s.password) == 0 || s.auth.UID == "" || s.authClient == nil {
|
||||||
s.log.
|
s.log.
|
||||||
WithField("hasPass", len(s.password) != 0).
|
WithField("hasPass", len(s.password) != 0).
|
||||||
@ -439,8 +449,20 @@ func (s *Service) finishLogin() {
|
|||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
|
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hv.IsHvRequest(err) {
|
||||||
|
s.handleHvRequest(err)
|
||||||
|
performCleanup = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.HumanValidationInvalidToken {
|
||||||
|
s.hvDetails = nil
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.log.WithError(err).Errorf("Finish login failed")
|
s.log.WithError(err).Errorf("Finish login failed")
|
||||||
s.twoPasswordAttemptCount++
|
s.twoPasswordAttemptCount++
|
||||||
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
||||||
@ -614,6 +636,18 @@ func (s *Service) monitorParentPID() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) handleHvRequest(err error) {
|
||||||
|
hvDet, hvErr := hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvErr != nil {
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hvErr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.hvDetails = hvDet
|
||||||
|
hvChallengeURL := hv.FormatHvURL(hvDet)
|
||||||
|
_ = s.SendEvent(NewLoginHvRequestedEvent(hvChallengeURL))
|
||||||
|
}
|
||||||
|
|
||||||
// computeFileSocketPath Return an available path for a socket file in the temp folder.
|
// computeFileSocketPath Return an available path for a socket file in the temp folder.
|
||||||
func computeFileSocketPath() (string, error) {
|
func computeFileSocketPath() (string, error) {
|
||||||
tempPath := os.TempDir()
|
tempPath := os.TempDir()
|
||||||
|
|||||||
@ -396,6 +396,14 @@ func (s *Service) RequestKnowledgeBaseSuggestions(_ context.Context, userInput *
|
|||||||
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
||||||
s.log.WithField("username", login.Username).Debug("Login")
|
s.log.WithField("username", login.Username).Debug("Login")
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
if login.UseHvDetails != nil && *login.UseHvDetails {
|
||||||
|
hvDetails = s.hvDetails
|
||||||
|
s.useHvDetails = true
|
||||||
|
} else {
|
||||||
|
s.useHvDetails = false
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
@ -407,7 +415,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password)
|
client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer s.loginClean()
|
defer s.loginClean()
|
||||||
|
|
||||||
@ -421,6 +429,13 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
case proton.PaidPlanRequired:
|
case proton.PaidPlanRequired:
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
|
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
|
||||||
|
|
||||||
|
case proton.HumanVerificationRequired:
|
||||||
|
s.handleHvRequest(apiErr)
|
||||||
|
|
||||||
|
case proton.HumanValidationInvalidToken:
|
||||||
|
s.hvDetails = nil
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error()))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
||||||
}
|
}
|
||||||
@ -522,7 +537,6 @@ func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.loginAbort()
|
s.loginAbort()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
62
internal/hv/hv.go
Normal file
62
internal/hv/hv.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
// Proton Mail 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.
|
||||||
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package hv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyAndExtractHvRequest expects an error request as input
|
||||||
|
// determines whether the given error is a Proton human verification request; if it isn't then it returns -> nil, nil (no details, no error)
|
||||||
|
// if it is a HV req. then it tries to parse the json data and verify that the captcha method is included; if either fails -> nil, err
|
||||||
|
// if the HV request was successfully decoded and the preconditions were met it returns the hv details -> hvDetails, nil.
|
||||||
|
func VerifyAndExtractHvRequest(err error) (*proton.APIHVDetails, error) {
|
||||||
|
if err == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var protonErr *proton.APIError
|
||||||
|
if errors.As(err, &protonErr) && protonErr.IsHVError() {
|
||||||
|
hvDetails, hvErr := protonErr.GetHVDetails()
|
||||||
|
if hvErr != nil {
|
||||||
|
return nil, fmt.Errorf("received HV request, but can't decode HV details")
|
||||||
|
}
|
||||||
|
return hvDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHvRequest(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var protonErr *proton.APIError
|
||||||
|
if errors.As(err, &protonErr) && protonErr.IsHVError() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatHvURL(details *proton.APIHVDetails) string {
|
||||||
|
return fmt.Sprintf("https://verify.proton.me/?methods=%v&token=%v",
|
||||||
|
strings.Join(details.Methods, ","),
|
||||||
|
details.Token)
|
||||||
|
}
|
||||||
144
internal/hv/hv_test.go
Normal file
144
internal/hv/hv_test.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail 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.
|
||||||
|
//
|
||||||
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package hv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyAndExtractHvRequest(t *testing.T) {
|
||||||
|
det1, _ := json.Marshal("test")
|
||||||
|
det2, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"ownership-email"}, Token: "test"})
|
||||||
|
det3, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"captcha"}, Token: "test"})
|
||||||
|
det4, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"ownership-email", "test"}, Token: "test"})
|
||||||
|
det5, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"captcha", "ownership-email"}, Token: "test"})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
err error
|
||||||
|
hasHvDetails bool
|
||||||
|
hasErr bool
|
||||||
|
}{
|
||||||
|
{err: nil,
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: fmt.Errorf("test"),
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: new(proton.APIError),
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Status: 429},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Status: 9001},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: true},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det1},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: true},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det2},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det3},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det4},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det5},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
hvDetails, err := VerifyAndExtractHvRequest(test.err)
|
||||||
|
hasHv := hvDetails != nil
|
||||||
|
hasErr := err != nil
|
||||||
|
require.True(t, hasHv == test.hasHvDetails && hasErr == test.hasErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsHvRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
err error
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
err: nil,
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: fmt.Errorf("test"),
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: new(proton.APIError),
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Status: 429},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Status: 9001},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Code: 9001},
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
isHvRequest := IsHvRequest(test.err)
|
||||||
|
require.Equal(t, test.result, isHvRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatHvURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
details *proton.APIHVDetails
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{"test"}, Token: "test"},
|
||||||
|
result: "https://verify.proton.me/?methods=test&token=test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{""}, Token: "test"},
|
||||||
|
result: "https://verify.proton.me/?methods=&token=test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{"test"}, Token: ""},
|
||||||
|
result: "https://verify.proton.me/?methods=test&token=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, el := range tests {
|
||||||
|
result := FormatHvURL(el.details)
|
||||||
|
require.Equal(t, el.result, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user