diff --git a/src/utils/CertificateProvider.js b/src/utils/CertificateProvider.js index 04b48e444..ee2f4fcd6 100644 --- a/src/utils/CertificateProvider.js +++ b/src/utils/CertificateProvider.js @@ -331,11 +331,7 @@ export default class CertificateProvider { csr: string, ): Promise { return androidUtil - .executeCommandAsApp( - deviceId, - processName, - `cat ${directory + csrFileName}`, - ) + .pull(deviceId, processName, directory + csrFileName) .then(deviceCsr => { return this.santitizeString(deviceCsr.toString()) === csr; }); diff --git a/src/utils/androidContainerUtility.js b/src/utils/androidContainerUtility.js index 2577e89a9..da1455c77 100644 --- a/src/utils/androidContainerUtility.js +++ b/src/utils/androidContainerUtility.js @@ -4,58 +4,37 @@ * LICENSE file in the root directory of this source tree. * @format */ -import {getAdbClient} from './adbClient'; -const adbkit = require('adbkit-fb'); - -const logTag = 'androidContainerUtility'; -const appNotDebuggableRegex = /debuggable/; -const allowedAppNameRegex = /^[a-zA-Z0-9._\-]+$/; -const operationNotPermittedRegex = /not permitted/; - -const adb = getAdbClient(); - -export function executeCommandAsApp( - deviceId: string, - app: string, - command: string, -): Promise { - if (!app.match(allowedAppNameRegex)) { - return Promise.reject(new Error(`Disallowed run-as user: ${app}`)); - } - if (command.match(/[']/)) { - return Promise.reject(new Error(`Disallowed escaping command: ${command}`)); - } - return adb - .then(client => - client.shell(deviceId, `echo '${command}' | run-as '${app}'`), - ) - .then(adbkit.util.readAll) - .then(buffer => buffer.toString()) - .then(output => { - if (output.match(appNotDebuggableRegex)) { - throw new Error( - `Android app ${app} is not debuggable. To use it with Flipper, add android:debuggable="true" to the application section of AndroidManifest.xml`, - ); - } - if (output.toLowerCase().match(operationNotPermittedRegex)) { - throw new Error( - `Your android device (${deviceId}) does not support the adb shell run-as command. We're tracking this at https://github.com/facebook/flipper/issues/92`, - ); - } - return output; - }); -} +import { + validateAppName, + validateFilePath, + validateFileContent, + _push, + _pull, +} from './androidContainerUtilityInternal'; export function push( deviceId: string, app: string, - filename: string, + filepath: string, contents: string, ): Promise { - console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag); - return executeCommandAsApp( - deviceId, - app, - `echo "${contents}" > ${filename} && chmod 600 ${filename}`, - ).then(output => undefined); + return validateAppName(app).then(validApp => + validateFilePath(filepath).then(validFilepath => + validateFileContent(contents).then(validContent => + _push(deviceId, validApp, validFilepath, validContent), + ), + ), + ); +} + +export function pull( + deviceId: string, + app: string, + path: string, +): Promise { + return validateAppName(app).then(validApp => + validateFilePath(path).then(validPath => + _pull(deviceId, validApp, validPath), + ), + ); } diff --git a/src/utils/androidContainerUtilityInternal.js b/src/utils/androidContainerUtilityInternal.js new file mode 100644 index 000000000..01fa5ac5b --- /dev/null +++ b/src/utils/androidContainerUtilityInternal.js @@ -0,0 +1,98 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +/* + * This file is intentionally separate from androidContainerUtility so the + * opaque types will ensure the commands are only ever run on validated + * arguments. + */ +import {getAdbClient} from './adbClient'; +const adbkit = require('adbkit-fb'); + +const allowedAppNameRegex = /^[a-zA-Z0-9._\-]+$/; +const appNotDebuggableRegex = /debuggable/; +const operationNotPermittedRegex = /not permitted/; +const logTag = 'androidContainerUtility'; + +const adb = getAdbClient(); + +export opaque type AppName = string; +export opaque type Command = string; +export opaque type FilePath = string; +export opaque type FileContent = string; + +export function validateAppName(app: string): Promise { + if (app.match(allowedAppNameRegex)) { + return Promise.resolve(app); + } + return Promise.reject(new Error(`Disallowed run-as user: ${app}`)); +} + +export function validateFilePath(filePath: string): Promise { + if (!filePath.match(/[']/)) { + return Promise.resolve(filePath); + } + return Promise.reject(new Error(`Disallowed escaping filepath: ${filePath}`)); +} + +export function validateFileContent(content: string): Promise { + if (!content.match(/["]/)) { + return Promise.resolve(content); + } + return Promise.reject( + new Error(`Disallowed escaping file content: ${content}`), + ); +} + +export function _push( + deviceId: string, + app: AppName, + filename: FilePath, + contents: FileContent, +): Promise { + console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag); + return executeCommandAsApp( + deviceId, + app, + `echo "${contents}" > '${filename}' && chmod 600 '${filename}'`, + ).then(output => undefined); +} + +export function _pull( + deviceId: string, + app: AppName, + path: FilePath, +): Promise { + return executeCommandAsApp(deviceId, app, `cat '${path}'`); +} + +// Keep this method private since it relies on pre-validated arguments +function executeCommandAsApp( + deviceId: string, + app: string, + command: string, +): Promise { + return adb + .then(client => + client.shell(deviceId, `echo '${command}' | run-as '${app}'`), + ) + .then(adbkit.util.readAll) + .then(buffer => buffer.toString()) + .then(output => { + if (output.match(appNotDebuggableRegex)) { + throw new Error( + `Android app ${app} is not debuggable. To use it with Flipper, add android:debuggable="true" to the application section of AndroidManifest.xml`, + ); + } + if (output.toLowerCase().match(operationNotPermittedRegex)) { + throw new Error( + `Your android device (${deviceId}) does not support the adb shell run-as command. We're tracking this at https://github.com/facebook/flipper/issues/92`, + ); + } + return output; + }); +}