Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
/test/puppeteer.json
/test/node_modules/
/test/build/*
/test/samples/oversize.txt
/node_modules/
/build/*
release/log.txt
release/*
npm-debug.log
npm-debug.log
6 changes: 5 additions & 1 deletion .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ blocks:
- npm install
- echo "NODE=$(node --version), NPM=$(npm --version), TSC=$( ./node_modules/typescript/bin/tsc --version)"
- npm run-script pretest
- sudo sh -c "echo '209.250.232.81 cron.flowcrypt.com' >> /etc/hosts" && sudo sh -c "echo '127.0.0.1 google.mock.flowcryptlocal.com' >> /etc/hosts"
- sudo sh -c "echo '209.250.232.81 cron.flowcrypt.com' >> /etc/hosts"
- sudo sh -c "echo '127.0.0.1 google.mock.flowcryptlocal.com' >> /etc/hosts"
- sudo sh -c "echo '127.0.0.1 fes.standardsubdomainfes.com' >> /etc/hosts"
- sudo sh -c "echo '127.0.0.1 standardsubdomainfes.com' >> /etc/hosts"
- sudo sh -c "echo '127.0.0.1 wellknownfes.com' >> /etc/hosts"

jobs:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ApiErr } from '../../../js/common/api/shared/api-error.js';
import { ViewModule } from '../../../js/common/view-module.js';
import { ComposeView } from '../compose.js';
import { AcctStore } from '../../../js/common/platform/store/acct-store.js';
import { AccountServer } from '../../../js/common/api/account-server.js';

export class ComposePwdOrPubkeyContainerModule extends ViewModule<ComposeView> {

Expand Down Expand Up @@ -91,7 +90,7 @@ export class ComposePwdOrPubkeyContainerModule extends ViewModule<ComposeView> {
expirationTextEl.text(Str.pluralize(this.MSG_EXPIRE_DAYS_DEFAULT, 'day'));
} else {
try {
const response = await AccountServer.accountGetAndUpdateLocalStore(authInfo);
const response = await this.view.acctServer.accountGetAndUpdateLocalStore(authInfo);
expirationTextEl.text(Str.pluralize(response.account.default_message_expire, 'day'));
} catch (e) {
ApiErr.reportIfSignificant(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { ContactStore } from '../../../js/common/platform/store/contact-store.js
import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js';

import { Settings } from '../../../js/common/settings.js';
import { AccountServer } from '../../../js/common/api/account-server.js';

export class ComposeStorageModule extends ViewModule<ComposeView> {

Expand Down Expand Up @@ -211,7 +210,7 @@ export class ComposeStorageModule extends ViewModule<ComposeView> {
const auth = await AcctStore.authInfo(this.view.acctEmail);
if (auth.uuid) {
try {
await AccountServer.accountGetAndUpdateLocalStore(auth); // updates storage
await this.view.acctServer.accountGetAndUpdateLocalStore(auth); // updates storage
} catch (e) {
if (ApiErr.isAuthErr(e)) {
Settings.offerToLoginWithPopupShowModalOnErr(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { Ui } from '../../../../js/common/browser/ui.js';
import { Xss } from '../../../../js/common/platform/xss.js';
import { AcctStore } from '../../../../js/common/platform/store/acct-store.js';
import { FlowCryptWebsite } from '../../../../js/common/api/flowcrypt-website.js';
import { AccountServer } from '../../../../js/common/api/account-server.js';
import { FcUuidAuth } from '../../../../js/common/api/account-servers/flowcrypt-com-api.js';
import { SmimeKey } from '../../../../js/common/core/crypto/smime/smime-key.js';

Expand All @@ -45,7 +44,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
const msgBodyWithReplyToken = await this.getPwdMsgSendableBodyWithOnlineReplyMsgToken(authInfo, newMsg);
const pgpMimeWithAtts = await Mime.encode(msgBodyWithReplyToken, { Subject: newMsg.subject }, await this.view.attsModule.attach.collectAtts());
const { data: pwdEncryptedWithAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed
const { short, admin_code } = await AccountServer.messageUpload(
const { short, admin_code } = await this.view.acctServer.messageUpload(
authInfo.uuid ? authInfo : undefined,
pwdEncryptedWithAtts,
(p) => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF'), // still need to upload to Gmail later, this request represents first half of progress
Expand Down Expand Up @@ -110,7 +109,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
}
const recipients = Array.prototype.concat.apply([], Object.values(newMsgData.recipients));
try {
const response = await AccountServer.messageToken(authInfo);
const response = await this.view.acctServer.messageToken(authInfo);
const infoDiv = Ui.e('div', {
'style': 'display: none;',
'class': 'cryptup_reply',
Expand Down
3 changes: 3 additions & 0 deletions extension/chrome/elements/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { Catch } from '../../js/common/platform/catch.js';
import { OrgRules } from '../../js/common/org-rules.js';
import { PubLookup } from '../../js/common/api/pub-lookup.js';
import { Scopes, AcctStore } from '../../js/common/platform/store/acct-store.js';
import { AccountServer } from '../../js/common/api/account-server.js';

export class ComposeView extends View {

Expand All @@ -54,6 +55,7 @@ export class ComposeView extends View {
public emailProvider: EmailProviderInterface;
public orgRules!: OrgRules;
public pubLookup!: PubLookup;
public acctServer: AccountServer;

public quoteModule!: ComposeQuoteModule;
public sendBtnModule!: ComposeSendBtnModule;
Expand Down Expand Up @@ -136,6 +138,7 @@ export class ComposeView extends View {
this.replyMsgId = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'replyMsgId') || '';
this.isReplyBox = !!this.replyMsgId;
this.emailProvider = new Gmail(this.acctEmail);
this.acctServer = new AccountServer(this.acctEmail);
opgp.initWorker({ path: '/lib/openpgp.worker.js' });
}

Expand Down
4 changes: 3 additions & 1 deletion extension/chrome/elements/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ View.run(class SubscribeView extends View {
private readonly placement: string | undefined;
private authInfo: FcUuidAuth | undefined;
private tabId: string | undefined;
private acctServer: AccountServer;

private readonly PRODUCTS: { [productName in ProductName]: Product } = {
null: { id: null, method: null, name: null, level: null }, // tslint:disable-line:no-null-keyword
Expand All @@ -42,6 +43,7 @@ View.run(class SubscribeView extends View {
this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
this.placement = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'placement', ['settings', 'settings_compose', 'default', 'dialog', 'gmail', 'compose', undefined]);
this.acctServer = new AccountServer(this.acctEmail);
}

public render = async () => {
Expand Down Expand Up @@ -74,7 +76,7 @@ View.run(class SubscribeView extends View {
private renderSubscriptionDetails = async () => {
this.authInfo = await AcctStore.authInfo(this.acctEmail);
try {
await AccountServer.accountGetAndUpdateLocalStore(this.authInfo);
await this.acctServer.accountGetAndUpdateLocalStore(this.authInfo);
} catch (e) {
if (ApiErr.isAuthErr(e)) {
Xss.sanitizeRender('#content', `Not logged in. ${Ui.retryLink()}`);
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/index.htm
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ <h1 class="text-center">FlowCrypt Settings</h1>
<span id="status_google" class="link" title="Your Google Account">g:?</span>
<span id="status_flowcrypt" title="Your FlowCrypt Account">fc:?</span>
<span id="status_subscription" title="Your FlowCrypt Subscription">s:?</span>
<span id="status_local_store" class="link" title="Local Extension Storage">local_store</span>
<span id="status_local_store" class="link" title="Local Extension Storage" data-test="action-show-local-store-contents">local_store</span>
</div>
</section>

Expand Down
4 changes: 3 additions & 1 deletion extension/chrome/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ View.run(class SettingsView extends View {
private tabId!: string;
private notifications!: Notifications;
private orgRules: OrgRules | undefined;
private acctServer: AccountServer | undefined;

private altAccounts: JQuery<HTMLElement>;

Expand All @@ -56,6 +57,7 @@ View.run(class SettingsView extends View {
if (this.acctEmail) {
this.acctEmail = this.acctEmail.toLowerCase().trim();
this.gmail = new Gmail(this.acctEmail);
this.acctServer = new AccountServer(this.acctEmail);
}
this.altAccounts = $('#alt-accounts');
}
Expand Down Expand Up @@ -310,7 +312,7 @@ View.run(class SettingsView extends View {
const authInfo = await AcctStore.authInfo(this.acctEmail!);
if (authInfo.uuid) { // have auth email set
try {
const response = await AccountServer.accountGetAndUpdateLocalStore(authInfo);
const response = await this.acctServer!.accountGetAndUpdateLocalStore(authInfo);
$('#status-row #status_flowcrypt').text(`fc:ok`);
if (response?.account?.alias) {
statusContainer.find('.status-indicator-text').css('display', 'none');
Expand Down
4 changes: 2 additions & 2 deletions extension/chrome/settings/modules/debug_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ View.run(class DebugApiView extends View {
'notification_setup_needed_dismissed', 'email_provider', 'google_token_scopes', 'hide_message_password', 'sendAs', 'outgoing_language',
'full_name', 'cryptup_enabled', 'setup_done',
'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'openid',
'rules', 'subscription', 'use_rich_text',
'rules', 'subscription', 'use_rich_text', 'fesUrl'
]);
this.renderCallRes('Local account storage', { acctEmail: this.acctEmail }, storage);
} else {
Expand All @@ -54,7 +54,7 @@ View.run(class DebugApiView extends View {
}

private renderCallRes = (api: string, variables: Dict<any>, result: any, error?: any) => {
const r = `<b>${api} ${JSON.stringify(variables)}</b><pre>${JSON.stringify(result, undefined, 2)} (${error ? JSON.stringify(error) : 'no err'})</pre>`;
const r = `<b>${api} ${JSON.stringify(variables)}</b><pre data-test="container-pre">${JSON.stringify(result, undefined, 2)} (${error ? JSON.stringify(error) : 'no err'})</pre>`;
Xss.sanitizeAppend('#content', r);
}

Expand Down
6 changes: 4 additions & 2 deletions extension/chrome/settings/modules/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ View.run(class SecurityView extends View {
private primaryKi: KeyInfo | undefined;
private authInfo: FcUuidAuth | undefined;
private orgRules!: OrgRules;
private acctServer: AccountServer;

constructor() {
super();
const uncheckedUrlParams = Url.parse(['acctEmail', 'parentTabId']);
this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
this.acctServer = new AccountServer(this.acctEmail);
}

public render = async () => {
Expand Down Expand Up @@ -89,7 +91,7 @@ View.run(class SecurityView extends View {
if (subscription.active) {
Xss.sanitizeRender('.select_loader_container', Ui.spinner('green'));
try {
const response = await AccountServer.accountGetAndUpdateLocalStore(this.authInfo!);
const response = await this.acctServer.accountGetAndUpdateLocalStore(this.authInfo!);
$('.select_loader_container').text('');
$('.default_message_expire').val(Number(response.account.default_message_expire).toString()).prop('disabled', false).css('display', 'inline-block');
$('.default_message_expire').change(this.setHandler(() => this.onDefaultExpireUserChange()));
Expand All @@ -113,7 +115,7 @@ View.run(class SecurityView extends View {
private onDefaultExpireUserChange = async () => {
Xss.sanitizeRender('.select_loader_container', Ui.spinner('green'));
$('.default_message_expire').css('display', 'none');
await AccountServer.accountUpdate(this.authInfo!, { default_message_expire: Number($('.default_message_expire').val()) });
await this.acctServer.accountUpdate(this.authInfo!, { default_message_expire: Number($('.default_message_expire').val()) });
window.location.reload();
}

Expand Down
50 changes: 37 additions & 13 deletions extension/js/common/api/account-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,67 @@

'use strict';

import { EnterpriseServer } from './account-servers/enterprise-server.js';
import { BackendRes, FcUuidAuth, FlowCryptComApi, ProfileUpdate } from './account-servers/flowcrypt-com-api.js';
import { WellKnownHostMeta } from './account-servers/well-known-host-meta.js';
import { Api, ProgressCb } from './shared/api.js';


/**
* This may be calling to FlowCryptComApi or Enterprise Server (FES, customer on-prem) depending on
* domain configuration fetched using WellKnownHostMeta.
*
* Current implementation only calls FlowCryptComApi, FES integration is planned
*/
export class AccountServer extends Api {

public static loginWithOpenid = async (acctEmail: string, uuid: string, idToken: string): Promise<void> => {
return await FlowCryptComApi.loginWithOpenid(acctEmail, uuid, idToken);
private wellKnownHostMeta: WellKnownHostMeta;

constructor(private acctEmail: string) {
super();
this.wellKnownHostMeta = new WellKnownHostMeta(acctEmail);
}

public loginWithOpenid = async (acctEmail: string, uuid: string, idToken: string): Promise<void> => {
const fesUrl = await this.wellKnownHostMeta.getFesUrlFromCache();
if (fesUrl) {
const fes = new EnterpriseServer(fesUrl, this.acctEmail);
await fes.getAccessTokenAndUpdateLocalStore(idToken);
} else {
await FlowCryptComApi.loginWithOpenid(acctEmail, uuid, idToken);
}
}

public static accountUpdate = async (fcAuth: FcUuidAuth, profileUpdate: ProfileUpdate): Promise<BackendRes.FcAccountUpdate> => {
return await FlowCryptComApi.accountUpdate(fcAuth, profileUpdate);
public accountGetAndUpdateLocalStore = async (fcAuth: FcUuidAuth): Promise<BackendRes.FcAccountGet> => {
const fesUrl = await this.wellKnownHostMeta.getFesUrlFromCache();
if (fesUrl) {
const fes = new EnterpriseServer(fesUrl, this.acctEmail);
return await fes.getAccountAndUpdateLocalStore();
} else {
return await FlowCryptComApi.accountGetAndUpdateLocalStore(fcAuth);
}
}

public static accountGetAndUpdateLocalStore = async (fcAuth: FcUuidAuth): Promise<BackendRes.FcAccountGet> => {
return await FlowCryptComApi.accountGetAndUpdateLocalStore(fcAuth);
public accountUpdate = async (fcAuth: FcUuidAuth, profileUpdate: ProfileUpdate): Promise<void> => {
const fesUrl = await this.wellKnownHostMeta.getFesUrlFromCache();
if (fesUrl) {
const fes = new EnterpriseServer(fesUrl, this.acctEmail);
await fes.accountUpdate(profileUpdate);
} else {
await FlowCryptComApi.accountUpdate(fcAuth, profileUpdate);
}
}

public static messageUpload = async (fcAuth: FcUuidAuth | undefined, encryptedDataBinary: Uint8Array, progressCb: ProgressCb): Promise<BackendRes.FcMsgUpload> => {
public messageUpload = async (fcAuth: FcUuidAuth | undefined, encryptedDataBinary: Uint8Array, progressCb: ProgressCb): Promise<BackendRes.FcMsgUpload> => {
return await FlowCryptComApi.messageUpload(fcAuth, encryptedDataBinary, progressCb);
}

public static messageToken = async (fcAuth: FcUuidAuth): Promise<BackendRes.FcMsgToken> => {
public messageToken = async (fcAuth: FcUuidAuth): Promise<BackendRes.FcMsgToken> => {
return await FlowCryptComApi.messageToken(fcAuth);
}

public static messageExpiration = async (fcAuth: FcUuidAuth, adminCodes: string[], addDays?: number): Promise<BackendRes.ApirFcMsgExpiration> => {
public messageExpiration = async (fcAuth: FcUuidAuth, adminCodes: string[], addDays?: number): Promise<BackendRes.ApirFcMsgExpiration> => {
return await FlowCryptComApi.messageExpiration(fcAuth, adminCodes, addDays);
}

public static linkMessage = async (short: string): Promise<BackendRes.FcLinkMsg> => {
public linkMessage = async (short: string): Promise<BackendRes.FcLinkMsg> => {
return await FlowCryptComApi.linkMessage(short);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { ErrorReport, UnreportableError } from '../../platform/catch.js';
// todo - decide which tags to use
type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'import-prv';

namespace FesRes {
export namespace FesRes {
export type AccessToken = { accessToken: string };
export type ServiceInfo = { vendor: string, service: string, orgId: string, version: string, apiVersion: string }
}

/**
Expand All @@ -36,6 +37,10 @@ export class EnterpriseServer extends Api {
this.fesUrl = fesUrl.replace(/\/$/, '');
}

public getServiceInfo = async (): Promise<FesRes.ServiceInfo> => {
return await this.request<FesRes.ServiceInfo>('GET', `/api/`);
}

public getAccessTokenAndUpdateLocalStore = async (idToken: string): Promise<void> => {
const response = await this.request<FesRes.AccessToken>('GET', `/api/${this.apiVersion}/account/access-token`, { Authorization: `Bearer ${idToken}` });
await AcctStore.set(this.acctEmail, { fesAccessToken: response.accessToken });
Expand Down Expand Up @@ -66,7 +71,7 @@ export class EnterpriseServer extends Api {
}

private request = async <RT>(method: ReqMethod, path: string, headers: Dict<string> = {}, vals?: Dict<any>): Promise<RT> => {
return await FlowCryptComApi.apiCall(this.fesUrl, path, vals, 'JSON', undefined, headers, 'json', method);
return await FlowCryptComApi.apiCall(this.fesUrl, path, vals, method === 'GET' ? undefined : 'JSON', undefined, headers, 'json', method);
}

}
37 changes: 30 additions & 7 deletions extension/js/common/api/account-servers/well-known-host-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Catch } from '../../platform/catch.js';
import { AcctStore } from '../../platform/store/acct-store.js';
import { Api } from '../shared/api.js';
import { ApiErr } from '../shared/api-error.js';
import { FesRes, EnterpriseServer } from './enterprise-server.js';

type HostMetaResponse = { links?: { rel?: string, href?: string }[] }

Expand All @@ -17,6 +18,7 @@ export class WellKnownHostMeta extends Api {
private domain: string;
private hostMetaUrl: string;
private fesRel = 'https://flowcrypt.com/fes';
private laxCheck = ['dmFsZW8uY29t'];

constructor(private acctEmail: string) {
super();
Expand All @@ -32,21 +34,42 @@ export class WellKnownHostMeta extends Api {
return undefined;
}
const responseBuf = await this.attemptToFetchFesUrlIgnoringErrorsOnConsumerFlavor();
if (!responseBuf) {
await this.setFesUrlToCache(undefined);
return undefined;
if (responseBuf) {
const hostMetaResponse = this.parseBufAsHostMetaResponseIgnoringErrorsOnConsumerFlavor(responseBuf);
const fesUrl = hostMetaResponse?.links?.find(link => link.rel === this.fesRel)?.href;
await this.setFesUrlToCache(fesUrl);
return fesUrl;
}
const hostMetaResponse = this.parseBufAsHostMetaResponseIgnoringErrorsOnConsumerFlavor(responseBuf);
const fesUrl = hostMetaResponse?.links?.find(link => link.rel === this.fesRel)?.href;
await this.setFesUrlToCache(fesUrl);
return fesUrl;
const standardFesUrl = `https://fes.${this.domain}`;
const fesServiceInfo = await this.tryCallingFesDirectlyIgnoringErrorsOnConsumerFlavor(standardFesUrl);
if (fesServiceInfo && fesServiceInfo.service === 'enterprise-server') {
await this.setFesUrlToCache(standardFesUrl);
return standardFesUrl;
}
await this.setFesUrlToCache(undefined);
return undefined;
}

public getFesUrlFromCache = async (): Promise<string | undefined> => {
const { fesUrl } = await AcctStore.get(this.acctEmail, ['fesUrl']);
return fesUrl;
}

private tryCallingFesDirectlyIgnoringErrorsOnConsumerFlavor = async (fesUrl: string): Promise<FesRes.ServiceInfo | undefined> => {
const fes = new EnterpriseServer(fesUrl, this.acctEmail);
try {
return await fes.getServiceInfo();
} catch (e) {
if (FLAVOR === 'consumer' || ApiErr.isNotFound(e)) {
return;
} else if (this.laxCheck.includes(btoa(this.domain)) && ApiErr.isNetErr(e)) {
return undefined; // cannot reach server for enterprises where we don't expect to find it
} else {
throw e; // strict enterprise
}
}
}

private setFesUrlToCache = async (fesUrl: string | undefined): Promise<void> => {
if (fesUrl) {
await AcctStore.set(this.acctEmail, { fesUrl });
Expand Down
Loading