Skip to content

Commit

Permalink
Assign emulator ports and resolve hostnames upfront. (#5083)
Browse files Browse the repository at this point in the history
* Assign emulator ports and resolve hostnames upfront.

* Fix test ref.

* Add changelog.

* Restore IPv6 dual stack on wildcard.

* Update hard-coded localhost in tests.
  • Loading branch information
yuchenshi committed Oct 7, 2022
1 parent a7a3e62 commit 6d9a79e
Show file tree
Hide file tree
Showing 25 changed files with 996 additions and 498 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -1 +1,2 @@
- Enable single project mode for the database emulator (#5068).
- Ravamp emulator networking to assign ports early and explictly listen on IP addresses (#5083).
27 changes: 12 additions & 15 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Expand Up @@ -10,13 +10,12 @@ import * as logform from "logform";

import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared";
import { FunctionsEmulator } from "../../src/emulator/functionsEmulator";
import { Emulators } from "../../src/emulator/types";
import { EmulatorInfo, Emulators } from "../../src/emulator/types";
import { FakeEmulator } from "../../src/test/emulators/fakeEmulator";
import { TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures";
import { logger } from "../../src/logger";
import * as registry from "../../src/emulator/registry";
import * as secretManager from "../../src/gcp/secretManager";
import { findAvailablePort } from "../../src/emulator/portUtils";

if ((process.env.DEBUG || "").toLowerCase().includes("spec")) {
const dropLogLevels = (info: logform.TransformableInfo) => info.message;
Expand Down Expand Up @@ -614,22 +613,20 @@ describe("FunctionsEmulator-Hub", function () {
});

describe("environment variables", () => {
const host = "127.0.0.1";
const startFakeEmulator = async (emulator: Emulators): Promise<number> => {
const port = await findAvailablePort(host, 4000);
const fake = new FakeEmulator(emulator, host, port);
const startFakeEmulator = async (emulator: Emulators): Promise<EmulatorInfo> => {
const fake = await FakeEmulator.create(emulator);
await registry.EmulatorRegistry.start(fake);
return port;
return fake.getInfo();
};

afterEach(() => {
return registry.EmulatorRegistry.stopAll();
});

it("should set env vars when the emulator is running", async () => {
const databasePort = await startFakeEmulator(Emulators.DATABASE);
const firestorePort = await startFakeEmulator(Emulators.FIRESTORE);
const authPort = await startFakeEmulator(Emulators.AUTH);
const database = await startFakeEmulator(Emulators.DATABASE);
const firestore = await startFakeEmulator(Emulators.FIRESTORE);
const auth = await startFakeEmulator(Emulators.AUTH);

await useFunction(emu, "functionId", () => {
return {
Expand All @@ -649,14 +646,14 @@ describe("FunctionsEmulator-Hub", function () {
.get("/fake-project-id/us-central1/functionId")
.expect(200)
.then((res) => {
expect(res.body.databaseHost).to.eql(`${host}:${databasePort}`);
expect(res.body.firestoreHost).to.eql(`${host}:${firestorePort}`);
expect(res.body.authHost).to.eql(`${host}:${authPort}`);
expect(res.body.databaseHost).to.eql(`${database.host}:${database.port}`);
expect(res.body.firestoreHost).to.eql(`${firestore.host}:${firestore.port}`);
expect(res.body.authHost).to.eql(`${auth.host}:${auth.port}`);
});
}).timeout(TIMEOUT_MED);

it("should return an emulated databaseURL when RTDB emulator is running", async () => {
const databasePort = await startFakeEmulator(Emulators.DATABASE);
const database = await startFakeEmulator(Emulators.DATABASE);

await useFunction(emu, "functionId", () => {
return {
Expand All @@ -673,7 +670,7 @@ describe("FunctionsEmulator-Hub", function () {
.expect(200)
.then((res) => {
expect(res.body.databaseURL).to.eql(
`http://${host}:${databasePort}/?ns=fake-project-id-default-rtdb`
`http://${database.host}:${database.port}/?ns=fake-project-id-default-rtdb`
);
});
}).timeout(TIMEOUT_MED);
Expand Down
2 changes: 1 addition & 1 deletion scripts/extensions-emulator-tests/tests.ts
Expand Up @@ -51,7 +51,7 @@ describe("CF3 and Extensions emulator", () => {

const config = readConfig();
const storagePort = config.emulators!.storage.port;
process.env.STORAGE_EMULATOR_HOST = `http://localhost:${storagePort}`;
process.env.STORAGE_EMULATOR_HOST = `http://127.0.0.1:${storagePort}`;

const firestorePort = config.emulators!.firestore.port;
process.env.FIRESTORE_EMULATOR_HOST = `localhost:${firestorePort}`;
Expand Down
4 changes: 2 additions & 2 deletions scripts/storage-emulator-integration/utils.ts
Expand Up @@ -26,15 +26,15 @@ export function readEmulatorConfig(config = FIREBASE_EMULATOR_CONFIG): Framework
export function getStorageEmulatorHost(emulatorConfig: FrameworkOptions) {
const port = emulatorConfig.emulators?.storage?.port;
if (port) {
return `http://localhost:${port}`;
return `http://127.0.0.1:${port}`;
}
throw new Error("Storage emulator config not found or invalid");
}

export function getAuthEmulatorHost(emulatorConfig: FrameworkOptions) {
const port = emulatorConfig.emulators?.auth?.port;
if (port) {
return `http://localhost:${port}`;
return `http://127.0.0.1:${port}`;
}
throw new Error("Auth emulator config not found or invalid");
}
Expand Down
127 changes: 127 additions & 0 deletions src/emulator/ExpressBasedEmulator.ts
@@ -0,0 +1,127 @@
import * as cors from "cors";
import * as express from "express";
import * as bodyParser from "body-parser";

import * as utils from "../utils";
import { Emulators, EmulatorInstance, EmulatorInfo, ListenSpec } from "./types";
import { createServer } from "node:http";
import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED } from "./dns";
import { ListenOptions } from "node:net";

export interface ExpressBasedEmulatorOptions {
listen: ListenSpec[];
noCors?: boolean;
noBodyParser?: boolean;
}

/**
* An EmulatorInstance that starts express servers with multi-listen support.
*
* This class correctly destroys the server(s) when `stop()`-ed. When overriding
* life-cycle methods, make sure to call the super methods for those behaviors.
*/
export abstract class ExpressBasedEmulator implements EmulatorInstance {
static PATH_EXPORT = "/_admin/export";
static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers";
static PATH_ENABLE_FUNCTIONS = "/functions/enableBackgroundTriggers";
static PATH_EMULATORS = "/emulators";

private destroyers = new Set<() => Promise<void>>();

constructor(private options: ExpressBasedEmulatorOptions) {}

protected createExpressApp(): Promise<express.Express> {
const app = express();
if (!this.options.noCors) {
// Enable CORS for all APIs, all origins (reflected), and all headers (reflected).
// This is enabled by default since most emulators are cookieless.
app.use(cors({ origin: true }));

// Return access-control-allow-private-network heder if requested
// Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227
// Aligns with https://wicg.github.io/private-network-access/#headers
// Replace with cors option if adopted, see https://github.com/expressjs/cors/issues/236
app.use((req, res, next) => {
if (req.headers["access-control-request-private-network"]) {
res.setHeader("access-control-allow-private-network", "true");
}
next();
});
}
if (!this.options.noBodyParser) {
app.use(bodyParser.json()); // used in most emulators
}
app.set("json spaces", 2);

return Promise.resolve(app);
}

async start(): Promise<void> {
const app = await this.createExpressApp();

const promises = [];
const specs = this.options.listen;

const listenOptions: ListenOptions[] = [];

const dualStackPorts = new Set();
for (const spec of specs) {
if (spec.address === IPV6_UNSPECIFIED.address) {
if (specs.some((s) => s.port === spec.port && s.address === IPV4_UNSPECIFIED.address)) {
// We can use the default dual-stack behavior in Node.js to listen on
// the same port on both IPv4 and IPv6 unspecified addresses on most OSes.
// https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback
listenOptions.push({
port: spec.port,
ipv6Only: false,
});
dualStackPorts.add(spec.port);
}
}
}
// Then add options for non-dual-stack addresses and ports.
for (const spec of specs) {
if (!dualStackPorts.has(spec.port)) {
listenOptions.push({
host: spec.address,
port: spec.port,
ipv6Only: spec.family === "IPv6",
});
}
}

for (const opt of listenOptions) {
promises.push(
new Promise((resolve, reject) => {
const server = createServer(app).listen(opt);
server.once("listening", resolve);
server.once("error", reject);
this.destroyers.add(utils.createDestroyer(server));
})
);
}
}

async connect(): Promise<void> {
// no-op
}

async stop(): Promise<void> {
const promises = [];
for (const destroyer of this.destroyers) {
promises.push(destroyer().then(() => this.destroyers.delete(destroyer)));
}
await Promise.all(promises);
}

getInfo(): EmulatorInfo {
return {
name: this.getName(),
listen: this.options.listen,
host: this.options.listen[0].address,
port: this.options.listen[0].port,
};
}

abstract getName(): Emulators;
}

0 comments on commit 6d9a79e

Please sign in to comment.