From d1ed676a489f477e032b182c56fe41c4811b2c29 Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Fri, 13 May 2022 03:19:47 -0700 Subject: [PATCH] Remove dynamic dependencies from flipper-server Summary: Currently, Flipper Server has a few productions dependencies (mac-ca, node-fetch) that are not bundled with the Flipper Server. It makes it harder to distribute Flipper Server, as now all potential consumers need not only to download the bundle, but also install these additional dependencies. This diff makes it possible to bundle `mac-ca` and `node-fetch` with Flipper Server. As a result, Flipper Server becomes dependency-free in production. Reviewed By: lblasa Differential Revision: D36345213 fbshipit-source-id: 2cd6ba1b3301b45dc2295891964ba020fd107586 --- desktop/app/src/init.tsx | 9 -- .../src/electron-requires.tsx | 4 +- .../src/prefixed-node-requires.tsx | 40 ++++++ .../babel-transformer/src/transform-app.tsx | 1 + .../src/transform-plugin.tsx | 1 + .../src/transform-server-add-on.tsx | 1 + .../src/transform-server-prod.tsx | 1 + desktop/flipper-server-core/package.json | 2 + .../src/FlipperServerImpl.tsx | 1 + .../flipper-server-core/src/utils/macCa.tsx | 128 ++++++++++++++++++ desktop/flipper-server/package.json | 16 +-- .../flipper-server/src/startFlipperServer.tsx | 9 -- desktop/static/package.json | 1 - desktop/yarn.lock | 14 +- 14 files changed, 190 insertions(+), 38 deletions(-) create mode 100644 desktop/babel-transformer/src/prefixed-node-requires.tsx create mode 100644 desktop/flipper-server-core/src/utils/macCa.tsx diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 4513918a5..0bd8b60e9 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -41,15 +41,6 @@ import fs from 'fs-extra'; enableMapSet(); -if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { - // By default Node.JS has its internal certificate storage and doesn't use - // the system store. Because of this, it's impossible to access ondemand / devserver - // which are signed using some internal self-issued FB certificates. These certificates - // are automatically installed to MacOS system store on FB machines, so here we're using - // this "mac-ca" library to load them into Node.JS. - electronRequire('mac-ca'); -} - async function start() { const app = remote.app; const execPath = process.execPath || remote.process.execPath; diff --git a/desktop/babel-transformer/src/electron-requires.tsx b/desktop/babel-transformer/src/electron-requires.tsx index 4adca0f06..ec59ff3b2 100644 --- a/desktop/babel-transformer/src/electron-requires.tsx +++ b/desktop/babel-transformer/src/electron-requires.tsx @@ -46,10 +46,10 @@ export const BUILTINS = [ 'repl', 'timers', 'perf_hooks', + 'worker_threads', + 'encoding', 'fsevents', './fsevents.node', - // MWE node-fetch looks strange here, not sure what the effect of changing that would be - 'node-fetch', // jest is referred to in source code, like in TestUtils, but we don't want to ever bundle it up! 'jest', '@testing-library/react', diff --git a/desktop/babel-transformer/src/prefixed-node-requires.tsx b/desktop/babel-transformer/src/prefixed-node-requires.tsx new file mode 100644 index 000000000..b7957e125 --- /dev/null +++ b/desktop/babel-transformer/src/prefixed-node-requires.tsx @@ -0,0 +1,40 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {CallExpression} from '@babel/types'; +import {NodePath} from '@babel/traverse'; + +// Core modules can be required as `node:fs` to bypass teh require cache +// https://nodejs.org/api/modules.html#core-modules +// It is not supported for "require" until Node v14.18. +// TODO: Remove this transform when we upgrade electron and node.js +const bypassRequireCachePrefix = 'node:'; + +module.exports = () => ({ + name: 'change-require-to-electronRequire-in-electron-app', + visitor: { + CallExpression(path: NodePath) { + const node = path.node; + if ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments[0].type === 'StringLiteral' + ) { + const source = node.arguments[0].value; + if (source.startsWith(bypassRequireCachePrefix)) { + node.arguments[0].value = source.substring( + bypassRequireCachePrefix.length, + ); + } + } + }, + }, +}); diff --git a/desktop/babel-transformer/src/transform-app.tsx b/desktop/babel-transformer/src/transform-app.tsx index 022d3ae11..c5e4cb1e9 100644 --- a/desktop/babel-transformer/src/transform-app.tsx +++ b/desktop/babel-transformer/src/transform-app.tsx @@ -13,6 +13,7 @@ import {default as getCacheKey} from './get-cache-key'; const presets = [require('@babel/preset-react')]; const plugins = [ require('./fsevents-dynamic-imports'), + require('./prefixed-node-requires'), require('./electron-requires'), require('./import-react'), require('./app-flipper-requires'), diff --git a/desktop/babel-transformer/src/transform-plugin.tsx b/desktop/babel-transformer/src/transform-plugin.tsx index d8ad39836..bce751feb 100644 --- a/desktop/babel-transformer/src/transform-plugin.tsx +++ b/desktop/babel-transformer/src/transform-plugin.tsx @@ -12,6 +12,7 @@ import {default as doTransform} from './transform'; const presets = [require('@babel/preset-react')]; const plugins = [ require('./fsevents-dynamic-imports'), + require('./prefixed-node-requires'), require('./electron-requires'), require('./plugin-flipper-requires'), require('./fb-stubs'), diff --git a/desktop/babel-transformer/src/transform-server-add-on.tsx b/desktop/babel-transformer/src/transform-server-add-on.tsx index 071e68069..a833332c6 100644 --- a/desktop/babel-transformer/src/transform-server-add-on.tsx +++ b/desktop/babel-transformer/src/transform-server-add-on.tsx @@ -25,6 +25,7 @@ const presets = [ const plugins = [ require('./fsevents-dynamic-imports'), + require('./prefixed-node-requires'), require('./electron-requires'), require('./plugin-flipper-requires'), require('./fb-stubs'), diff --git a/desktop/babel-transformer/src/transform-server-prod.tsx b/desktop/babel-transformer/src/transform-server-prod.tsx index 2fc6df3bd..6549af44e 100644 --- a/desktop/babel-transformer/src/transform-server-prod.tsx +++ b/desktop/babel-transformer/src/transform-server-prod.tsx @@ -29,6 +29,7 @@ const presets = [ // (which effectively makes them external, as electronRequire === require, but not rolled up with Metro) const plugins = [ require('./fsevents-dynamic-imports'), + require('./prefixed-node-requires'), require('./electron-requires'), require('./plugin-flipper-requires'), require('./fb-stubs'), diff --git a/desktop/flipper-server-core/package.json b/desktop/flipper-server-core/package.json index 8a4227ab6..84b24d731 100644 --- a/desktop/flipper-server-core/package.json +++ b/desktop/flipper-server-core/package.json @@ -27,6 +27,7 @@ "js-base64": "^3.7.2", "lodash.memoize": "^4.1.2", "node-fetch": "^3.2.4", + "node-forge": "^0.10.0", "open": "^8.3.0", "openssl-wrapper": "^0.3.4", "promisify-child-process": "^4.1.1", @@ -48,6 +49,7 @@ "@types/invariant": "^2.2.35", "@types/memorystream": "^0.3.0", "@types/node": "^17.0.31", + "@types/node-forge": "^0.10", "@types/rimraf": "^3.0.2", "@types/rsocket-core": "^0.0.7", "@types/rsocket-tcp-server": "^0.0.2", diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index 43afbfb64..957ab72f2 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -7,6 +7,7 @@ * @format */ +import './utils/macCa'; import './utils/fetch-polyfill'; import EventEmitter from 'events'; import {ServerController} from './comms/ServerController'; diff --git a/desktop/flipper-server-core/src/utils/macCa.tsx b/desktop/flipper-server-core/src/utils/macCa.tsx new file mode 100644 index 000000000..d8f6d4d6b --- /dev/null +++ b/desktop/flipper-server-core/src/utils/macCa.tsx @@ -0,0 +1,128 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// Copy-paste from https://github.com/jfromaniello/mac-ca +// Babel does not want to transform https://github.com/jfromaniello/mac-ca/blob/fb2b2824c91e3c7f7ffdc8329dd8992e172b967b/lib/formatter.js#L2 because "package" is a reserved word +// It is easier to copy the package then add ye another babel transform to change the reserved word on the fly + +import https from 'https'; +import forge from 'node-forge'; + +const validFormats = { + der: 0, + pem: 1, + txt: 2, + asn1: 3, +}; + +function myASN(pem: any) { + const der = forge.pki.pemToDer(pem); + const asn1 = forge.asn1; + // @ts-expect-error + let crt = asn1.fromDer(der.data.toString('binary')).value[0].value; + const serial = crt[0]; + const hasSerial = + serial.tagClass === asn1.Class.CONTEXT_SPECIFIC && + serial.type === 0 && + serial.constructed; + crt = crt.slice(hasSerial); + return { + serial: crt[0], + issuer: crt[2], + valid: crt[3], + subject: crt[4], + }; +} + +function txtFormat(pem: string) { + const crt = myASN(pem); + const d = new Date(); + return `Subject\t${crt.subject.value + .map((rdn: any) => rdn.value[0].value[1].value) + .join('/')} +Valid\t${crt.valid.value.map((date: any) => date.value).join(' - ')} +Saved\t${d.toLocaleDateString()} ${d + .toTimeString() + .replace(/\s*\(.*\)\s*/, '')} by mac-ca +${pem}`; +} + +const transform = function (format: any) { + return function (pem: any) { + try { + switch (format) { + case validFormats.der: + return forge.pki.pemToDer(pem); + case validFormats.pem: + return pem; + case validFormats.txt: + return txtFormat(pem); + case validFormats.asn1: + return myASN(pem); + default: + return forge.pki.certificateFromPem(pem); + } + } catch (er) { + return; + } + }; +}; + +if (process.platform !== 'darwin') { + module.exports.all = () => []; + module.exports.each = () => {}; +} else { + const child_process = require('child_process'); + + const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g; + const systemRootCertsPath = + '/System/Library/Keychains/SystemRootCertificates.keychain'; + const args = ['find-certificate', '-a', '-p']; + + // eslint-disable-next-line node/no-sync + const allTrusted = child_process + .spawnSync('/usr/bin/security', args) + .stdout.toString() + .split(splitPattern); + + // eslint-disable-next-line node/no-sync + const allRoot = child_process + .spawnSync('/usr/bin/security', args.concat(systemRootCertsPath)) + .stdout.toString() + .split(splitPattern); + + https.globalAgent.options.ca = https.globalAgent.options.ca || []; + + const ca = https.globalAgent.options.ca; + + function duplicated(cert: any, index: any, arr: any) { + return arr.indexOf(cert) === index; + } + + const all = allTrusted.concat(allRoot); + + all.filter(duplicated).forEach((cert: any) => (ca as any).push(cert)); + + module.exports.der2 = validFormats; + + module.exports.all = function (format: any) { + return all.map(transform(format)).filter((c: any) => c); + }; + + module.exports.each = function (format: any, callback: any) { + if (typeof format === 'function') { + callback = format; + format = undefined; + } + return all + .map(transform(format)) + .filter((c: any) => c) + .forEach(callback); + }; +} diff --git a/desktop/flipper-server/package.json b/desktop/flipper-server/package.json index e5f3964f0..67d95004b 100644 --- a/desktop/flipper-server/package.json +++ b/desktop/flipper-server/package.json @@ -8,19 +8,13 @@ "bin": "server.js", "license": "MIT", "bugs": "https://github.com/facebook/flipper/issues", - "dependenciesComment": "mac-ca is required dynamically for darwin, node-fetch is treated special in electron-requires, not sure why", - "dependencies": { - "exit-hook": "^2.1.1", - "http-proxy": "^1.18.1", - "mac-ca": "^1.0.6", - "node-fetch": "^2.6.7", - "ws": "^8.5.0", - "xdg-basedir": "^4" - }, + "dependencies": {}, "devDependencies": { "@types/express": "^4.17.13", "@types/http-proxy": "^1.17.8", "@types/node": "^17.0.31", + "exit-hook": "^2.1.1", + "http-proxy": "^1.18.1", "chalk": "^4", "express": "^4.17.3", "flipper-common": "0.0.0", @@ -31,7 +25,9 @@ "metro": "^0.70.2", "open": "^8.3.0", "p-filter": "^2.1.0", - "yargs": "^17.0.1" + "yargs": "^17.0.1", + "ws": "^8.5.0", + "xdg-basedir": "^4" }, "peerDependencies": {}, "scripts": { diff --git a/desktop/flipper-server/src/startFlipperServer.tsx b/desktop/flipper-server/src/startFlipperServer.tsx index 906f73ca6..a3371747b 100644 --- a/desktop/flipper-server/src/startFlipperServer.tsx +++ b/desktop/flipper-server/src/startFlipperServer.tsx @@ -40,15 +40,6 @@ export async function startFlipperServer( settingsString: string, enableLauncherSettings: boolean, ): Promise { - if (os.platform() === 'darwin') { - // By default Node.JS has its internal certificate storage and doesn't use - // the system store. Because of this, it's impossible to access ondemand / devserver - // which are signed using some internal self-issued FB certificates. These certificates - // are automatically installed to MacOS system store on FB machines, so here we're using - // this "mac-ca" library to load them into Node.JS. - electronRequire('mac-ca'); - } - const execPath = process.execPath; const appPath = rootDir; const isProduction = diff --git a/desktop/static/package.json b/desktop/static/package.json index e07bfc79e..25303cb7a 100644 --- a/desktop/static/package.json +++ b/desktop/static/package.json @@ -7,7 +7,6 @@ "dependencies": { "electron-devtools-installer": "^3.2.0", "fix-path": "^3.0.0", - "mac-ca": "^1.0.6", "mkdirp": "^1.0.4", "node-fetch": "^2.6.7", "ws": "^8.5.0", diff --git a/desktop/yarn.lock b/desktop/yarn.lock index da3566b76..2cd2ed904 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -2764,6 +2764,13 @@ "@types/node" "*" form-data "^3.0.0" +"@types/node-forge@^0.10": + version "0.10.10" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.10.10.tgz#07ffccf0f7f3ebb97de67446555912803be50e7b" + integrity sha512-iixn5bedlE9fm/5mN7fPpXraXlxCVrnNWHZekys8c5fknridLVWGnNRqlaWpenwaijIuB3bNI0lEOm+JD6hZUA== + dependencies: + "@types/node" "*" + "@types/node@*": version "16.11.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.0.tgz#4b95f2327bacd1ef8f08d8ceda193039c5d7f52e" @@ -8806,13 +8813,6 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= -mac-ca@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/mac-ca/-/mac-ca-1.0.6.tgz#89860edfeebcc4593567044281ab3500961ec15f" - integrity sha512-uuCaT+41YtIQlDDvbigP1evK1iUk97zRirP9+8rZJz8x0eIQZG8Z7YQegMTsCiMesLPb6LBgCS95uyAvVA1tmg== - dependencies: - node-forge "^0.10.0" - make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"