Skip to content

Commit

Permalink
enable user agent rotating per account opt-in feature, closes #273
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Apr 11, 2020
1 parent 737e00a commit e4d3a28
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 16 deletions.
31 changes: 28 additions & 3 deletions src/electron-main/api/endpoints-builders/account.ts
Expand Up @@ -11,14 +11,27 @@ export async function buildEndpoints(
): Promise<Pick<IpcMainApiEndpoints, "addAccount" | "updateAccount" | "changeAccountOrder" | "removeAccount">> {
return {
async addAccount(
{login, title, entryUrl, database, persistentSession, credentials, proxy, loginDelayUntilSelected, loginDelaySecondsRange},
{

login,
title,
entryUrl,
database,
persistentSession,
rotateUserAgent,
credentials,
proxy,
loginDelayUntilSelected,
loginDelaySecondsRange,
},
) {
const account: AccountConfig = {
login,
title,
entryUrl,
database,
persistentSession,
rotateUserAgent,
credentials,
proxy,
loginDelayUntilSelected,
Expand All @@ -30,13 +43,24 @@ export async function buildEndpoints(

const result = await ctx.settingsStore.write(settings);

await initSessionByAccount(ctx, pick(account, ["login", "proxy"]));
await initSessionByAccount(ctx, pick(account, ["login", "proxy", "rotateUserAgent"]));

return result;
},

async updateAccount(
{login, title, entryUrl, database, persistentSession, credentials, proxy, loginDelayUntilSelected, loginDelaySecondsRange},
{
login,
title,
entryUrl,
database,
persistentSession,
rotateUserAgent,
credentials,
proxy,
loginDelayUntilSelected,
loginDelaySecondsRange,
},
) {
const settings = await ctx.settingsStore.readExisting();
const account = pickAccountStrict(settings.accounts, {login});
Expand All @@ -45,6 +69,7 @@ export async function buildEndpoints(
account.title = title;
account.database = database;
account.persistentSession = persistentSession;
account.rotateUserAgent = rotateUserAgent;

if (typeof entryUrl === "undefined") {
throw new Error('"entryUrl" is undefined');
Expand Down
4 changes: 2 additions & 2 deletions src/electron-main/api/index.ts
Expand Up @@ -203,8 +203,8 @@ export const initApi = async (ctx: Context): Promise<IpcMainApiEndpoints> => {

ctx.settingsStore = store;

for (const {login, proxy} of settings.accounts) {
await initSessionByAccount(ctx, {login, proxy});
for (const {login, proxy, rotateUserAgent} of settings.accounts) {
await initSessionByAccount(ctx, {login, proxy, rotateUserAgent});
}

await (async (): Promise<void> => {
Expand Down
1 change: 1 addition & 0 deletions src/electron-main/model.ts
Expand Up @@ -45,6 +45,7 @@ export interface Context {
databaseView?: boolean;
};
getSpellCheckController: () => SpellCheckController;
userAgentsPool?: Config["userAgents"];
}

export interface UIContext {
Expand Down
2 changes: 1 addition & 1 deletion src/electron-main/protocol.ts
Expand Up @@ -84,7 +84,7 @@ async function resolveFileSystemResourceLocation(
throw new Error(`Failed to resolve "${resource}" file system resource`);
}

export async function registerSessionProtocols(ctx: Context, session: Session): Promise<void> {
export async function registerSessionProtocols(ctx: DeepReadonly<Context>, session: Session): Promise<void> {
// TODO setup timeout on "ready" even firing
await app.whenReady();

Expand Down
45 changes: 37 additions & 8 deletions src/electron-main/session.ts
@@ -1,18 +1,25 @@
import _logger from "electron-log";
import {Session, session as electronSession} from "electron";
import {concatMap} from "rxjs/operators";
import {concatMap, take} from "rxjs/operators";
import {from, race, throwError, timer} from "rxjs";

import {AccountConfig} from "src/shared/model/account";
import {Context} from "./model";
import {IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
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 {getWebViewPartition} from "src/shared/util";
import {curryFunctionMembers, getRandomInt, getWebViewPartition} from "src/shared/util";
import {initWebRequestListeners} from "src/electron-main/web-request";
import {registerSessionProtocols} from "src/electron-main/protocol";

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

// TODO move "usedPartitions" prop to "ctx"
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const usedPartitions: Set<Parameters<typeof initSessionByAccount>[1]["login"]> = new Set();

// TODO move "usedSessions" prop to "ctx"
// TODO remove the session from map on account removing
const usedSessions: Map<string, Session> = new Map();

Expand All @@ -39,16 +46,39 @@ function purifyUserAgentHeader(session: Session): void {
}

export async function initSession(
ctx: Context,
ctx: DeepReadonly<StrictOmit<Context, "userAgentsPool">> & Pick<Context, "userAgentsPool">,
session: Session,
{rotateUserAgent}: DeepReadonly<Partial<Pick<AccountConfig, "rotateUserAgent">>> = {},
): Promise<void> {
if (rotateUserAgent) {
if (!ctx.userAgentsPool || !ctx.userAgentsPool.length) {
const {userAgents} = await ctx.config$
.pipe(take(1))
.toPromise();
ctx.userAgentsPool = [...userAgents];
}
const {userAgentsPool} = ctx;
if (userAgentsPool.length) {
const idx = getRandomInt(0, userAgentsPool.length - 1);
const userAgent = userAgentsPool[idx];
logger.info("picked user agent to set", JSON.stringify({idx, userAgent, userAgentsPoolSize: userAgentsPool.length}));
userAgentsPool.splice(idx, 1); // removing used value from the pool
session.setUserAgent(userAgent);
} else {
const message = `Can't rotate the "session.userAgent" since user agents pool is empty`;
IPC_MAIN_API_NOTIFICATION$.next(
IPC_MAIN_API_NOTIFICATION_ACTIONS.InfoMessage({message}),
);
logger.warn(message);
}
}
purifyUserAgentHeader(session);
await registerSessionProtocols(ctx, session);
initWebRequestListeners(ctx, session);
}

export async function configureSessionByAccount(
account: Pick<AccountConfig, "login" | "proxy">,
account: DeepReadonly<Pick<AccountConfig, "login" | "proxy">>,
): Promise<void> {
const {proxy} = account;
const session = resolveInitialisedSession({login: account.login});
Expand All @@ -75,8 +105,8 @@ export async function configureSessionByAccount(
}

export async function initSessionByAccount(
ctx: Context,
account: Pick<AccountConfig, "login" | "proxy">,
ctx: DeepReadonly<StrictOmit<Context, "userAgentsPool">> & Pick<Context, "userAgentsPool">,
account: DeepReadonly<Pick<AccountConfig, "login" | "proxy" | "rotateUserAgent">>,
): Promise<void> {
const partition = getWebViewPartition(account.login);

Expand All @@ -89,7 +119,7 @@ export async function initSessionByAccount(

usedSessions.set(partition, session);

await initSession(ctx, session);
await initSession(ctx, session, {rotateUserAgent: account.rotateUserAgent});
await configureSessionByAccount(account);

usedPartitions.add(partition);
Expand All @@ -104,4 +134,3 @@ export function getDefaultSession(): Session {

return defaultSession;
}

8 changes: 8 additions & 0 deletions src/electron-main/storage-upgrade.ts
Expand Up @@ -325,6 +325,14 @@ const CONFIG_UPGRADES: Record<string, (config: Config) => void> = {
: INITIAL_STORES.config()[key];
})();
},
"4.5.0": (config) => {
(() => {
const key: keyof Pick<Config, "userAgents"> = "userAgents";
if (!Array.isArray(config[key])) {
config[key] = INITIAL_STORES.config()[key];
}
})();
},
// WARN needs to be the last updater
"100.0.0": (config) => {
// ensuring default base props are set
Expand Down
2 changes: 1 addition & 1 deletion src/electron-main/web-request.ts
Expand Up @@ -224,7 +224,7 @@ const patchReponseHeaders: (arg: { requestProxy: RequestProxy; details: Response
};

// TODO pass additional "account type" argument and apply only respective listeners
export function initWebRequestListeners(ctx: Context, session: Session): void {
export function initWebRequestListeners(ctx: DeepReadonly<Context>, session: Session): void {
const webClientsOrigins = ctx.locations.webClients
.map(({entryUrl}) => new URL(entryUrl).origin);

Expand Down
1 change: 1 addition & 0 deletions src/shared/model/account.ts
Expand Up @@ -14,6 +14,7 @@ export type AccountConfig = NoExtraProperties<{
loginDelayUntilSelected?: boolean;
loginDelaySecondsRange?: NoExtraProperties<{ start: number; end: number }>;
persistentSession?: boolean;
rotateUserAgent?: boolean;
}>;

export type AccountPersistentSession = NoExtraProperties<{
Expand Down
1 change: 1 addition & 0 deletions src/shared/model/container.ts
Expand Up @@ -28,5 +28,6 @@ export type AccountConfigCreateUpdatePatch = NoExtraProperties<Pick<AccountConfi
| "loginDelaySecondsRange"
| "loginDelayUntilSelected"
| "persistentSession"
| "rotateUserAgent"
| "proxy"
| "title">>;
1 change: 1 addition & 0 deletions src/shared/model/options.ts
Expand Up @@ -38,6 +38,7 @@ export interface Config extends BaseConfig, Partial<StoreModel.StoreEntity> {
indexingBootstrapBufferSize: number;
jsFlags: string[];
localDbMailsListViewMode: "plain" | "conversation";
userAgents: string[];
// base
checkUpdateAndNotify: boolean;
doNotRenderNotificationBadgeValue: boolean;
Expand Down
12 changes: 12 additions & 0 deletions src/shared/util.ts
Expand Up @@ -60,6 +60,18 @@ export function initialConfig(): Config {
"--max-old-space-size=3072",
],
localDbMailsListViewMode: "plain",
userAgents: [
/* eslint-disable max-len */
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 YaBrowser/18.3.1.1232 Yowser/2.5 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 OPR/56.0.3051.52",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 Edg/80.0.361.69",
"Mozilla/5.0 (X11; CrOS x86_64 12739.111.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4102.3 Safari/537.36",
"Mozilla/5.0 CK={} (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
/* eslint-enable max-len */
],
// base
doNotRenderNotificationBadgeValue: false,
checkUpdateAndNotify: false,
Expand Down
16 changes: 16 additions & 0 deletions src/web/browser-window/app/_options/account-edit.component.html
Expand Up @@ -122,6 +122,22 @@
</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" formControlName="rotateUserAgent" id="rotateUserAgentCheckbox">
<label class="custom-control-label d-flex" for="rotateUserAgentCheckbox">
User-agent rotation
<i
class="fa fa-info-circle text-warning align-self-center ml-1"
container="body"
[placement]="'bottom'"
popover="App restart required for the option to take effect."
triggers="mouseenter:mouseleave"
></i>
<a class="ml-1" href="https://github.com/vladimiry/ElectronMail/issues/273">#273</a>
</label>
</div>
</div>
<div class="form-group">
<label class="d-block">
Handle button alias
Expand Down
12 changes: 11 additions & 1 deletion src/web/browser-window/app/_options/account-edit.component.ts
Expand Up @@ -23,14 +23,22 @@ import {validateLoginDelaySecondsRange} from "src/shared/util";
export class AccountEditComponent implements OnInit, OnDestroy {
entryUrlItems = [...PROTON_API_ENTRY_RECORDS];
controls: Record<keyof Pick<AccountConfig,
| "login" | "title" | "database" | "persistentSession" | "entryUrl" | "loginDelayUntilSelected" | "loginDelaySecondsRange">
| "login"
| "title"
| "database"
| "persistentSession"
| "rotateUserAgent"
| "entryUrl"
| "loginDelayUntilSelected"
| "loginDelaySecondsRange">
| keyof Pick<Required<Required<AccountConfig>["proxy"]>, "proxyRules" | "proxyBypassRules">
| keyof AccountConfig["credentials"],
AbstractControl> = {
login: new FormControl(null, Validators.required),
title: new FormControl(null),
database: new FormControl(null),
persistentSession: new FormControl(null),
rotateUserAgent: new FormControl(null),
entryUrl: new FormControl(null, Validators.required),
proxyRules: new FormControl(null),
proxyBypassRules: new FormControl(null),
Expand Down Expand Up @@ -105,6 +113,7 @@ export class AccountEditComponent implements OnInit, OnDestroy {
controls.title.patchValue(account.title);
controls.database.patchValue(account.database);
controls.persistentSession.patchValue(account.persistentSession);
controls.rotateUserAgent.patchValue(account.rotateUserAgent);
controls.entryUrl.patchValue(account.entryUrl);

controls.proxyRules.patchValue(account.proxy ? account.proxy.proxyRules : null);
Expand Down Expand Up @@ -139,6 +148,7 @@ export class AccountEditComponent implements OnInit, OnDestroy {
entryUrl: controls.entryUrl.value,
database: Boolean(controls.database.value),
persistentSession: Boolean(controls.persistentSession.value),
rotateUserAgent: Boolean(controls.rotateUserAgent.value),
credentials: {
password: controls.password.value,
twoFactorCode: controls.twoFactorCode.value,
Expand Down

0 comments on commit e4d3a28

Please sign in to comment.