diff --git a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts index 1ca2ad0535b..615fcbbcd44 100644 --- a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts +++ b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts @@ -398,36 +398,6 @@ describe("FunctionsEmulator-Runtime", function () { }); }); }); - describe("_InitializeFunctionsConfigHelper()", () => { - const cfgPath = path.join(FUNCTIONS_DIR, ".runtimeconfig.json"); - - before(async () => { - await fs.writeFile(cfgPath, '{"real":{"exist":"already exists" }}'); - }); - - after(async () => { - await fs.unlink(cfgPath); - }); - - it("should tell the user if they've accessed a non-existent function field", async () => { - runtime = await startRuntime("functionId", "event", () => { - require("firebase-admin").initializeApp(); - return { - functionId: require("firebase-functions") - .firestore.document("test/test") - .onCreate(() => { - // Exists - console.log(require("firebase-functions").config().real); - // Does not exist - console.log(require("firebase-functions").config().foo); - console.log(require("firebase-functions").config().bar); - }), - }; - }); - await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); - expect(runtime.sysMsg["functions-config-missing-value"]?.length).to.eq(2); - }); - }); describe("Runtime", () => { describe("HTTPS", () => { it("should handle a GET request", async () => { diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 88055cb30f3..be5d5288f27 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -3,7 +3,6 @@ import * as path from "path"; import * as express from "express"; import * as clc from "colorette"; import * as http from "http"; -import * as jwt from "jsonwebtoken"; import * as cors from "cors"; import * as semver from "semver"; import { URL } from "url"; @@ -28,7 +27,6 @@ import { FunctionsRuntimeFeatures, getFunctionService, getSignatureType, - HttpConstants, ParsedTriggerDefinition, emulatedFunctionsFromEndpoints, emulatedFunctionsByRegion, @@ -1732,41 +1730,6 @@ export class FunctionsEmulator implements EmulatorInstance { return EmulatorRegistry.getInfo(emulator); } - private tokenFromAuthHeader(authHeader: string) { - const match = /^Bearer (.*)$/.exec(authHeader); - if (!match) { - return; - } - - let idToken = match[1]; - logger.debug(`ID Token: ${idToken}`); - - // The @firebase/testing library sometimes produces JWTs with invalid padding, so we - // remove that via regex. This is the spec that says trailing = should be removed: - // https://tools.ietf.org/html/rfc7515#section-2 - if (idToken && idToken.includes("=")) { - idToken = idToken.replace(/[=]+?\./g, "."); - logger.debug(`ID Token contained invalid padding, new value: ${idToken}`); - } - - try { - const decoded = jwt.decode(idToken, { complete: true }) as any; - if (!decoded || typeof decoded !== "object") { - logger.debug(`Failed to decode ID Token: ${decoded}`); - return; - } - - // In firebase-functions we manually copy 'sub' to 'uid' - // https://github.com/firebase/firebase-admin-node/blob/0b2082f1576f651e75069e38ce87e639c25289af/src/auth/token-verifier.ts#L249 - const claims = decoded.payload as jwt.JwtPayload; - claims.uid = claims.sub; - - return claims; - } catch (e: any) { - return; - } - } - private async handleHttpsTrigger(req: express.Request, res: express.Response) { const method = req.method; let triggerId: string = req.params.trigger_name; @@ -1804,27 +1767,6 @@ export class FunctionsEmulator implements EmulatorInstance { } } - // For callable functions we want to accept tokens without actually calling verifyIdToken - const isCallable = trigger.labels && trigger.labels["deployment-callable"] === "true"; - const authHeader = req.header("Authorization"); - if (authHeader && isCallable && trigger.platform !== "gcfv2") { - const token = this.tokenFromAuthHeader(authHeader); - if (token) { - const contextAuth = { - uid: token.uid, - token: token, - }; - - // Stash the "Authorization" header in a temporary place, we will replace it - // when invoking the callable handler - req.headers[HttpConstants.ORIGINAL_AUTH_HEADER] = req.headers["authorization"]; - delete req.headers["authorization"]; - - req.headers[HttpConstants.CALLABLE_AUTH_HEADER] = encodeURIComponent( - JSON.stringify(contextAuth), - ); - } - } // For analytics, track the invoked service void trackEmulator(EVENT_INVOKE_GA4, { function_service: getFunctionService(trigger), diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index 8720c3b08ab..b05acd40560 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -1,6 +1,4 @@ -import * as fs from "fs"; - -import { CloudFunction, DeploymentOptions, https } from "firebase-functions"; +import { CloudFunction, DeploymentOptions } from "firebase-functions"; import * as express from "express"; import * as path from "path"; import * as admin from "firebase-admin"; @@ -10,12 +8,7 @@ import * as _ from "lodash"; import { EmulatorLog } from "./types"; import { Constants } from "./constants"; -import { - findModuleRoot, - FunctionsRuntimeBundle, - HttpConstants, - SignatureType, -} from "./functionsEmulatorShared"; +import { findModuleRoot, FunctionsRuntimeBundle, SignatureType } from "./functionsEmulatorShared"; import { compareVersionStrings, isLocalHost } from "./functionsEmulatorUtils"; import { EventUtils } from "./events/types"; @@ -252,7 +245,7 @@ async function assertResolveDeveloperNodeModule(name: string): Promise { const modBundles = [ { name: "firebase-admin", isDev: false, minVersion: "8.9.0" }, - { name: "firebase-functions", isDev: false, minVersion: "3.13.1" }, + { name: "firebase-functions", isDev: false, minVersion: "3.16.0" }, ]; for (const modBundle of modBundles) { @@ -383,7 +376,6 @@ function initializeNetworkFiltering(): void { logDebug("Outgoing network have been stubbed.", results); } -type CallableHandler = (data: any, context: https.CallableContext) => any | Promise; type HttpsHandler = (req: Request, resp: Response) => void; /* @@ -431,116 +423,12 @@ async function initializeFirebaseFunctionsStubs(): Promise { httpsProvider.onRequest = (handler: HttpsHandler) => { return httpsProvider[onRequestInnerMethodName](handler, {}); }; - - // Mocking https.onCall is very similar to onRequest - const onCallInnerMethodName = "_onCallWithOptions"; - const onCallMethodOriginal = httpsProvider[onCallInnerMethodName]; - - // Newer versions of the firebase-functions package's _onCallWithOptions method expects 3 arguments. - if (onCallMethodOriginal.length === 3) { - httpsProvider[onCallInnerMethodName] = ( - opts: any, - handler: any, - deployOpts: DeploymentOptions, - ) => { - const wrapped = wrapCallableHandler(handler); - const cf = onCallMethodOriginal(opts, wrapped, deployOpts); - return cf; - }; - } else { - httpsProvider[onCallInnerMethodName] = (handler: any, opts: DeploymentOptions) => { - const wrapped = wrapCallableHandler(handler); - const cf = onCallMethodOriginal(wrapped, opts); - return cf; - }; - } - - // Newer versions of the firebase-functions package's onCall method can accept upto 2 arguments. - httpsProvider.onCall = function (optsOrHandler: any, handler: CallableHandler) { - if (onCallMethodOriginal.length === 3) { - let opts; - if (arguments.length === 1) { - opts = {}; - handler = optsOrHandler as CallableHandler; - } else { - opts = optsOrHandler; - } - return httpsProvider[onCallInnerMethodName](opts, handler, {}); - } else { - return httpsProvider[onCallInnerMethodName](optsOrHandler, {}); - } - }; -} - -/** - * Wrap a callable functions handler with an outer method that extracts a special authorization - * header used to mock auth in the emulator. - */ -function wrapCallableHandler(handler: CallableHandler): CallableHandler { - const newHandler = (data: any, context: https.CallableContext) => { - if (context.rawRequest) { - const authContext = context.rawRequest.header(HttpConstants.CALLABLE_AUTH_HEADER); - if (authContext) { - logDebug("Callable functions auth override", { - key: HttpConstants.CALLABLE_AUTH_HEADER, - value: authContext, - }); - context.auth = JSON.parse(decodeURIComponent(authContext)); - delete context.rawRequest.headers[HttpConstants.CALLABLE_AUTH_HEADER]; - } else { - logDebug("No callable functions auth found"); - } - - // Restore the original auth header in case the code relies on parsing it (for - // example, the code could forward it to another function or server). - const originalAuth = context.rawRequest.header(HttpConstants.ORIGINAL_AUTH_HEADER); - if (originalAuth) { - context.rawRequest.headers["authorization"] = originalAuth; - delete context.rawRequest.headers[HttpConstants.ORIGINAL_AUTH_HEADER]; - } - } - return handler(data, context); - }; - - return newHandler; } function getDefaultConfig(): any { return JSON.parse(process.env.FIREBASE_CONFIG || "{}"); } -function initializeRuntimeConfig() { - // Most recent version of Firebase Functions SDK automatically picks up locally - // stored .runtimeconfig.json to populate the config entries. - // However, due to a bug in some older version of the Function SDK, this process may fail. - // - // See the following issues for more detail: - // https://github.com/firebase/firebase-tools/issues/3793 - // https://github.com/firebase/firebase-functions/issues/877 - // - // As a workaround, the emulator runtime will load the contents of the .runtimeconfig.json - // to the CLOUD_RUNTIME_CONFIG environment variable IF the env var is unused. - // In the future, we will bump up the minimum version of the Firebase Functions SDK - // required to run the functions emulator to v3.15.1 and get rid of this workaround. - if (!process.env.CLOUD_RUNTIME_CONFIG) { - const configPath = `${process.cwd()}/.runtimeconfig.json`; - try { - const configContent = fs.readFileSync(configPath, "utf8"); - if (configContent) { - try { - JSON.parse(configContent.toString()); - logDebug(`Found local functions config: ${configPath}`); - process.env.CLOUD_RUNTIME_CONFIG = configContent.toString(); - } catch (e) { - new EmulatorLog("SYSTEM", "function-runtimeconfig-json-invalid", "").log(); - } - } - } catch (e) { - // Ignore, config is optional - } - } -} - /** * This stub is the most important and one of the only non-optional stubs.This feature redirects * writes from the admin SDK back into emulated resources. @@ -709,48 +597,6 @@ function warnAboutStorageProd(): void { ).log(); } -async function initializeFunctionsConfigHelper(): Promise { - const functionsResolution = await assertResolveDeveloperNodeModule("firebase-functions"); - const localFunctionsModule = require(functionsResolution.resolution); - - logDebug("Checked functions.config()", { - config: localFunctionsModule.config(), - }); - - const originalConfig = localFunctionsModule.config(); - const proxiedConfig = new Proxied(originalConfig) - .any((parentConfig, parentKey) => { - const isInternal = parentKey.startsWith("Symbol(") || parentKey.startsWith("inspect"); - if (!parentConfig[parentKey] && !isInternal) { - new EmulatorLog("SYSTEM", "functions-config-missing-value", "", { - key: parentKey, - }).log(); - } - - return parentConfig[parentKey]; - }) - .finalize(); - - const functionsModuleProxy = new Proxied(localFunctionsModule); - const proxiedFunctionsModule = functionsModuleProxy - .when("config", () => () => { - return proxiedConfig; - }) - .finalize(); - - // Stub the functions module in the require cache - const v = require.cache[functionsResolution.resolution]; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is not precedent. - require.cache[functionsResolution.resolution] = Object.assign(v!, { - exports: proxiedFunctionsModule, - path: path.dirname(functionsResolution.resolution), - }); - - logDebug("firebase-functions has been stubbed.", { - functionsResolution, - }); -} - /* Retains a reference to the raw body buffer to allow access to the raw body for things like request signature validation. This is used as the "verify" function in body-parser options. @@ -897,9 +743,7 @@ async function initializeRuntime(): Promise { return; } - initializeRuntimeConfig(); initializeNetworkFiltering(); - await initializeFunctionsConfigHelper(); await initializeFirebaseFunctionsStubs(); await initializeFirebaseAdminStubs(); } diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 033fe4db335..dfddd8a0b58 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -106,11 +106,6 @@ export interface FunctionsRuntimeFeatures { timeout?: boolean; } -export class HttpConstants { - static readonly CALLABLE_AUTH_HEADER: string = "x-callable-context-auth"; - static readonly ORIGINAL_AUTH_HEADER: string = "x-original-auth"; -} - export class EmulatedTrigger { /* Here we create a trigger from a single definition (data about what resources does this trigger on, etc) and