diff --git a/desktop/flipper-server-client/src/FlipperServerClient.tsx b/desktop/flipper-server-client/src/FlipperServerClient.tsx index 670a9c2ce..56e29a0ca 100644 --- a/desktop/flipper-server-client/src/FlipperServerClient.tsx +++ b/desktop/flipper-server-client/src/FlipperServerClient.tsx @@ -30,9 +30,10 @@ export type {FlipperServer, FlipperServerCommands, FlipperServerExecOptions}; export function createFlipperServer( host: string, port: number, + args: string, onStateChange: (state: FlipperServerState) => void, ): Promise { - const socket = new ReconnectingWebSocket(`ws://${host}:${port}`); + const socket = new ReconnectingWebSocket(`ws://${host}:${port}${args}`); return createFlipperServerWithSocket(socket as WebSocket, onStateChange); } diff --git a/desktop/flipper-server-core/package.json b/desktop/flipper-server-core/package.json index 8384efd3c..a37668370 100644 --- a/desktop/flipper-server-core/package.json +++ b/desktop/flipper-server-core/package.json @@ -28,6 +28,7 @@ "http-proxy": "^1.18.1", "invariant": "^2.2.4", "js-base64": "^3.7.5", + "jsonwebtoken": "^9.0.0", "lodash.memoize": "^4.1.2", "memorystream": "^0.3.1", "node-fetch": "2", @@ -53,6 +54,7 @@ "@types/express": "^4.17.13", "@types/http-proxy": "^1.17.8", "@types/invariant": "^2.2.35", + "@types/jsonwebtoken": "^9.0.1", "@types/memorystream": "^0.3.0", "@types/node": "^17.0.31", "@types/node-fetch": "2", diff --git a/desktop/flipper-server-core/src/index.tsx b/desktop/flipper-server-core/src/index.tsx index f556aa793..a9cbf3e45 100644 --- a/desktop/flipper-server-core/src/index.tsx +++ b/desktop/flipper-server-core/src/index.tsx @@ -21,3 +21,5 @@ export * from './server/utilities'; export {isFBBuild} from './fb-stubs/constants'; export {WEBSOCKET_MAX_MESSAGE_SIZE} from './comms/ServerWebSocket'; + +export {getAuthToken} from './utils/certificateUtils'; diff --git a/desktop/flipper-server-core/src/server/startServer.tsx b/desktop/flipper-server-core/src/server/startServer.tsx index 8342a2b10..cf0840763 100644 --- a/desktop/flipper-server-core/src/server/startServer.tsx +++ b/desktop/flipper-server-core/src/server/startServer.tsx @@ -23,6 +23,7 @@ import exitHook from 'exit-hook'; import {attachSocketServer} from './attachSocketServer'; import {FlipperServerImpl} from '../FlipperServerImpl'; import {FlipperServerCompanionEnv} from 'flipper-server-companion'; +import {validateAuthToken} from '../utils/certificateUtils'; type Config = { port: number; @@ -36,10 +37,36 @@ type ReadyForConnections = ( companionEnv: FlipperServerCompanionEnv, ) => Promise; +const verifyAuthToken = (req: http.IncomingMessage): boolean => { + let token: string | null = null; + if (req.url) { + const url = new URL(req.url, `http://${req.headers.host}`); + token = url.searchParams.get('token'); + } + + if (!token && req.headers['x-access-token']) { + token = req.headers['x-access-token'] as string; + } + + if (!token) { + console.warn('[conn] A token is required for authentication'); + return false; + } + + try { + validateAuthToken(token); + console.info('[conn] Token was successfully validated'); + } catch (err) { + console.warn('[conn] An invalid token was supplied for authentication'); + return false; + } + return true; +}; + /** - * Orchestrates the creation of the HTTP server, proxy, and web socket. + * Orchestrates the creation of the HTTP server, proxy, and WS server. * @param config Server configuration. - * @returns Returns a promise to the created server, proxy and web socket. + * @returns Returns a promise to the created server, proxy and WS server. */ export async function startServer(config: Config): Promise<{ app: Express; @@ -217,8 +244,9 @@ function addWebsocket(server: http.Server, config: Config) { possibleHosts.includes(req.headers.host) ) { // no origin header? The request is not originating from a browser, so should be OK to pass through - // If origin matches our own address, it means we are serving the page - return true; + // If origin matches our own address, it means we are serving the page. + + return verifyAuthToken(req); } else { // for now we don't allow cross origin request, so that an arbitrary website cannot try to // connect a socket to localhost:serverport, and try to use the all powerful Flipper APIs to read diff --git a/desktop/flipper-server-core/src/utils/certificateUtils.tsx b/desktop/flipper-server-core/src/utils/certificateUtils.tsx index 66b416080..682b587e0 100644 --- a/desktop/flipper-server-core/src/utils/certificateUtils.tsx +++ b/desktop/flipper-server-core/src/utils/certificateUtils.tsx @@ -9,6 +9,7 @@ import {promisify} from 'util'; import fs from 'fs-extra'; +import os from 'os'; import { openssl, isInstalled as opensslInstalled, @@ -18,13 +19,15 @@ import tmp, {FileOptions} from 'tmp'; import {reportPlatformFailures} from 'flipper-common'; import {isTest} from 'flipper-common'; import {flipperDataFolder} from './paths'; +import * as jwt from 'jsonwebtoken'; +import {getFlipperServerConfig} from '../FlipperServerConfig'; const tmpFile = promisify(tmp.file) as ( options?: FileOptions, ) => Promise; -const getFilePath = (fileName: string): string => { - return path.resolve(flipperDataFolder, 'certs', fileName); +const getFilePath = (filename: string): string => { + return path.resolve(flipperDataFolder, 'certs', filename); }; // Desktop file paths @@ -69,16 +72,23 @@ export const ensureOpenSSLIsAvailable = async (): Promise => { } }; +let serverConfig: SecureServerConfig | undefined; export const loadSecureServerConfig = async (): Promise => { + if (serverConfig) { + return serverConfig; + } + await ensureOpenSSLIsAvailable(); await certificateSetup(); - return { + await generateAuthToken(); + serverConfig = { key: await fs.readFile(serverKey), cert: await fs.readFile(serverCert), ca: await fs.readFile(caCert), requestCert: true, rejectUnauthorized: true, // can be false if necessary as we don't strictly need to verify the client }; + return serverConfig; }; export const extractAppNameFromCSR = async (csr: string): Promise => { @@ -254,3 +264,44 @@ const writeToTempFile = async (content: string): Promise => { await fs.writeFile(path, content); return path; }; + +const getStaticFilePath = (filename: string): string => { + return path.resolve(getFlipperServerConfig().paths.staticPath, filename); +}; + +const tokenFilename = 'auth.token'; +const getTokenPath = (): string => { + const path = getStaticFilePath(tokenFilename); + return path; +}; + +export const generateAuthToken = async () => { + const privateKey = await fs.readFile(serverKey); + const token = jwt.sign({unixname: os.userInfo().username}, privateKey, { + algorithm: 'RS256', + expiresIn: '21 days', + }); + + await fs.writeFile(getTokenPath(), token); + + return token; +}; + +export const getAuthToken = async () => { + if (!(await fs.pathExists(getTokenPath()))) { + return generateAuthToken(); + } + + const token = await fs.readFile(getTokenPath()); + return token.toString(); +}; + +export const validateAuthToken = (token: string) => { + if (!serverConfig) { + throw new Error( + 'Unable to validate auth token as no server configuration is available', + ); + } + + jwt.verify(token, serverConfig.cert); +}; diff --git a/desktop/flipper-server/src/index.tsx b/desktop/flipper-server/src/index.tsx index 5c5f425b0..e06b3f980 100644 --- a/desktop/flipper-server/src/index.tsx +++ b/desktop/flipper-server/src/index.tsx @@ -21,6 +21,7 @@ import {initCompanionEnv} from 'flipper-server-companion'; import {startFlipperServer, startServer} from 'flipper-server-core'; import {isTest} from 'flipper-common'; import exitHook from 'exit-hook'; +import {getAuthToken} from 'flipper-server-core'; const argv = yargs .usage('yarn flipper-server [args]') @@ -163,7 +164,7 @@ process.on('unhandledRejection', (reason, promise) => { }); start() - .then(() => { + .then(async () => { if (!argv.tcp) { console.log('Flipper server started and listening'); return; @@ -171,7 +172,8 @@ start() console.log( 'Flipper server started and listening at port ' + chalk.green(argv.port), ); - const url = `http://localhost:${argv.port}`; + const token = await getAuthToken(); + const url = `http://localhost:${argv.port}?token=${token}`; console.log('Go to: ' + chalk.green(chalk.bold(url))); if (argv.open) { open(url); diff --git a/desktop/flipper-ui-browser/src/index.tsx b/desktop/flipper-ui-browser/src/index.tsx index 9321f4c69..cd5456030 100644 --- a/desktop/flipper-ui-browser/src/index.tsx +++ b/desktop/flipper-ui-browser/src/index.tsx @@ -30,6 +30,7 @@ async function start() { const flipperServer = await createFlipperServer( location.hostname, parseInt(location.port, 10), + location.search, (state: FlipperServerState) => { switch (state) { case FlipperServerState.CONNECTING: diff --git a/desktop/static/index.web.dev.html b/desktop/static/index.web.dev.html index 45db413aa..2e0152b35 100644 --- a/desktop/static/index.web.dev.html +++ b/desktop/static/index.web.dev.html @@ -43,6 +43,7 @@ left: 0; position: absolute; } + @@ -59,85 +60,85 @@ diff --git a/desktop/yarn.lock b/desktop/yarn.lock index e9caa393c..6875baaf8 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -3789,6 +3789,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== + dependencies: + "@types/node" "*" + "@types/lockfile@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.2.tgz#3f77e84171a2b7e3198bd5717c7547a54393baf8" @@ -5504,6 +5511,11 @@ buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-equal@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" @@ -6800,6 +6812,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -10361,6 +10380,16 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -10399,6 +10428,23 @@ jszip@^3.10.1: readable-stream "~2.3.6" setimmediate "^1.0.5" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"