Skip to content

Commit 171b78b

Browse files
authored
Handle IPv6 addresses in emulator autoinit. (#6673)
* Handle IPv6 addresses in emulator autoinit. * Create shy-dragons-fly.md * Fix typo. * Fix more typos. * Add API review changes. * Fix tests.
1 parent 1fbc4c4 commit 171b78b

File tree

9 files changed

+201
-27
lines changed

9 files changed

+201
-27
lines changed

.changeset/shy-dragons-fly.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@firebase/database": patch
3+
"@firebase/firestore": patch
4+
"@firebase/functions": patch
5+
"@firebase/storage": patch
6+
"@firebase/util": patch
7+
---
8+
9+
Handle IPv6 addresses in emulator autoinit.

common/api-review/util.api.md

+3
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ export const getDefaultAppConfig: () => Record<string, string> | undefined;
218218
// @public
219219
export const getDefaultEmulatorHost: (productName: string) => string | undefined;
220220

221+
// @public
222+
export const getDefaultEmulatorHostnameAndPort: (productName: string) => [hostname: string, port: number] | undefined;
223+
221224
// @public
222225
export const getExperimentalSetting: <T extends ExperimentalKey>(name: T) => FirebaseDefaults[`_${T}`];
223226

packages/database/src/api/Database.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
getModularInstance,
2929
createMockUserToken,
3030
EmulatorMockTokenOptions,
31-
getDefaultEmulatorHost
31+
getDefaultEmulatorHostnameAndPort
3232
} from '@firebase/util';
3333

3434
import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider';
@@ -320,10 +320,9 @@ export function getDatabase(
320320
const db = _getProvider(app, 'database').getImmediate({
321321
identifier: url
322322
}) as Database;
323-
const databaseEmulatorHost = getDefaultEmulatorHost('database');
324-
if (databaseEmulatorHost) {
325-
const [host, port] = databaseEmulatorHost.split(':');
326-
connectDatabaseEmulator(db, host, parseInt(port, 10));
323+
const emulator = getDefaultEmulatorHostnameAndPort('database');
324+
if (emulator) {
325+
connectDatabaseEmulator(db, ...emulator);
327326
}
328327
return db;
329328
}

packages/firestore/src/api/database.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
FirebaseApp,
2222
getApp
2323
} from '@firebase/app';
24-
import { deepEqual, getDefaultEmulatorHost } from '@firebase/util';
24+
import { deepEqual, getDefaultEmulatorHostnameAndPort } from '@firebase/util';
2525

2626
import { User } from '../auth/user';
2727
import {
@@ -242,10 +242,9 @@ export function getFirestore(
242242
identifier: databaseId
243243
}) as Firestore;
244244
if (!db._initialized) {
245-
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
246-
if (firestoreEmulatorHost) {
247-
const [host, port] = firestoreEmulatorHost.split(':');
248-
connectFirestoreEmulator(db, host, parseInt(port, 10));
245+
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
246+
if (emulator) {
247+
connectFirestoreEmulator(db, ...emulator);
249248
}
250249
}
251250
return db;

packages/firestore/src/lite-api/database.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import {
2626
createMockUserToken,
2727
EmulatorMockTokenOptions,
28-
getDefaultEmulatorHost
28+
getDefaultEmulatorHostnameAndPort
2929
} from '@firebase/util';
3030

3131
import {
@@ -270,10 +270,9 @@ export function getFirestore(
270270
identifier: databaseId
271271
}) as Firestore;
272272
if (!db._initialized) {
273-
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
274-
if (firestoreEmulatorHost) {
275-
const [host, port] = firestoreEmulatorHost.split(':');
276-
connectFirestoreEmulator(db, host, parseInt(port, 10));
273+
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
274+
if (emulator) {
275+
connectFirestoreEmulator(db, ...emulator);
277276
}
278277
}
279278
return db;

packages/functions/src/api.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ import {
2727
httpsCallable as _httpsCallable,
2828
httpsCallableFromURL as _httpsCallableFromURL
2929
} from './service';
30-
import { getModularInstance, getDefaultEmulatorHost } from '@firebase/util';
30+
import {
31+
getModularInstance,
32+
getDefaultEmulatorHostnameAndPort
33+
} from '@firebase/util';
3134

3235
export * from './public-types';
3336

@@ -51,11 +54,9 @@ export function getFunctions(
5154
const functionsInstance = functionsProvider.getImmediate({
5255
identifier: regionOrCustomDomain
5356
});
54-
const functionsEmulatorHost = getDefaultEmulatorHost('functions');
55-
if (functionsEmulatorHost) {
56-
const [host, port] = functionsEmulatorHost.split(':');
57-
// eslint-disable-next-line no-restricted-globals
58-
connectFunctionsEmulator(functionsInstance, host, parseInt(port, 10));
57+
const emulator = getDefaultEmulatorHostnameAndPort('functions');
58+
if (emulator) {
59+
connectFunctionsEmulator(functionsInstance, ...emulator);
5960
}
6061
return functionsInstance;
6162
}

packages/storage/src/api.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { STORAGE_TYPE } from './constants';
5353
import {
5454
EmulatorMockTokenOptions,
5555
getModularInstance,
56-
getDefaultEmulatorHost
56+
getDefaultEmulatorHostnameAndPort
5757
} from '@firebase/util';
5858
import { StringFormat } from './implementation/string';
5959

@@ -334,11 +334,9 @@ export function getStorage(
334334
const storageInstance = storageProvider.getImmediate({
335335
identifier: bucketUrl
336336
});
337-
const storageEmulatorHost = getDefaultEmulatorHost('storage');
338-
if (storageEmulatorHost) {
339-
const [host, port] = storageEmulatorHost.split(':');
340-
// eslint-disable-next-line no-restricted-globals
341-
connectStorageEmulator(storageInstance, host, parseInt(port, 10));
337+
const emulator = getDefaultEmulatorHostnameAndPort('storage');
338+
if (emulator) {
339+
connectStorageEmulator(storageInstance, ...emulator);
342340
}
343341
return storageInstance;
344342
}

packages/util/src/defaults.ts

+28
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,40 @@ const getDefaults = (): FirebaseDefaults | undefined =>
8989
/**
9090
* Returns emulator host stored in the __FIREBASE_DEFAULTS__ object
9191
* for the given product.
92+
* @returns a URL host formatted like `127.0.0.1:9999` or `[::1]:4000` if available
9293
* @public
9394
*/
9495
export const getDefaultEmulatorHost = (
9596
productName: string
9697
): string | undefined => getDefaults()?.emulatorHosts?.[productName];
9798

99+
/**
100+
* Returns emulator hostname and port stored in the __FIREBASE_DEFAULTS__ object
101+
* for the given product.
102+
* @returns a pair of hostname and port like `["::1", 4000]` if available
103+
* @public
104+
*/
105+
export const getDefaultEmulatorHostnameAndPort = (
106+
productName: string
107+
): [hostname: string, port: number] | undefined => {
108+
const host = getDefaultEmulatorHost(productName);
109+
if (!host) {
110+
return undefined;
111+
}
112+
const separatorIndex = host.lastIndexOf(':'); // Finding the last since IPv6 addr also has colons.
113+
if (separatorIndex <= 0 || separatorIndex + 1 === host.length) {
114+
throw new Error(`Invalid host ${host} with no separate hostname and port!`);
115+
}
116+
// eslint-disable-next-line no-restricted-globals
117+
const port = parseInt(host.substring(separatorIndex + 1), 10);
118+
if (host[0] === '[') {
119+
// Bracket-quoted `[ipv6addr]:port` => return "ipv6addr" (without brackets).
120+
return [host.substring(1, separatorIndex - 1), port];
121+
} else {
122+
return [host.substring(0, separatorIndex), port];
123+
}
124+
};
125+
98126
/**
99127
* Returns Firebase app config stored in the __FIREBASE_DEFAULTS__ object.
100128
* @public

packages/util/test/defaults.test.ts

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* @license
3+
* Copyright 2017 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { expect } from 'chai';
18+
import {
19+
getDefaultEmulatorHost,
20+
getDefaultEmulatorHostnameAndPort
21+
} from '../src/defaults';
22+
import { getGlobal } from '../src/environment';
23+
24+
describe('getDefaultEmulatorHost', () => {
25+
after(() => {
26+
delete getGlobal().__FIREBASE_DEFAULTS__;
27+
});
28+
29+
context('with no config', () => {
30+
it('returns undefined', () => {
31+
expect(getDefaultEmulatorHost('firestore')).to.be.undefined;
32+
});
33+
});
34+
35+
context('with global config not listing the emulator', () => {
36+
before(() => {
37+
getGlobal().__FIREBASE_DEFAULTS__ = {
38+
emulatorHosts: {
39+
/* no firestore */
40+
database: '127.0.0.1:8080'
41+
}
42+
};
43+
});
44+
45+
it('returns undefined', () => {
46+
expect(getDefaultEmulatorHost('firestore')).to.be.undefined;
47+
});
48+
});
49+
50+
context('with IPv4 hostname in global config', () => {
51+
before(() => {
52+
getGlobal().__FIREBASE_DEFAULTS__ = {
53+
emulatorHosts: {
54+
firestore: '127.0.0.1:8080'
55+
}
56+
};
57+
});
58+
59+
it('returns host', () => {
60+
expect(getDefaultEmulatorHost('firestore')).to.equal('127.0.0.1:8080');
61+
});
62+
});
63+
64+
context('with quoted IPv6 hostname in global config', () => {
65+
before(() => {
66+
getGlobal().__FIREBASE_DEFAULTS__ = {
67+
emulatorHosts: {
68+
firestore: '[::1]:8080'
69+
}
70+
};
71+
});
72+
73+
it('returns host', () => {
74+
expect(getDefaultEmulatorHost('firestore')).to.equal('[::1]:8080');
75+
});
76+
});
77+
});
78+
79+
describe('getDefaultEmulatorHostnameAndPort', () => {
80+
after(() => {
81+
delete getGlobal().__FIREBASE_DEFAULTS__;
82+
});
83+
84+
context('with no config', () => {
85+
it('returns undefined', () => {
86+
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.be.undefined;
87+
});
88+
});
89+
90+
context('with global config not listing the emulator', () => {
91+
before(() => {
92+
getGlobal().__FIREBASE_DEFAULTS__ = {
93+
emulatorHosts: {
94+
/* no firestore */
95+
database: '127.0.0.1:8080'
96+
}
97+
};
98+
});
99+
100+
it('returns undefined', () => {
101+
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.be.undefined;
102+
});
103+
});
104+
105+
context('with IPv4 hostname in global config', () => {
106+
before(() => {
107+
getGlobal().__FIREBASE_DEFAULTS__ = {
108+
emulatorHosts: {
109+
firestore: '127.0.0.1:8080'
110+
}
111+
};
112+
});
113+
114+
it('returns hostname and port splitted', () => {
115+
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.eql([
116+
'127.0.0.1',
117+
8080
118+
]);
119+
});
120+
});
121+
122+
context('with quoted IPv6 hostname in global config', () => {
123+
before(() => {
124+
getGlobal().__FIREBASE_DEFAULTS__ = {
125+
emulatorHosts: {
126+
firestore: '[::1]:8080'
127+
}
128+
};
129+
});
130+
131+
it('returns unquoted hostname and port splitted', () => {
132+
expect(getDefaultEmulatorHostnameAndPort('firestore')).to.eql([
133+
'::1',
134+
8080
135+
]);
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)