Skip to content

Commit

Permalink
Handle IPv6 addresses in emulator autoinit. (#6673)
Browse files Browse the repository at this point in the history
* Handle IPv6 addresses in emulator autoinit.

* Create shy-dragons-fly.md

* Fix typo.

* Fix more typos.

* Add API review changes.

* Fix tests.
  • Loading branch information
yuchenshi committed Oct 10, 2022
1 parent 1fbc4c4 commit 171b78b
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 27 deletions.
9 changes: 9 additions & 0 deletions .changeset/shy-dragons-fly.md
@@ -0,0 +1,9 @@
---
"@firebase/database": patch
"@firebase/firestore": patch
"@firebase/functions": patch
"@firebase/storage": patch
"@firebase/util": patch
---

Handle IPv6 addresses in emulator autoinit.
3 changes: 3 additions & 0 deletions common/api-review/util.api.md
Expand Up @@ -218,6 +218,9 @@ export const getDefaultAppConfig: () => Record<string, string> | undefined;
// @public
export const getDefaultEmulatorHost: (productName: string) => string | undefined;

// @public
export const getDefaultEmulatorHostnameAndPort: (productName: string) => [hostname: string, port: number] | undefined;

// @public
export const getExperimentalSetting: <T extends ExperimentalKey>(name: T) => FirebaseDefaults[`_${T}`];

Expand Down
9 changes: 4 additions & 5 deletions packages/database/src/api/Database.ts
Expand Up @@ -28,7 +28,7 @@ import {
getModularInstance,
createMockUserToken,
EmulatorMockTokenOptions,
getDefaultEmulatorHost
getDefaultEmulatorHostnameAndPort
} from '@firebase/util';

import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider';
Expand Down Expand Up @@ -320,10 +320,9 @@ export function getDatabase(
const db = _getProvider(app, 'database').getImmediate({
identifier: url
}) as Database;
const databaseEmulatorHost = getDefaultEmulatorHost('database');
if (databaseEmulatorHost) {
const [host, port] = databaseEmulatorHost.split(':');
connectDatabaseEmulator(db, host, parseInt(port, 10));
const emulator = getDefaultEmulatorHostnameAndPort('database');
if (emulator) {
connectDatabaseEmulator(db, ...emulator);
}
return db;
}
Expand Down
9 changes: 4 additions & 5 deletions packages/firestore/src/api/database.ts
Expand Up @@ -21,7 +21,7 @@ import {
FirebaseApp,
getApp
} from '@firebase/app';
import { deepEqual, getDefaultEmulatorHost } from '@firebase/util';
import { deepEqual, getDefaultEmulatorHostnameAndPort } from '@firebase/util';

import { User } from '../auth/user';
import {
Expand Down Expand Up @@ -242,10 +242,9 @@ export function getFirestore(
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
if (firestoreEmulatorHost) {
const [host, port] = firestoreEmulatorHost.split(':');
connectFirestoreEmulator(db, host, parseInt(port, 10));
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
if (emulator) {
connectFirestoreEmulator(db, ...emulator);
}
}
return db;
Expand Down
9 changes: 4 additions & 5 deletions packages/firestore/src/lite-api/database.ts
Expand Up @@ -25,7 +25,7 @@ import {
import {
createMockUserToken,
EmulatorMockTokenOptions,
getDefaultEmulatorHost
getDefaultEmulatorHostnameAndPort
} from '@firebase/util';

import {
Expand Down Expand Up @@ -270,10 +270,9 @@ export function getFirestore(
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
if (firestoreEmulatorHost) {
const [host, port] = firestoreEmulatorHost.split(':');
connectFirestoreEmulator(db, host, parseInt(port, 10));
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
if (emulator) {
connectFirestoreEmulator(db, ...emulator);
}
}
return db;
Expand Down
13 changes: 7 additions & 6 deletions packages/functions/src/api.ts
Expand Up @@ -27,7 +27,10 @@ import {
httpsCallable as _httpsCallable,
httpsCallableFromURL as _httpsCallableFromURL
} from './service';
import { getModularInstance, getDefaultEmulatorHost } from '@firebase/util';
import {
getModularInstance,
getDefaultEmulatorHostnameAndPort
} from '@firebase/util';

export * from './public-types';

Expand All @@ -51,11 +54,9 @@ export function getFunctions(
const functionsInstance = functionsProvider.getImmediate({
identifier: regionOrCustomDomain
});
const functionsEmulatorHost = getDefaultEmulatorHost('functions');
if (functionsEmulatorHost) {
const [host, port] = functionsEmulatorHost.split(':');
// eslint-disable-next-line no-restricted-globals
connectFunctionsEmulator(functionsInstance, host, parseInt(port, 10));
const emulator = getDefaultEmulatorHostnameAndPort('functions');
if (emulator) {
connectFunctionsEmulator(functionsInstance, ...emulator);
}
return functionsInstance;
}
Expand Down
10 changes: 4 additions & 6 deletions packages/storage/src/api.ts
Expand Up @@ -53,7 +53,7 @@ import { STORAGE_TYPE } from './constants';
import {
EmulatorMockTokenOptions,
getModularInstance,
getDefaultEmulatorHost
getDefaultEmulatorHostnameAndPort
} from '@firebase/util';
import { StringFormat } from './implementation/string';

Expand Down Expand Up @@ -334,11 +334,9 @@ export function getStorage(
const storageInstance = storageProvider.getImmediate({
identifier: bucketUrl
});
const storageEmulatorHost = getDefaultEmulatorHost('storage');
if (storageEmulatorHost) {
const [host, port] = storageEmulatorHost.split(':');
// eslint-disable-next-line no-restricted-globals
connectStorageEmulator(storageInstance, host, parseInt(port, 10));
const emulator = getDefaultEmulatorHostnameAndPort('storage');
if (emulator) {
connectStorageEmulator(storageInstance, ...emulator);
}
return storageInstance;
}
Expand Down
28 changes: 28 additions & 0 deletions packages/util/src/defaults.ts
Expand Up @@ -89,12 +89,40 @@ const getDefaults = (): FirebaseDefaults | undefined =>
/**
* Returns emulator host stored in the __FIREBASE_DEFAULTS__ object
* for the given product.
* @returns a URL host formatted like `127.0.0.1:9999` or `[::1]:4000` if available
* @public
*/
export const getDefaultEmulatorHost = (
productName: string
): string | undefined => getDefaults()?.emulatorHosts?.[productName];

/**
* Returns emulator hostname and port stored in the __FIREBASE_DEFAULTS__ object
* for the given product.
* @returns a pair of hostname and port like `["::1", 4000]` if available
* @public
*/
export const getDefaultEmulatorHostnameAndPort = (
productName: string
): [hostname: string, port: number] | undefined => {
const host = getDefaultEmulatorHost(productName);
if (!host) {
return undefined;
}
const separatorIndex = host.lastIndexOf(':'); // Finding the last since IPv6 addr also has colons.
if (separatorIndex <= 0 || separatorIndex + 1 === host.length) {
throw new Error(`Invalid host ${host} with no separate hostname and port!`);
}
// eslint-disable-next-line no-restricted-globals
const port = parseInt(host.substring(separatorIndex + 1), 10);
if (host[0] === '[') {
// Bracket-quoted `[ipv6addr]:port` => return "ipv6addr" (without brackets).
return [host.substring(1, separatorIndex - 1), port];
} else {
return [host.substring(0, separatorIndex), port];
}
};

/**
* Returns Firebase app config stored in the __FIREBASE_DEFAULTS__ object.
* @public
Expand Down
138 changes: 138 additions & 0 deletions packages/util/test/defaults.test.ts
@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import {
getDefaultEmulatorHost,
getDefaultEmulatorHostnameAndPort
} from '../src/defaults';
import { getGlobal } from '../src/environment';

describe('getDefaultEmulatorHost', () => {
after(() => {
delete getGlobal().__FIREBASE_DEFAULTS__;
});

context('with no config', () => {
it('returns undefined', () => {
expect(getDefaultEmulatorHost('firestore')).to.be.undefined;
});
});

context('with global config not listing the emulator', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
/* no firestore */
database: '127.0.0.1:8080'
}
};
});

it('returns undefined', () => {
expect(getDefaultEmulatorHost('firestore')).to.be.undefined;
});
});

context('with IPv4 hostname in global config', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '127.0.0.1:8080'
}
};
});

it('returns host', () => {
expect(getDefaultEmulatorHost('firestore')).to.equal('127.0.0.1:8080');
});
});

context('with quoted IPv6 hostname in global config', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '[::1]:8080'
}
};
});

it('returns host', () => {
expect(getDefaultEmulatorHost('firestore')).to.equal('[::1]:8080');
});
});
});

describe('getDefaultEmulatorHostnameAndPort', () => {
after(() => {
delete getGlobal().__FIREBASE_DEFAULTS__;
});

context('with no config', () => {
it('returns undefined', () => {
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.be.undefined;
});
});

context('with global config not listing the emulator', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
/* no firestore */
database: '127.0.0.1:8080'
}
};
});

it('returns undefined', () => {
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.be.undefined;
});
});

context('with IPv4 hostname in global config', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '127.0.0.1:8080'
}
};
});

it('returns hostname and port splitted', () => {
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.eql([
'127.0.0.1',
8080
]);
});
});

context('with quoted IPv6 hostname in global config', () => {
before(() => {
getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '[::1]:8080'
}
};
});

it('returns unquoted hostname and port splitted', () => {
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.eql([
'::1',
8080
]);
});
});
});

0 comments on commit 171b78b

Please sign in to comment.