Skip to content

Commit

Permalink
additionally save the "proton session" on related cookies change, #227
Browse files Browse the repository at this point in the history
* this should improve preserving by the app actually alive session
  • Loading branch information
vladimiry committed Nov 15, 2020
1 parent b8c33ba commit 1ba4b78
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 45 deletions.
15 changes: 3 additions & 12 deletions src/electron-main/api/endpoints-builders/proton-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@ import {concatMap, first} from "rxjs/operators";
import {from, race, throwError, timer} from "rxjs";
import {pick} from "remeda";

import {AccountPersistentSession} from "src/shared/model/account";
import {Context} from "src/electron-main/model";
import {IpcMainApiEndpoints} from "src/shared/api/main";
import {filterProtonSessionTokenCookies} from "src/electron-main/util";
import {resolveInitializedSession} from "src/electron-main/session";

// TODO enable minimal logging
// const logger = curryFunctionMembers(electronLog, "[electron-main/api/endpoints-builders/proton-session]");

function resolveTokenCookies(
items: DeepReadonly<AccountPersistentSession>["cookies"],
): Readonly<{ accessTokens: typeof items; refreshTokens: typeof items }> {
return {
accessTokens: items.filter(({name}) => name.toUpperCase().startsWith("AUTH-")),
refreshTokens: items.filter(({name}) => name.toUpperCase().startsWith("REFRESH-")),
} as const;
}

function pickTokenCookiePropsToApply(
cookie: DeepReadonly<Electron.Cookie>,
): Pick<typeof cookie, "httpOnly" | "name" | "path" | "secure" | "value"> {
Expand Down Expand Up @@ -69,7 +60,7 @@ export async function buildEndpoints(
},
} as const;

const {accessTokens, refreshTokens} = resolveTokenCookies(data.session.cookies);
const {accessTokens, refreshTokens} = filterProtonSessionTokenCookies(data.session.cookies);

if (accessTokens.length > 1 || refreshTokens.length > 1) {
throw new Error([
Expand Down Expand Up @@ -97,7 +88,7 @@ export async function buildEndpoints(
return false;
}

const tokenCookie = resolveTokenCookies(savedSession.cookies);
const tokenCookie = filterProtonSessionTokenCookies(savedSession.cookies);
const accessTokenCookie = [...tokenCookie.accessTokens].pop();
const refreshTokenCookie = [...tokenCookie.refreshTokens].pop();

Expand Down
41 changes: 38 additions & 3 deletions src/electron-main/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import _logger from "electron-log";
import electronLog from "electron-log";
import {Session, session as electronSession} from "electron";
import {concatMap, first} from "rxjs/operators";
import {from, race, throwError, timer} from "rxjs";
Expand All @@ -10,10 +10,11 @@ import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main";
import {LoginFieldContainer} from "src/shared/model/container";
import {ONE_SECOND_MS, PACKAGE_NAME} from "src/shared/constants";
import {curryFunctionMembers, getRandomInt, getWebViewPartition} from "src/shared/util";
import {filterProtonSessionTokenCookies} from "src/electron-main/util";
import {initWebRequestListenersByAccount} from "src/electron-main/web-request";
import {registerSessionProtocols} from "src/electron-main/protocol";

const logger = curryFunctionMembers(_logger, "[src/electron-main/session]");
const _logger = curryFunctionMembers(electronLog, "[src/electron-main/session]");

// TODO move "usedPartitions" prop to "ctx"
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Expand Down Expand Up @@ -50,6 +51,8 @@ export async function initSession(
session: Session,
{rotateUserAgent}: DeepReadonly<Partial<Pick<AccountConfig, "rotateUserAgent">>> = {},
): Promise<void> {
const logger = curryFunctionMembers(_logger, "initSession()");

if (rotateUserAgent) {
if (!ctx.userAgentsPool || !ctx.userAgentsPool.length) {
const {userAgents} = await ctx.config$.pipe(first()).toPromise();
Expand Down Expand Up @@ -82,7 +85,7 @@ export async function configureSessionByAccount(
ctx: DeepReadonly<Context>,
account: DeepReadonly<AccountConfig>,
): Promise<void> {
logger.info("configureSessionByAccount()");
_logger.info("configureSessionByAccount()");

const {proxy} = account;
const session = resolveInitializedSession({login: account.login});
Expand Down Expand Up @@ -115,6 +118,7 @@ export async function initSessionByAccount(
// eslint-disable-next-line max-len
account: DeepReadonly<AccountConfig>,
): Promise<void> {
const logger = curryFunctionMembers(_logger, "initSessionByAccount()");
const partition = getWebViewPartition(account.login);

if (usedPartitions.has(partition)) {
Expand All @@ -130,6 +134,37 @@ export async function initSessionByAccount(
await registerSessionProtocols(ctx, session);
await configureSessionByAccount(ctx, account);

{
type Cause = "explicit" | "overwrite" | "expired" | "evicted" | "expired-overwrite";

const skipCauses: ReadonlyArray<Cause> = ["expired", "evicted", "expired-overwrite"];

session.cookies.on(
"changed",
// TODO electron/TS: drop explicit callback args typing (currently typed as Function in electron.d.ts)
(...[, cookie, cause, removed]: [
event: unknown,
cookie: Electron.Cookie,
cause: "explicit" | "overwrite" | "expired" | "evicted" | "expired-overwrite",
removed: boolean
]) => {
if (removed || skipCauses.includes(cause)) {
return;
}

const protonSessionTokenCookies = filterProtonSessionTokenCookies([cookie]);

if (protonSessionTokenCookies.accessTokens.length || protonSessionTokenCookies.refreshTokens.length) {
logger.verbose("proton session token cookies modified");

IPC_MAIN_API_NOTIFICATION$.next(
IPC_MAIN_API_NOTIFICATION_ACTIONS.ProtonSessionTokenCookiesModified({key: {login: account.login}}),
);
}
},
);
}

usedPartitions.add(partition);
}

Expand Down
9 changes: 9 additions & 0 deletions src/electron-main/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,12 @@ export function readConfigSync({configStore}: DeepReadonly<Context>): Config | n
? JSON.parse(configFile.toString()) as Config
: null;
}

export const filterProtonSessionTokenCookies = <T extends { name: string }>(
items: readonly T[],
): { readonly accessTokens: typeof items; readonly refreshTokens: typeof items } => {
return {
accessTokens: items.filter(({name}) => name.toUpperCase().startsWith("AUTH-")),
refreshTokens: items.filter(({name}) => name.toUpperCase().startsWith("REFRESH-")),
} as const;
};
1 change: 1 addition & 0 deletions src/shared/api/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const IPC_MAIN_API_NOTIFICATION_ACTIONS = unionize({
InfoMessage: ofType<{ message: string }>(),
TrayIconDataURL: ofType<string>(),
PowerMonitor: ofType<{ message: "suspend" | "resume" | "shutdown" }>(),
ProtonSessionTokenCookiesModified: ofType<{ key: DbModel.DbAccountPk }>(),
},
{
tag: "type",
Expand Down
90 changes: 60 additions & 30 deletions src/web/browser-window/app/_accounts/account.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BehaviorSubject, EMPTY, Observable, Subject, Subscription, combineLatest, race, throwError, timer} from "rxjs";
import {BehaviorSubject, EMPTY, Observable, Subject, Subscription, combineLatest, merge, of, race, throwError, timer} from "rxjs";
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -27,6 +27,7 @@ import {
switchMap,
take,
takeUntil,
tap,
withLatestFrom,
} from "rxjs/operators";

Expand All @@ -43,8 +44,8 @@ import {ONE_SECOND_MS, PRODUCT_NAME} from "src/shared/constants";
import {ProtonClientSession} from "src/shared/model/proton";
import {State} from "src/web/browser-window/app/store/reducers/accounts";
import {WebAccount} from "src/web/browser-window/app/model";
import {curryFunctionMembers, parseUrlOriginWithNullishCheck} from "src/shared/util";
import {getZoneNameBoundWebLogger} from "src/web/browser-window/util";
import {parseUrlOriginWithNullishCheck} from "src/shared/util";

let componentIndex = 0;

Expand Down Expand Up @@ -316,36 +317,65 @@ export class AccountComponent extends NgChangesObservableComponent implements On
return value;
};

this.subscription.add(
this.store.pipe(
select(OptionsSelectors.CONFIG.persistentSessionSavingInterval),
switchMap((persistentSessionSavingInterval) => {
return combineLatest([
this.loggedIn$,
this.persistentSession$,
timer(0, persistentSessionSavingInterval),
]).pipe(
filter(([loggedIn, persistentSession]) => persistentSession && loggedIn),
withLatestFrom(this.account$),
);
}),
).subscribe(([, {accountConfig}]) => {
this.logger.verbose("saving proton session");
{
const logger = curryFunctionMembers(this.logger, "saving proton session");

(async () => {
await this.ipcMainClient("saveProtonSession")({
login: accountConfig.login,
clientSession: await resolveSavedProtonClientSession(),
apiEndpointOrigin: parseUrlOriginWithNullishCheck(
this.core.parseEntryUrl(accountConfig, "proton-mail").entryApiUrl,
),
this.subscription.add(
this.store.pipe(
select(OptionsSelectors.CONFIG.persistentSessionSavingInterval),
switchMap((persistentSessionSavingInterval) => {
return combineLatest([
this.loggedIn$.pipe(
tap((value) => logger.verbose("trigger: loggedIn$", value)),
),
this.persistentSession$.pipe(
tap((value) => logger.verbose("trigger: persistentSession$", value)),
),
merge(
of(null), // fired once to unblock the "combineLatest"
this.store.pipe(
select(OptionsSelectors.FEATURED.mainProcessNotification),
filter(IPC_MAIN_API_NOTIFICATION_ACTIONS.is.ProtonSessionTokenCookiesModified),
debounceTime(ONE_SECOND_MS),
withLatestFrom(this.account$),
filter(([{payload: {key}}, {accountConfig: {login}}]) => key.login === login),
tap(() => logger.verbose("trigger: proton session token cookies modified")),
),
),
(
persistentSessionSavingInterval > 0 // negative value skips the interval-based trigger
? (
timer(0, persistentSessionSavingInterval).pipe(
tap((value) => logger.verbose("trigger: interval", value)),
)
)
: of(null) // fired once to unblock the "combineLatest"
),
]).pipe(
filter(([loggedIn, persistentSession]) => persistentSession && loggedIn),
withLatestFrom(this.account$),
);
}),
).subscribe(([, {accountConfig}]) => {
const ipcMainAction = "saveProtonSession";

logger.verbose(ipcMainAction);

(async () => {
await this.ipcMainClient(ipcMainAction)({
login: accountConfig.login,
clientSession: await resolveSavedProtonClientSession(),
apiEndpointOrigin: parseUrlOriginWithNullishCheck(
this.core.parseEntryUrl(accountConfig, "proton-mail").entryApiUrl,
),
});
})().catch((error) => {
// TODO make "AppErrorHandler.handleError" catch promise rejection errors
this.onDispatchInLoggerZone(NOTIFICATION_ACTIONS.Error(error));
});
})().catch((error) => {
// TODO make "AppErrorHandler.handleError" catch promise rejection errors
this.onDispatchInLoggerZone(NOTIFICATION_ACTIONS.Error(error));
});
}),
);
}),
);
}

this.subscription.add(
combineLatest([
Expand Down

0 comments on commit 1ba4b78

Please sign in to comment.