Navigation Menu

Skip to content

Commit

Permalink
support login delay: applying the delays
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Mar 28, 2019
1 parent b057f53 commit 75a7aa0
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 78 deletions.
8 changes: 4 additions & 4 deletions src/electron-main/api/endpoints-builders/account.ts
Expand Up @@ -12,7 +12,7 @@ export async function buildEndpoints(
): Promise<Pick<Endpoints, "addAccount" | "updateAccount" | "changeAccountOrder" | "removeAccount">> {
return {
addAccount: (
{type, login, entryUrl, database, credentials, proxy, loginDelayOnSelect, loginDelaySecondsRange},
{type, login, entryUrl, database, credentials, proxy, loginDelayUntilSelected, loginDelaySecondsRange},
) => from((async () => {
const account = {
type,
Expand All @@ -21,7 +21,7 @@ export async function buildEndpoints(
database,
credentials,
proxy,
loginDelayOnSelect,
loginDelayUntilSelected,
loginDelaySecondsRange,
} as AccountConfig; // TODO ger rid of "TS as" casting
const settings = await ctx.settingsStore.readExisting();
Expand All @@ -36,7 +36,7 @@ export async function buildEndpoints(
})()),

updateAccount: (
{login, entryUrl, database, credentials, proxy, loginDelayOnSelect, loginDelaySecondsRange},
{login, entryUrl, database, credentials, proxy, loginDelayUntilSelected, loginDelaySecondsRange},
) => from((async () => {
const settings = await ctx.settingsStore.readExisting();
const account = pickAccountStrict(settings.accounts, {login});
Expand Down Expand Up @@ -64,7 +64,7 @@ export async function buildEndpoints(
account.proxy = proxy;
await configureSessionByAccount(pick(["login", "proxy"], account));

account.loginDelayOnSelect = loginDelayOnSelect;
account.loginDelayUntilSelected = loginDelayUntilSelected;
account.loginDelaySecondsRange = loginDelaySecondsRange;

return await ctx.settingsStore.write(settings);
Expand Down
2 changes: 1 addition & 1 deletion src/shared/model/account.ts
Expand Up @@ -10,7 +10,7 @@ export interface GenericAccountConfig<Type extends AccountType, CredentialFields
proxyRules?: string;
proxyBypassRules?: string;
};
loginDelayOnSelect?: boolean;
loginDelayUntilSelected?: boolean;
loginDelaySecondsRange?: { start: number; end: number; };
}

Expand Down
2 changes: 1 addition & 1 deletion src/shared/model/container.ts
Expand Up @@ -30,4 +30,4 @@ export type AccountConfigCreatePatch<T extends AccountType = AccountType> = Acco

export type AccountConfigUpdatePatch<T extends AccountType = AccountType> = Pick<AccountConfig<T>, "login">
& Partial<Pick<AccountConfig,
"login" | "entryUrl" | "database" | "credentials" | "proxy" | "loginDelayOnSelect" | "loginDelaySecondsRange">>;
"login" | "entryUrl" | "database" | "credentials" | "proxy" | "loginDelayUntilSelected" | "loginDelaySecondsRange">>;
7 changes: 7 additions & 0 deletions src/shared/util.ts
Expand Up @@ -198,6 +198,13 @@ export function isDatabaseBootstrapped(
: Boolean(Object.keys(metadata.groupEntityEventBatchIds || {}).length);
}

export function getRandomInt(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);

return min + Math.floor(Math.random() * (max - min)); // the maximum is exclusive and the minimum is inclusive
}

export const getWebViewPartition: (login: AccountConfig<AccountType>["login"]) => string = (() => {
const prefix = "memory/";
const result: typeof getWebViewPartition = (login) => `${prefix}${login}`;
Expand Down
15 changes: 11 additions & 4 deletions src/web/src/app/_accounts/account.component.ts
Expand Up @@ -40,9 +40,13 @@ type WebViewSubjectState =

let componentIndex = 0;

const pickCredentialFields = ((fields: Array<keyof AccountConfig>) => (ac: AccountConfig) => pick(fields, ac))([
"credentials",
]);
const pickCredentialFields = ((fields: Array<keyof AccountConfig>) => {
return (accountConfig: AccountConfig) => pick(fields, accountConfig);
})(["credentials"]);

const pickLoginDelayFields = ((fields: Array<keyof AccountConfig>) => {
return (accountConfig: AccountConfig) => pick(fields, accountConfig);
})(["loginDelayUntilSelected", "loginDelaySecondsRange"]);

@Component({
selector: "electron-mail-account",
Expand Down Expand Up @@ -236,9 +240,12 @@ export class AccountComponent extends NgChangesObservableComponent implements On
) || (
// creds changed
!equals(pickCredentialFields(prev.accountConfig), pickCredentialFields(curr.accountConfig))
) || (
// login delay values changed
!equals(pickLoginDelayFields(prev.accountConfig), pickLoginDelayFields(curr.accountConfig))
);
}),
map(([prev, curr]) => curr),
map(([, curr]) => curr),
).subscribe((account) => {
this.logger.info(`onWebViewMounted(): dispatch "TryToLogin"`);
this.dispatchInLoggerZone(ACCOUNTS_ACTIONS.TryToLogin({account, webView}));
Expand Down
212 changes: 159 additions & 53 deletions src/web/src/app/_accounts/accounts.effects.ts
@@ -1,5 +1,5 @@
import {Actions, Effect} from "@ngrx/effects";
import {EMPTY, Subject, concat, from, fromEvent, merge, of, throwError, timer} from "rxjs";
import {EMPTY, Observable, Subject, concat, from, fromEvent, merge, of, race, throwError, timer} from "rxjs";
import {Injectable} from "@angular/core";
import {Store, select} from "@ngrx/store";
import {
Expand All @@ -13,6 +13,7 @@ import {
map,
mergeMap,
switchMap,
take,
takeUntil,
tap,
withLatestFrom,
Expand All @@ -26,8 +27,8 @@ import {ElectronService} from "src/web/src/app/_core/electron.service";
import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main";
import {ONE_SECOND_MS} from "src/shared/constants";
import {State} from "src/web/src/app/store/reducers/accounts";
import {getRandomInt, isDatabaseBootstrapped} from "src/shared/util";
import {getZoneNameBoundWebLogger, logActionTypeAndBoundLoggerWithActionType} from "src/web/src/util";
import {isDatabaseBootstrapped} from "src/shared/util";

const rollingRateLimiter = __ELECTRON_EXPOSURE__.require["rolling-rate-limiter"]();
const _logger = getZoneNameBoundWebLogger("[accounts.effects]");
Expand Down Expand Up @@ -252,52 +253,158 @@ export class AccountsEffects {

switch (pageType) {
case "login": {
const password = payload.password || credentials.password;
if (!credentials.password) {
logger.info("fillLogin");

if (password) {
rateLimitingCheck(password);
return this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("fillLogin")({login, zoneName})),
mergeMap(() => of(ACCOUNTS_ACTIONS.Patch({login, patch: {loginFilledOnce: true}}))),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
);
}

logger.info("login");
return merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {password: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("login")({login, password, zoneName})),
mergeMap(() => EMPTY),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {password: false}}))),
const delayTriggers: Array<Observable<{ trigger: string }>> = [];
const {loginDelaySecondsRange, loginDelayUntilSelected = false} = accountConfig;

logger.info(`login delay configs: ${JSON.stringify({loginDelayUntilSelected, loginDelaySecondsRange})}`);

if (loginDelaySecondsRange) {
const {start, end} = loginDelaySecondsRange;
const delayTime = getRandomInt(start, end) * ONE_SECOND_MS;

logger.info(`resolved login delay (ms): ${delayTime}`);

delayTriggers.push(
timer(delayTime).pipe(
map(() => ({trigger: `triggered on login delay expiration (ms): ${delayTime}`})),
),
);
}

logger.info("fillLogin");
return this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("fillLogin")({login, zoneName})),
mergeMap(() => of(ACCOUNTS_ACTIONS.Patch({login, patch: {loginFilledOnce: true}}))),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
if (loginDelayUntilSelected) {
delayTriggers.push(
this.store.pipe(
select(AccountsSelectors.FEATURED.selectedLogin),
filter((selectedLogin) => selectedLogin === login),
map(() => ({trigger: "triggered on account selection"})),
),
);
}

const triggerReset$ = race([
this.actions$.pipe(
unionizeActionFilter(ACCOUNTS_ACTIONS.is.TryToLogin),
filter(({payload: anoterPayload}) => {
return payload.account.accountConfig.login === anoterPayload.account.accountConfig.login;
}),
map(({type: actionType}) => {
return `another "${actionType}" action triggered`;
}),
),
this.store.pipe(
select(AccountsSelectors.ACCOUNTS.pickAccount({login})),
map((storeAccount) => {
if (!storeAccount) {
return;
}
if (storeAccount.notifications.pageType.type !== "login") {
return `page type changed to ${JSON.stringify(storeAccount.notifications.pageType)}`;
}
if (storeAccount.progress.password) {
return `"login" action performing is already in progress`;
}
return;
}),
filter((reason) => {
return typeof reason === "string";
}),
),
]).pipe(
take(1),
tap((reason) => {
logger.info(`resetting delayed "login" action with the following reason: ${reason}`);
}),
);
const trigger$ = delayTriggers.length
? race(delayTriggers).pipe(
take(1), // WARN: just one notification
takeUntil(triggerReset$),
)
: of({trigger: "triggered immediate login (as no delays defined)"});
const executeLoginAction = (password: string) => merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {password: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("login")({login, password, zoneName})),
mergeMap(() => this.store.pipe(
select(AccountsSelectors.FEATURED.selectedLogin),
take(1),
mergeMap((selectedLogin) => {
if (selectedLogin) {
return EMPTY;
}
// let's select the account if none has been selected
return of(ACCOUNTS_ACTIONS.Activate({login}));
}),
)),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {password: false}}))),
),
);

return trigger$.pipe(
mergeMap(({trigger}) => this.store.pipe(
select(AccountsSelectors.ACCOUNTS.pickAccount({login})),
mergeMap((value) => {
if (!value) {
// early skipping if account got removed during login delaying
logger.info("account got removed during login delaying?");
return EMPTY;
}
return [{password: value.accountConfig.credentials.password}];
}),
// WARN: do not react to all the notifications
// but to only one in order to just pick up the to date password
// or multiple login form submitting attempts can happen
take(1),
mergeMap(({password}) => {
logger.info(`login trigger: ${trigger})`);

if (!password) {
logger.info("login action canceled due to the empty password");
return EMPTY;
}

rateLimitingCheck(password);

logger.info("login");

return executeLoginAction(password);
}),
)),
);
}
case "login2fa": {
const secret = payload.password || credentials.twoFactorCode;

// tslint:disable-next-line:early-exit
if (secret) {
rateLimitingCheck(secret);

logger.info("login2fa");
return merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {twoFactorCode: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("login2fa")({secret, zoneName})),
mergeMap(() => EMPTY),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {twoFactorCode: false}}))),
),
);
const {twoFactorCode: secret} = credentials;

if (!secret) {
break;
}

break;
rateLimitingCheck(secret);

logger.info("login2fa");

return merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {twoFactorCode: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("login2fa")({secret, zoneName})),
mergeMap(() => EMPTY),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {twoFactorCode: false}}))),
),
);
}
case "unlock": {
if (type !== "protonmail") {
Expand All @@ -306,25 +413,24 @@ export class AccountsEffects {
);
}

const mailPassword = payload.password || ("mailPassword" in credentials && credentials.mailPassword);

// tslint:disable-next-line:early-exit
if (mailPassword) {
rateLimitingCheck(mailPassword);
const mailPassword = "mailPassword" in credentials && credentials.mailPassword;

return merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {mailPassword: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("unlock")({mailPassword, zoneName})),
mergeMap(() => EMPTY),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {mailPassword: false}}))),
),
);
if (!mailPassword) {
break;
}

break;
rateLimitingCheck(mailPassword);

return merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {mailPassword: true}})),
resetNotificationsState$,
this.api.webViewClient(webView, type).pipe(
mergeMap((webViewClient) => webViewClient("unlock")({mailPassword, zoneName})),
mergeMap(() => EMPTY),
catchError((error) => of(CORE_ACTIONS.Fail(error))),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {mailPassword: false}}))),
),
);
}
}

Expand Down

0 comments on commit 75a7aa0

Please sign in to comment.