Skip to content

Use the FID from the server if client FID generation fails #1925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 1, 2019
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
2 changes: 1 addition & 1 deletion packages/firebase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@firebase/database": "0.4.6",
"@firebase/firestore": "1.4.2",
"@firebase/functions": "0.4.10",
"@firebase/installations": "0.1.7",
"@firebase/installations": "0.2.0",
"@firebase/messaging": "0.4.3",
"@firebase/polyfill": "0.3.14",
"@firebase/storage": "0.3.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/installations/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firebase/installations",
"version": "0.1.7",
"version": "0.2.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"esm2017": "dist/index.esm2017.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/installations/src/api/create-installation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ describe('createInstallation', () => {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
expiresIn: '604800s'
}
},
fid: FID
};
fetchSpy = stub(self, 'fetch');
});
Expand Down
2 changes: 1 addition & 1 deletion packages/installations/src/api/create-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function createInstallation(
if (response.ok) {
const responseValue: CreateInstallationResponse = await response.json();
const registeredInstallationEntry: RegisteredInstallationEntry = {
fid,
fid: responseValue.fid,
registrationStatus: RequestStatus.COMPLETED,
refreshToken: responseValue.refreshToken,
authToken: extractAuthTokenInfoFromResponse(responseValue.authToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,4 @@ describe('bufferToBase64', () => {
BASE_64_REPRESENTATION
);
});

it('returns a base64 representation of an ArrayBuffer', () => {
expect(bufferToBase64UrlSafe(TYPED_ARRAY_REPRESENTATION.buffer)).to.equal(
BASE_64_REPRESENTATION
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
* limitations under the License.
*/

export function bufferToBase64UrlSafe(
buffer: ArrayBuffer | Uint8Array
): string {
const array = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
export function bufferToBase64UrlSafe(array: Uint8Array): string {
const b64 = btoa(String.fromCharCode(...array));
return b64.replace(/\+/g, '-').replace(/\//g, '_');
}
16 changes: 12 additions & 4 deletions packages/installations/src/helpers/generate-fid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import '../testing/setup';
import { generateFid } from './generate-fid';
import { generateFid, VALID_FID_PATTERN } from './generate-fid';

/** A few random values to generate a FID from. */
// prettier-ignore
Expand Down Expand Up @@ -53,8 +53,6 @@ const EXPECTED_FIDS = [
'f_____________________'
];

const VALID_FID = /^[cdef][A-Za-z0-9_-]{21}$/;

describe('generateFid', () => {
it('deterministically generates FIDs based on crypto.getRandomValues', () => {
let randomValueIndex = 0;
Expand All @@ -77,7 +75,10 @@ describe('generateFid', () => {
it('generates valid FIDs', () => {
for (let i = 0; i < 1000; i++) {
const fid = generateFid();
expect(VALID_FID.test(fid)).to.equal(true, `${fid} is not a valid FID`);
expect(VALID_FID_PATTERN.test(fid)).to.equal(
true,
`${fid} is not a valid FID`
);
}
});

Expand Down Expand Up @@ -117,4 +118,11 @@ describe('generateFid', () => {
});
}
}).timeout(30000);

it('returns an empty string if FID generation fails', () => {
stub(crypto, 'getRandomValues').throws();

const fid = generateFid();
expect(fid).to.equal('');
});
});
31 changes: 23 additions & 8 deletions packages/installations/src/helpers/generate-fid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,32 @@

import { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe';

/** Generates a new FID using random values from Web Crypto API. */
export const VALID_FID_PATTERN = /^[cdef][\w-]{21}$/;
export const INVALID_FID = '';

/**
* Generates a new FID using random values from Web Crypto API.
* Returns an empty string if FID generation fails for any reason.
*/
export function generateFid(): string {
// A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5
// bytes. our implementation generates a 17 byte array instead.
const fidByteArray = new Uint8Array(17);
crypto.getRandomValues(fidByteArray);
try {
// A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5
// bytes. our implementation generates a 17 byte array instead.
const fidByteArray = new Uint8Array(17);
const crypto =
self.crypto || ((self as unknown) as { msCrypto: Crypto }).msCrypto;
crypto.getRandomValues(fidByteArray);

// Replace the first 4 random bits with the constant FID header of 0b0111.
fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000);

// Replace the first 4 random bits with the constant FID header of 0b0111.
fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000);
const fid = encode(fidByteArray);

return encode(fidByteArray);
return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID;
} catch {
// FID generation errored
return INVALID_FID;
}
}

/** Converts a FID Uint8Array to a base64 string representation. */
Expand Down
31 changes: 26 additions & 5 deletions packages/installations/src/helpers/get-installation-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { getFakeAppConfig } from '../testing/get-fake-app';
import '../testing/setup';
import { ERROR_FACTORY, ErrorCode } from '../util/errors';
import { sleep } from '../util/sleep';
import * as fidGenerator from './generate-fid';
import * as generateFidModule from './generate-fid';
import { getInstallationEntry } from './get-installation-entry';
import { get, set } from './idb-manager';

Expand All @@ -53,7 +53,8 @@ describe('getInstallationEntry', () => {
async (_, installationEntry): Promise<RegisteredInstallationEntry> => {
await sleep(100); // Request would take some time
const registeredInstallationEntry: RegisteredInstallationEntry = {
fid: installationEntry.fid,
// Returns new FID if client FID is invalid.
fid: installationEntry.fid || FID,
registrationStatus: RequestStatus.COMPLETED,
refreshToken: 'refreshToken',
authToken: {
Expand Down Expand Up @@ -179,9 +180,10 @@ describe('getInstallationEntry', () => {
let generateInstallationEntrySpy: SinonStub<[], string>;

beforeEach(() => {
generateInstallationEntrySpy = stub(fidGenerator, 'generateFid').returns(
FID
);
generateInstallationEntrySpy = stub(
generateFidModule,
'generateFid'
).returns(FID);
});

it('returns a new pending InstallationEntry and triggers createInstallation', async () => {
Expand Down Expand Up @@ -255,6 +257,25 @@ describe('getInstallationEntry', () => {

expect(createInstallationSpy).to.be.calledOnce;
});

it('waits for the FID from the server if FID generation fails', async () => {
clock.restore();
// Needed to allow the createInstallation request to complete.
clock = useFakeTimers({ shouldAdvanceTime: true });

// FID generation fails.
generateInstallationEntrySpy.returns(generateFidModule.INVALID_FID);

const getInstallationEntryPromise = getInstallationEntry(appConfig);

const {
installationEntry,
registrationPromise
} = await getInstallationEntryPromise;

expect(installationEntry.fid).to.equal(FID);
expect(registrationPromise).to.be.undefined;
});
});

describe('when there is an unregistered InstallationEntry in the database', () => {
Expand Down
52 changes: 32 additions & 20 deletions packages/installations/src/helpers/get-installation-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ import { AppConfig } from '../interfaces/app-config';
import {
InProgressInstallationEntry,
InstallationEntry,
RequestStatus
RequestStatus,
RegisteredInstallationEntry
} from '../interfaces/installation-entry';
import { PENDING_TIMEOUT_MS } from '../util/constants';
import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors';
import { sleep } from '../util/sleep';
import { generateFid } from './generate-fid';
import { generateFid, INVALID_FID } from './generate-fid';
import { remove, set, update } from './idb-manager';

export interface InstallationEntryWithRegistrationPromise {
installationEntry: InstallationEntry;
registrationPromise?: Promise<void>;
registrationPromise?: Promise<RegisteredInstallationEntry>;
}

/**
Expand All @@ -40,26 +41,33 @@ export interface InstallationEntryWithRegistrationPromise {
export async function getInstallationEntry(
appConfig: AppConfig
): Promise<InstallationEntryWithRegistrationPromise> {
let registrationPromise: Promise<void> | undefined;
let registrationPromise: Promise<RegisteredInstallationEntry> | undefined;

const installationEntry = await update(
appConfig,
(oldEntry?: InstallationEntry): InstallationEntry => {
const installationEntry = updateOrCreateInstallationEntry(oldEntry);
const entryWithPromise = triggerRegistrationIfNecessary(
appConfig,
installationEntry
);
registrationPromise = entryWithPromise.registrationPromise;
return entryWithPromise.installationEntry;
}
);

if (installationEntry.fid === INVALID_FID) {
// FID generation failed. Waiting for the FID from the server.
return { installationEntry: await registrationPromise! };
}

return {
installationEntry: await update(
appConfig,
(oldEntry?: InstallationEntry): InstallationEntry => {
const installationEntry = updateOrCreateFid(oldEntry);
const entryWithPromise = triggerRegistrationIfNecessary(
appConfig,
installationEntry
);
registrationPromise = entryWithPromise.registrationPromise;
return entryWithPromise.installationEntry;
}
),
installationEntry,
registrationPromise
};
}

function updateOrCreateFid(
function updateOrCreateInstallationEntry(
oldEntry: InstallationEntry | undefined
): InstallationEntry {
const entry: InstallationEntry = oldEntry || {
Expand Down Expand Up @@ -124,13 +132,13 @@ function triggerRegistrationIfNecessary(
async function registerInstallation(
appConfig: AppConfig,
installationEntry: InProgressInstallationEntry
): Promise<void> {
): Promise<RegisteredInstallationEntry> {
try {
const registeredInstallationEntry = await createInstallation(
appConfig,
installationEntry
);
await set(appConfig, registeredInstallationEntry);
return set(appConfig, registeredInstallationEntry);
} catch (e) {
if (isServerError(e) && e.serverCode === 409) {
// Server returned a "FID can not be used" error.
Expand All @@ -148,7 +156,9 @@ async function registerInstallation(
}

/** Call if FID registration is pending. */
async function waitUntilFidRegistration(appConfig: AppConfig): Promise<void> {
async function waitUntilFidRegistration(
appConfig: AppConfig
): Promise<RegisteredInstallationEntry> {
// Unfortunately, there is no way of reliably observing when a value in
// IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers),
// so we need to poll.
Expand All @@ -164,6 +174,8 @@ async function waitUntilFidRegistration(appConfig: AppConfig): Promise<void> {
if (entry.registrationStatus === RequestStatus.NOT_STARTED) {
throw ERROR_FACTORY.create(ErrorCode.CREATE_INSTALLATION_FAILED);
}

return entry;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/installations/src/interfaces/api-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
export interface CreateInstallationResponse {
readonly refreshToken: string;
readonly authToken: GenerateAuthTokenResponse;
readonly fid: string;
}

export interface GenerateAuthTokenResponse {
Expand Down
2 changes: 1 addition & 1 deletion packages/performance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"dependencies": {
"@firebase/logger": "0.1.17",
"@firebase/installations": "0.1.7",
"@firebase/installations": "0.2.0",
"@firebase/util": "0.2.20",
"@firebase/performance-types": "0.0.2",
"tslib": "1.9.3"
Expand Down